Vue 高频原理篇+详细解答之 二
大家好,我是林一一,我们又见面了,这是一篇关于 Vue 高频原理的面试题,上一篇的面试题已经发过了,建议读完上一篇的 Vue 原理再来读这一篇。
n久没有发文章了,这一篇文章在草稿箱里面躺了很久了😥。上下两篇都是针对 Vue2.x 版本的原理。以后会添加上 Vue3的原理解析?!😂。
上一篇 Vue 高频原理文章
PS:记得点赞啊,亲们 😥😋
面试题篇
13. vue.$set(obj, key, value) 方法的实现原理
小tip:用于给
data中响应式的对象/数组添加相应式的数据,才会触发试图更新。
$set()之所以给数组和对象添加新属性可以触发相应式的数据,是因为最后调用defineReactive()给新增的属性添加get/set- 调用
$set()给对象新增属性时可以触发对象依赖收集的Watcher去更新。 - 调用
$set()修改数组索引时,内部会通过调用 重写的splice()方法更新数组。 - 调用
set()不能给根实例vm或_data添加属性,因为性能消耗大。 - 扩展
// \src\core\observer\index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 给数组添加响应式数据,调用数组的 splice 方法
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
}
// 这里是给根实例 vm/_data 添加属性,会报错。
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 给不是响应式的对象添加属性,直接加上即可,但是不会更新视图
if (!ob) {
target[key] = val
return val
}
// 最后 defineReactive 给添加的属性增加 `get/set`
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
14. vue 为什么需要虚拟 DOM?
主要由两个点:
减少 DOM 操作,可以跨平台。
- visual DOM 是真实 dom 的描述对象,直接操作 DOM 可能会引发 DOM 的回流和重绘,性能消耗很大。
- js层的操作效率高,将真实DOM转化成对象来操作,通过
diff算法来比较对象差异更新 DOM,可以提高性能。 - 而且虚拟 DOM 不依赖平台的差异,能实现跨平台运行。
在 vue 已经生成 AST 以后,如果页面的 DOM 某个节点被删除,不会影响到已经生成的 AST。
15. ⭐ diff 算法原理
Vue 中的 diff 算法的比较是
平级比较,没有跨级比较。使用深度递归和双指针遍历
- 先比较前后虚拟 DOM,是否是相同的根节点。
- 相同的节点比较属性,复用旧的节点。
- 然后比较子的节点,新虚拟 dom 的子节点没有,老节点直接复用。新虚拟 dom 的子节点和老虚拟 DOM 子节点都有,比较标签名、属性等。
- 优化比较:双指针头头比较,尾尾比较,头尾比较,尾头比较,对比之后进行复用
源码的
patchVnode()和patchChildren()都是虚拟节点的比较
16. vue 通过数据劫持可以得知响应式数据的变化,为什么还需要虚拟dom 进行 diff 算法比较?
在 vue1.0时期,确实只使用了数据劫持做到响应式的数据变化,2.0时才引入diff算法。
- vue1.0时通过给每一个属性都添加一个渲染
Watcher(2.0是取值是添加一个 dep 来收集user watcher),但是每一个属性都有一个Watcher对性能消耗很大。 - 所以最后采用的是组件级的
Watcher来配合 diff 算法更新视图。组件级数据更新但是不知道那里更新所以需要配合diff算法来比较。
17. Vue 中 key 属性的作用和原理
v-for中的 key。 key 的存在使 diff 操作可以更准确、更快速。
- Vue 在调用
patch生成真实 dom 过程中通过对比 Vnode 中的 key 可以判断两个虚拟节点是否相同。key 相同可以复用老的节点,可以提高性能。 - 没有 key 会导致更新出现问题,尽量不使用下标
index作为 key。
因为
index作为key和没有设置key是一样的,假设有4个li标签(A,B,C,D)项删除第一项li标签,但是删除的是最后一项li,这是 diff 算法的比较的复用策略导致的。上面的比较过程是B复用A,C复用B,D复用C, D被删除,不论数组怎么变化下标index都是从0,1,2,3...开始的重写渲染后还是不会变化。所有不要使用index作为下标。
组件部分
18. 对组件化开发的理解
- 复用性高,能大幅度提高开发效率,还可以进行单元测试。
- vue 中的每一个组件都对应一个
Watcher,组件发生更新只需要Watcher更新即可。 - vue 中的数据流都是挡线的数据流
19. vue 中的函数式组件优势实现原理
函数式组件就是一个普通函数。
Vue.component('functional', {
functional: true,
props: {
items: {
type: Array,
required: true
}
},
render: h => h('div', {}, 'hello')
})
- 函数式组件的优势:无状态(没有响应式的数据data属性),无生命周期,没有 this。性能高:内部没有任何生命周期处理函数
- 具备组件的特点:可以传参
props,可以编写render函数 - 使用场景:只负责渲染,不需要状态。
20.老生常谈之,Vue 中组件的通信方式有哪些和区别是什么?
Vue 中的组件的传递方式常用的主要有 7 种。
props和$emit父子组件的传递。props父组件向子组件传递,$emit子组件向父组件传递。这个方法的内部使用的式发布订阅模式,源码位置\src\core\instance\event.js$parent和$children组件实例的获取,$parent获取当前组件的父组件的实例来执行相应操作,$children数组,获取当前组件的子组件实例,通过实例的_uid辨别实例。实例获取后可以执行添加数据。
// src\core\instance\lifecycle.js
export function initLifecycle (vm: Component) {
const options = vm.$options
// 获取父组件
let parent = options.parent
if (parent && !options.abstract) { // 排除抽象组件
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm) // 父组件记录子组件的实例放入到数组中
}
vm.$parent = parent // 增加 $parent 属性表示父组件
vm.$root = parent ? parent.$root : vm
// ...
}
注意通过
$children添加的数据,也不是响应式数据,数组中的实例也不保证顺序。
$attrs 和 $listeners子组件批量获取父组件传入的属性和事件,具备响应式,在组件上写入的数据会被写入到虚拟节点$vnode.data.attrs中,例如<my-component name="林一一" age=18 @handle="handle"></my-component>即$vnode.data.attrs={name:"林一一", age:18},通过this.$attrs 获取属性, this.$listeners获取事件,需要注意的是没有使用props接收的变量才可以使用$attrs获取,可以声明inheritAttrs:false
// \src\core\instance\render.js
export function initRender (vm: Component) {
vm._vnode = null
vm._staticTrees = null
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// 创建的虚拟 DOM
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
const parentData = parentVnode && parentVnode.data
//... some code
// defineReactive,在$attrs $listeners 中的数据具备响应式
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
-
provide和inject,父组件中通过provide来提供变量,子组件中通过inject来注入提供的变量。只要父组件中有provide提供的数据,深层的子组件中依然可以inject不停的向上查找父组件提供的provide属性,找到后定义在当前组件的身上。provide和inject不是响应式的,除非申明的属性是响应式的。 -
$refs可以获取真实 DOM 节点,也可以获取组件实例。在v-for中获取的 DOM 是一个数组。 -
eventBus平级组件的数据传递方式 -
vuex状态管理 -
slot插槽
父子组件的通信考虑
props/$emit, $parent/$children, $refs,隔代组件的通信考虑使用provide/inject, $attrs/$listener,平级组件可以考虑EventBus/Vuex,Vuex使用于所有的组件通信。
21. 组件通信的 $attrs 出现的目的和 provide/inject 能解决 $attrs 的问题嘛?
$attrs主要用于批量的获取父组件传递的数据,传入的数据具备响应式provide/inject可以跨级的传递数据,传入的数据除非是响应是的数据否则不具备响应式数据。
22. v-if 和 v-for 哪个优先级更高
v-if 和 v-for同时最用在同一个节点或组件上 vue 会抛出异常,不建议同时使用。
v-if 和 v-for编译阶段v-for的优先级比v-if高,如果需要使用到这样的场景可以考虑使用computed代替v-if的判断。- 扩展
// vue\src\compiler\codegen\index.js
// ...
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state) // 先循环
} else if (el.if && !el.ifProcessed) {
return genIf(el, state) // 再判断条件
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
}
// ...
23.Vue 中 v-model 的实现原理
v-model 是 vue 提供的一种语法糖,
- v-model 使用在表单中,也可以使用在组件上,实现双向数据绑定;
v-model对输入法做了处理,防止中文在输入时就进行双向数据绑定,要注意如果采用<input :value="msg" @input="chang" />代替v-model是不会对输入法有限制,这就是两者的差别。 v-model原理:使用v-model指令在表单input、textarea、select、等表单元素上可以创建双向数据绑定,v-model作用在不同的表单元素上,会触发不同的表单相应事件,比如text和textarea元素使用value属性和input事件·checkbox和radio使用checked属性和change事件select字段将value作为prop和change作用事件。
一个页面内有大量的表单,导致页面卡死,如果使用
v-model可以采用修饰符.lazy,类似防抖
// ...
// 这里是 v-model 作用在组件上的调用方法
if (el.component) {
genComponentModel(el, value, modifiers); // modifiers代表模板中添加的修饰符
// component v-model doesn't need extra runtime
return false
} else if (tag === 'select') {
genSelect(el, value, modifiers);
} else if (tag === 'input' && type === 'checkbox') {
genCheckboxModel(el, value, modifiers);
} else if (tag === 'input' && type === 'radio') {
genRadioModel(el, value, modifiers);
} else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers);
} else if (!config.isReservedTag(tag)) {
// ...
}
要注意:
v-model也可以使用在组件上而不是只可以使用在表单元素上。genComponentModel()方法,会将使用在组件上的v-model在AST树上添加model={value,expression, callback}属性。
24. Vue.use() 的作用和实现
Vue.use()是用于插件的,可以在插件中扩展全局的组件和指令等。例如插件VueRouterVue.use()会调用插件的install方法,将会 Vue 中的构造函数默认传入。如果插件没有install方法会直接调用插件
Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
// 判断是否被缓存过,不能多次缓存
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// 取出第一项
const args = toArray(arguments, 1)
args.unshift(this)
// 有install,调用 install 方法,
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') { // 没有install,直接调用插件(函数),
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
25.Vue 组件中属性 name 的作用
只有作为组件选项时起作用。
name具备3个作用
-
- 组件递归时,必须具备
name属性,因为需要使用到name属性查找组件。
- 组件递归时,必须具备
-
- 当使用
keep-alive时,可以使用这个name进行过滤。例如<keep-alive exclude="Home"></keep-alive>。
- 当使用
-
- 具备
name属性的命名组件可以方便我们在vue-devtools调式。
- 具备
26. Vue 中自定义指定的实现原理。
自定义指令钩子
自定义指令也具备和组件一样的声明周期钩子。钩子都是可选的
-
bind: 只调用一次,在指令绑定到节点上时被调用。
-
inserted:被绑定指令的元素插入到父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
-
update:VNode更新时也可能在VNode更新之前调用。
-
componentUpdated: 指令所在的VNode及其子VNode更新后调用。
-
unbind: 只调用一次,在指令和元素解绑时调用。
自定义组件的实现原理
-
- 在创建
AST语法树时,遇到指定会在当前元素上添加directives属性
- 在创建
-
- 通过
genDirectives生成指令代码
- 通过
-
- 在
patch生成真实DOM前,将指令的钩子提取出来放入到cbs,在patch过程中执行对应的钩子
- 在
-
- 调用对应指令的方法
27.Vue-Router 的导航守卫和执行流程。
Vue-Router 的导航守卫(钩子)中分为:全局守卫,路由守卫和组件守卫。路由执行流程
- 触发进入其他路由。
- 调用要离开路由的组件守卫
beforeRouteLeave - 调用全局前置守卫:
beforeEach - 在重用的组件里调用
beforeRouteUpdate - 调用路由独享守卫
beforeEnter。 - 解析异步路由组件。
- 在将要进入的路由组件中调用
beforeRouteEnter组件守卫 - 调用全局解析守卫
beforeResolve - 导航被确认。
- 调用全局后置钩子的
afterEach钩子。 - 触发DOM更新
(mounted)。 - 执行
beforeRouteEnter守卫中传给next的回调函数
上面的顺序调用是 vueRouter 在内部将上面的钩子函数放入到一个队列中最后,调用
runQueue()依次将队列中的钩子执行。
28. Vue-Router 中的路由模式和区别
Vue-Router 中有三种路由模式分别是
history/hash/abstract。vueRouter默认是hash模式。
hash模式,使用URL中的#作为路由,支持所有的浏览器。hash+popState/hashChange兼容性好,不刷新页面,因为服务端无法获取hash值,对 seo 优化不好,比如https://baidu.com/a会直接显示未找到,而https://baidu.com/#a还是显示的是百度的首页,因为服务器获取不到#后面的数据。history模式,浏览器提供historyApi( go/back/forward )和新增的两个popState()/replaceState (),这两个api可以在不刷新的情况下读取浏览器的历史记录,history模式美观,但是需要服务端的至此否则会出现404状态。abstract模式,在不支持浏览器的环境下使用。
上线时采用
history模式,cli 内部的配置可以防止出现404的情况。
29. Vux 的理解

Vuex 是为了大型项目开发的,实现不同组件之间的状态管理(数据共享)。适用于多个组件之间的数据交互
Vuex应用核心就是store(仓库)。store就是一个容器,包含了应用中大部分的state(状态)Vuex是单向的数据流,组件不能直接修改容器中state的状态(数据)- 改变
store中的状态的唯一途径就是显示commit(提交) mutation,组件状态state发生变化。 - 在非严格模式
strict:false允许直接来修改状态state,但是一帮都是在严格模式中通过mutations来修改数据。 - 缺点:Vuex 的数据不能持久化。 vuex 在页面刷新后数据不会再存在
主要有以下几个模块:
State: 定义了应用状态的数据结构,可以在这里设置默认的初始状态Getter: 允许组件从store中获取数据,mapGetters辅助函数仅仅是将store中的getter映射到计算属性。Mutation: 唯一更改store中状态的方法,且必须是同步函数。Action: 用于提交mutation, 而不是直接变更状态,可以包含任意异步操作。Module: 允许将单一的store拆分为多个store且同时保存在单一的状态树中 扩展:action 和 mutation 之间的区别
mutation对象是 Vuex中唯一一个可以修改state状态的方式。mutation对象中方法必须是同步的,因为提交的mutation触发时回调函数还没有被调用,那么数据的状态就无法准确知道是不是最新的。action对象用于提交的是一个mutation事件,不可以修改数据的状态。action中的方法支持异步。
30.vue 中的性能优化策略有哪些
- 对象或数组的层级不要过深。
- 善于使用
computed将值缓存,不频繁取值 - 设置
v-for中的key值,便于diff算法的优化 - 分辨好
v-show/v-for的使用场景 - 结合业务逻辑封装组件,控制组件的粒度
- 使用
keep-alive缓存组件 - 。。。
参考
转载自:https://juejin.cn/post/7089352071509442574