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()
是用于插件的,可以在插件中扩展全局的组件和指令等。例如插件VueRouter
Vue.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