前端面试-经典的Vue面试题
面试总结三大模块:Vue双向绑定及原理、生命周期、组件通信、Vue官方API
目录:1.Vue双向绑定及原理
1.1你对MVVM是怎么理解的?
1.2你对Vue响应式原理是怎么理解的?是否可以实现一个简版的?Vue2中是如何监听数组的变化的?Vue3使用Proxy重写,相比Vue2的Object.defineProperty,有哪些优势?
1.2.1响应式原理
1.2.2 Vue2 Object.defineProperty简版实现代码
1.2.3 针对弊端解决 无法监听数组问题:Vue2中是如何监听数组的变化的?
1.2.4 针对弊端解决 无法劫持对象的新增属性:this.$set能够解决对象新增属性问题。延伸问题:$set为啥能监测数组变动?
1.2.5 Vue3 proxy简版实现代码
1.3你对Vue的v-model双向绑定是怎么理解的?是否可以实现一个简版的
1.3.1 v-model双向绑定理解
1.3.2 v-model双向绑定实现原理代码简版
1.3.3 双向绑定原理
2.生命周期
2.1Vue有哪些生命周期?它们有哪些使用场景?你的接口请求一般都放在哪个生命周期方法中,为什么?你的获取dom的方法一般都放在哪个生命周期方法中,为什么?
2.1.1vue3中的选项式生命周期钩子及使用场景
$emit
向父组件触发一个事件,父组件监听这个事件就行了1.你对MVVM是怎么理解的?
MVVM是Model-View-ViewModel
缩写。Model层代表数据模型,View代表视图层,ViewModel是MVVM的核心,它是连接View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据,实现数据的双向绑定。可以不用再去低效又麻烦地通过操纵 DOM 去更新视图,专心处理和维护 ViewModel层。
2.你对Vue响应式原理是怎么理解的?是否可以实现一个简版的?Vue2中是如何监听数组的变化的?Vue3使用Proxy重写,相比Vue2的Object.defineProperty,有哪些优势?
2.1响应式原理
- Vue2的响应式是基于
Object.defineProperty
实现的 - Vue3的响应式是基于ES6的
Proxy
来实现的
2.2 Vue2 Object.defineProperty简版实现代码
// 响应式函数 function reactive(obj, key, value) { Object.defineProperty(data, key, { get() { console.log(`访问了${key}属性`) return value }, set(val) { console.log(`将${key}由->${value}->设置成->${val}`) if (value !== val) { value = val } } }) } const data = { name: '林三心', age: 22 } Object.keys(data).forEach(key => reactive(data, key, data[key])) console.log(data.name) // 访问了name属性 // 林三心 data.name = 'sunshine_lin' // 将name由->林三心->设置成->sunshine_lin console.log(data.name) // 访问了name属性 // sunshine_lin
弊端:1.data新增了hobby
属性,进行访问和设值,但是都不会触发get和set
,所以弊端就是:Object.defineProperty
只对初始对象里的属性有监听作用,而对新增的属性无效。这也是为什么Vue2中对象新增属性的修改需要使用Vue.$set
来设值的原因;2.不能对数组数据进行劫持,无法监测到数组长度变化,需重写数组方法。
// 接着上面代码 data.hobby = '打篮球' console.log(data.hobby) // 打篮球 data.hobby = '打游戏' console.log(data.hobby) // 打游戏
2.3 针对弊端解决 无法监听数组问题:Vue2中是如何监听数组的变化的?
import { def } from '../util/index' const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { // 缓存原来的方法 const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() return result }) })
看来Vue能对数组进行监听的原因是,把数组的方法重写了。总结起来就是这几步:
01先获取原生 Array 的原型方法,因为拦截后还是需要原生的方法帮我们实现数组的变化。
02对 Array 的原型方法使用 Object.defineProperty 做一些拦截操作。
03把需要被拦截的 Array 类型的数据原型指向改造后原型
2.4 针对弊端解决 无法劫持对象的新增属性:this.$set能够解决对象新增属性问题。延伸问题:$set为啥能监测数组变动?
function set (target, key, val) { //... if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key); target.splice(key, 1, val); return val } if (key in target && !(key in Object.prototype)) { target[key] = val; return val } //... defineReactive$$1(ob.value, key, val); ob.dep.notify(); return val }
- 如果target是一个数组且索引有效,就设置length的属性。
- 通过splice方法把value设置到target数组指定位置。
- 设置的时候,vue会拦截到target发生变化,然后把新增的value也变成响应式
- 最后返回value
这就是vue重写数组方法的原因,利用数组这些方法触发使得每一项的value都是响应式的。
2.5 Vue3 proxy简版实现代码(优势是解决了弊端)
target:源对象 prop:要进行操作的属性 value:set操作时的最新值
js是单线程遇到错误会直接挂机,Reflect兼容性更好,否则需要使用try catch捕获错误
3.你对Vue的v-model双向绑定是怎么理解的?是否可以实现一个简版的?
3.1 v-model双向绑定理解
v-model可以实现表单元素和数据之间的双向绑定,即修改表单的值,data中对应变量的值也会被修改。在data对应变量中修改值,被绑定的表单值也会被修改;所以称之为双向绑定。
3.2 v-model双向绑定实现原理代码简版
使用v-bind将文本框的value绑定给数据变量,实现变量改变,文本框值也随之改变的单向绑定;然后使用v-on绑定文本框的值改变事件,最后将事件对象中的文本框值传递给变量,即实现了双向绑定。
不使用方法实现:
<div id="ab"> <!-- <input type="text" v-model="msg"> --> <input type="text" name="" id="" v-bind:value="msg" v-on:input="msg = $event.target.value"> <h4>{{msg}}</h4> </div> <script> const app = new Vue({ el:'#ab', data:{ msg:'起飞' } }) </script>
使用方法实现:
<div id="ab"> <!-- <input type="text" v-model="msg"> --> <input type="text" name="" id="" v-bind:value="msg" v-on:input="Value"> <h4>{{msg}}</h4> </div> <script> const app = new Vue({ el:'#ab', data:{ msg:'起飞' }, methods:{ Value(event){ this.msg = event.target.value; } } }) </script>
3.3双向绑定原理
Vue 主要通过以下 4 个步骤来实现数据双向绑定的:
实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。
二、生命周期
1.Vue有哪些生命周期?它们有哪些使用场景?你的接口请求一般都放在哪个生命周期方法中,为什么?你的获取dom的方法一般都放在哪个生命周期方法中,为什么?
1.1 vue3中的选项式生命周期钩子及使用场景
beforeCreate
: 在实例初始化之后、进行数据侦听和事件/侦听器的配置之前同步调用created
:在实例创建完成后被立即同步调用beforeMount
:在挂载开始之前被调用mounted
:在实例挂载完成后被调用beforeUpdate
:在数据发生改变后,DOM 被更新之前被调用updated
:在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用beforeUnmount
(在Vue2中是:beforeDestroy
):在卸载组件实例之前调用unmounted
(在Vue2中是:destroyed
):卸载组件实例后调用
beforeCreate
和 Create
函数的。官方解释: 因为
setup
是围绕 beforeCreate
和 created
生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup
函数中编写。- 对于作为子组件被调用的组件里,异步请求应当在
mounted
里调用,因为这个时候子组件可能需要涉及到对dom的操作; - 对于页面级组件,当我们需要使用
ssr
(服务端渲染)的时候,只有created
是可用的,所以这个时候请求数据只能用它;能更快获取到服务端数据,减少页面 loading 时间; - 对于页面级组件, 当我们做异步操作时,涉及到要访问dom的操作,我们仍旧只能使用
mounted
; - 对于一般情况,
created
和mounted
都是可以的;
1. beforeCreated:生成$options选项,并给实例添加生命周期相关属性。在实例初始化之后,在 数据观测(data observer) 和event/watcher 事件配置之前被调用,也就是说,data,watcher,methods都不存在 这个阶段。但是有一个对象存在,那就是$route,因此此阶段就可以根据路由信息进行重定向等操作。
2. created:初始化与依赖注入相关的操作,会遍历传入methods的选项,初始化选项数据,从$options获取数据选项(vm.$options.data),给数据添加‘观察器’对象并创建观察器,定义getter、setter存储器属 性。在实例创建之后被调用,该阶段可以访问data,使用watcher、events、methods,也就是说 数据观测(data observer) 和event/watcher 事件配置 已完成。但是此时dom还没有被挂载。该阶段允许执行 http请求操作。
3. beforeMount:将HTML解析生成AST节点,再根据AST节点动态生成渲染函数。相关render函数首次被调用(划重点)。
4. mounted:在挂载完成之后被调用,执行render函数生成虚拟dom,创建真实dom替换虚拟dom,并挂载到实例。可以操作dom,比如事件监听
5. beforeUpdate:$vm.data更新之后,虚拟dom重新渲染之前被调用。在这个钩子可以修改$vm.data,并不会触发附加的冲渲染过程。
6. updated:虚拟dom重新渲染后调用,若再次修改$vm.data,会再次触发beforeUpdate、updated,进入死循环。
7. beforeDestroy:实例被销毁前调用,也就是说在这个阶段还是可以调用实例的。
8. destroyed:实例被销毁后调用,所有的事件监听器已被移除,子实例被销毁。
总结来说,虚拟dom开始渲染是在beforeMount时,dom实例挂载完成在mounted阶段显示。
在选项式生命周期中只有mounted时期挂载完成后才能够获取DOM,mounted时期前DOM未生成,mounted时期后调用进入死循环,同理在组合式生命周期只有在onMounted时期,setup时期等都不可以获取DOM。(或者可以在created时期使用$nextTick函数)
三、组件通信
1.Vue的子组件如何调用父组件的方法?
1.1直接在子组件中通过this.$parent.event来调用父组件的方法
// 父组件 <template> <div> <child></child> </div> </template> <script> import child from '~/components/dam/child'; export default { components: { child }, methods: { fatherMethod() { console.log('测试'); } } }; </script>
// 子组件 <template> <div> <button @click="childMethod()">点击</button> </div> </template> <script> export default { methods: { childMethod() { this.$parent.fatherMethod(); } } }; </script>
1.2在子组件里用$emit
向父组件触发一个事件,父组件监听这个事件就行了
// 父组件 <template> <div> <child @fatherMethod="fatherMethod"></child> </div> </template> <script> import child from '~/components/dam/child'; export default { components: { child }, methods: { fatherMethod() { console.log('测试'); } } }; </script>
// 子组件 <template> <div> <button @click="childMethod()">点击</button> </div> </template> <script> export default { methods: { childMethod() { this.$emit('fatherMethod'); } } }; </script>
1.3父组件把方法传入子组件中,在子组件里直接调用这个方法
// 父组件 <template> <div> <child :fatherMethod="fatherMethod"></child> </div> </template> <script> import child from '~/components/dam/child'; export default { components: { child }, methods: { fatherMethod() { console.log('测试'); } } }; </script>
// 子组件 <template> <div> <button @click="childMethod()">点击</button> </div> </template> <script> export default { props: { fatherMethod: { type: Function, default: null } }, methods: { childMethod() { if (this.fatherMethod) { this.fatherMethod(); } } } }; </script>
2.Vue组件通信有哪些方式?
- 父子通信: 父向子传递数据是通过 props,子向父是通过 events(
$emit
);通过父链 / 子链也可以通信($parent
/$children
);ref 也可以访问组件实例;provide / inject API;$attrs/$listeners
- 兄弟通信: Bus;Vuex
- 跨级通信: Bus;Vuex;provide / inject API、
$attrs/$listeners
- https://juejin.cn/post/6877101934600273934
2. Vue列表中加的key是干什么的?
vue中列表循环需加:key="唯一标识" 唯一标识可以是item里面id 等,因为vue组件高度复用增加Key可以标识组件的唯一性,为了更好地区别各个组件 key的作用主要是为了高效的更新虚拟DOM。
key在diff算法中的用法演示:https://juejin.cn/post/7156507746555133965#heading-5
延申问题:不推荐index作为key值
- 登录的状态、以及用户的信息
- 购物车的信息,收藏的信息等
- 用户的地理位置
- URL 中的 hash 值只是客户端的一种状态,向服务端发送请求的时候,hash 部分不会被发送。
- hash 值得改变会在浏览器的历史记增加访问记录,所以可以通过浏览器的回退、前进控制 hash 值的改变。
- 可以通过 a 标签设置 href 值或者通过 js 给location.hash 赋值来改变 hash 值。
- 可以通过hashchang 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。
popstate事件的执行是在点击浏览器的前进后退按钮的时候,才会被触发(popstate控制浏览器历史记录的api)
使用history模式需要更改router.js的mode为history,设置vue.config.js的publicPath:'/‘
- 通过 pushState 和 replaceState 两个API 来操作实现 URL 的变化。
- 可以通过 popstate 事假来监听 URL 的变化,从而对页面进行跳转(渲染)。
- history.pushState() 或 history.replaceState() 不会触发 popstate 事件,需要手动触发页面跳转
2.功能上:比如我们在开发app的时候有分享页面,那么这个分享出去的页面就是用vue或是react做的,咱们把这个页面分享到第三方的app里,有的app里面url是不允许带有#号的,所以要将#号去除那么就要使用history模式,但是使用history模式还有一个问题就是,在访问二级页面的时候,做刷新操作,会出现404错误,那么就需要和后端人配合,让他配置一下apache或是nginx的url重定向,重定向到你的首页路由上就ok了
传统的路由指的是:当用户访问一个url时,对应的服务器会接收这个请求,然后解析url中的路径,从而执行对应的处理逻辑。这样就完成了一次路由分发
前端路由是:不涉及服务器的,是前端利用hash或者HTML5的history API来实现的,一般用于不同内容的展示和切换
5.有没有写过Vue的指令,它的实现原理是什么?有没有写过Vue的插件,它的实现原理是什么?
5.1指令api:生命周期和钩子函数参数
bind
:指令第一次绑定到元素时调用,此钩子只会调用一次。在这里可以进行一次性的初始化设置。inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。update
:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。componentUpdated
:指令所在组件的 VNode 及其子 VNode 全部更新后调用。unbind
:只调用一次,指令与元素解绑时调用。
el
:指令所绑定的元素,可以用来直接操作DOM
。binding
:一个对象,包含以下属性:name
:指令名,不包括v-
前缀。value
:指令的绑定值,例如:v-my-directive="1 + 1"
中,绑定值为2
。oldValue
:指令绑定的前一个值,仅在update
和componentUpdated
钩子中可用。无论值是否改变都可用。expression
:字符串形式的指令表达式。例如v-my-directive="1 + 1"
中,表达式为 "1 + 1
"。arg
:传给指令的参数,可选。例如v-my-directive:foo
中,参数为 "foo
"。modifiers
:一个包含修饰符的对象。例如:v-my-directive.foo.bar
中,修饰符对象为{ foo: true, bar: true }
。vnode
:Vue
编译生成的虚拟节点。移步VNode API
来了解更多详情。oldVnode
:上一个虚拟节点,仅在update
和componentUpdated
钩子中可用。
5.2 指令和插件本质
Vue 插件机制的原理:本质上插件就是一个对象,在对象里面调用install 方法
Vue指令的原理:本质上插件就是一个对象
5.3 实现一键复制文本内容,用于鼠标右键粘贴的指令(配合插件执行全局注册)
import { Message } from 'ant-design-vue'; const vCopy = { // 名字爱取啥取啥 /* bind 钩子函数,第一次绑定时调用,可以在这里做初始化设置 el: 作用的 dom 对象 value: 传给指令的值,也就是我们要 copy 的值 */ bind(el, { value }) { el.$value = value; // 用一个全局属性来存传进来的值,因为这个值在别的钩子函数里还会用到 el.handler = () => { if (!el.$value) { // 值为空的时候,给出提示,我这里的提示是用的 ant-design-vue 的提示,你们随意 Message.warning('无复制内容'); return; } // 动态创建 textarea 标签 const textarea = document.createElement('textarea'); // 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域 textarea.readOnly = 'readonly'; textarea.style.position = 'absolute'; textarea.style.left = '-9999px'; // 将要 copy 的值赋给 textarea 标签的 value 属性 textarea.value = el.$value; // 将 textarea 插入到 body 中 document.body.appendChild(textarea); // 选中值并复制 textarea.select(); // textarea.setSelectionRange(0, textarea.value.length); const result = document.execCommand('Copy'); if (result) { Message.success('复制成功'); } document.body.removeChild(textarea); }; // 绑定点击事件,就是所谓的一键 copy 啦 el.addEventListener('click', el.handler); }, // 当传进来的值更新的时候触发 componentUpdated(el, { value }) { el.$value = value; }, // 指令与元素解绑的时候,移除事件绑定 unbind(el) { el.removeEventListener('click', el.handler); }, }; export default vCopy;
5.3.2定义一个能够注册指令的插件(不使用插件注册的方法是直接在main调用directive注册就可以)
import copy from './v-copy'; // 自定义指令 const directives = { copy, }; // 这种写法可以批量注册指令 export default { install(Vue) { //install函数是插件的语法 Object.keys(directives).forEach((key) => { Vue.directive(key, directives[key]); //注册指令对象 }); }, };
5.3.3在main中引入插件来进行指令注册
import Vue from 'vue'; import Directives from './directives'; Vue.use(Directives); //插件使用方法use
5.3.4使用指令方法
<template> <button v-copy="copyText">copy</button> </template> <script> export default { data() { return { copyText: '要 Copy 的内容', }; }, }; </script>
6.nxetTick是干什么用的?
将传入的回调函数包装成异步任务,异步任务又分微任务和宏任务,为了尽快执行所以优先选择微任务,把微任务放在DOM 更新后执行;
Vue2根据环境是否支持,选择的微任务优先级为:Promise---> MutationObserver---> setImmediate---> setTimeout
queueJob
-> queueFlush
-> flushJobs
-> nextTick参数的 fn,详情看 https://juejin.cn/post/7021688091513454622#heading-9
v-for
比v-if
具有更高的优先级,意味着v-if
将分别重复运行于每个v-for
循环中。v-for
的数据中v-if
的数据项给过滤处理了。const Foo = () => import('./Foo.vue') const router = new VueRouter({ routes: [ { path: '/foo', component: Foo } ] })
var element = { tagName: 'ul', // 节点标签名 props: { // DOM的属性,用一个对象存储键值对 id: 'list' }, children: [ // 该节点的子节点 {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]}, {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]}, {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]}, ] }
8.3实现原理
- 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
- diff 算法 — 比较两棵虚拟 DOM 树的差异;
- pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。