vue中常问面试题
Posted 赏花赏景赏时光
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue中常问面试题相关的知识,希望对你有一定的参考价值。
一、vue2.x中响应式原理
1)原理
采用数据劫持 + 订阅-发布模式的方式,通过Object.defineProperty来劫各个属性的setter getter,数据变化时,发布消息给订阅者,触发响应的回调。
即在创建Vue实例的时候,会遍历选项data的属性,用Object.defineProperty,为属性添加getter setter 对数据的读取进行劫持,在内部追踪依赖,在属性被访问或修改时,通知变化;如果属性是Object则会循环递归的处理
2)在实现响应式原理中有三大核心实现类
Observer:给对象属性添加getter setter,用于依赖收集、派发更新
Dep:用于收集当前响应式对象的依赖关系。每一个响应式对象及其子对象都有自己的Dep实例(里面的subs是Watcher实例数组),当数据有变更时,会通过dep.notify()通知各个Watcher
Watcher:观察者对象。Watcher实例分为3种:渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)
3)Watcher和Dep的关系
watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新
4)依赖收集
- initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集
- initState 时,对侦听属性初始化时,触发 user watcher 依赖收集
- render()的过程,触发 render watcher 依赖收集
- re-render 时,vm.render()再次执行,会移除所有 subs 中的 watcer 的订阅,重新赋值
5)派发更新
- 组件中对响应的数据进行了修改,触发 setter 的逻辑
- 调用 dep.notify()
- 遍历所有的 subs(Watcher 实例),调用每一个 watcher 的 update 方法
二、computed实现原理
computed 本质是一个惰性求值的观察者,同时持有一个 dep 实例;
内部通过watcher的 this.dirty 属性标记计算属性是否需要重新求值;
当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,computed watcher 通过 this.dep.subs.length 判断有没有订阅者,
有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。);
没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)
三、computed与watch的区别,运用场景
区别
computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存。只有它依赖的属性值发生改变时,才会重新计算computed的值
watch 侦听器 : 无缓存性,当监听的数据变化时,执行回调,进行处理
运用场景
computed:当我们需要进行数值计算,并且依赖于其它数据时,可以使用 computed。这样可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算
watch:当我们需要在数据变化时执行异步或开销较大的操作时,可以使用 watch。 watch 允许我们执行异步操作 ( 访问一个 API )、限制我们执行该操作的频率、并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的
四、vue3中实现响应式原理,为什么从Vue2中的Object.defineProperty改成了Proxy
从数组层面:
在Vue2中,通过索引修改数组的值,这时候是不能监听到数组变化的。虽然Object.defineProperty可以监控到数组下标变化的能力,但是从性能、体验的性价比出发,就放弃了该特性。为了解决该问题,Vue内部重写了push. pop. unshift. shift. reverse. sort. splice方法,来监听数组修改。由于只是对以上7种方法进行hack,数组的其他属性还是监听不到,有一定的局限性
从Object层面
Object.defineProperty只能劫持对象的属性,因此需要对每个对象的每个属性进行遍历,如果属性是一个Object,那么还需要深度遍历。如果能劫持整个对象才是最佳选择
Proxy可以劫持整个对象,并返回一个新的对象;可以代理数组、代理动态增加的属性
五、Vue中的key作用
key 是给每一个 vnode 的唯一 id,依靠 key,diff 操作可以更准确、更快速
diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点
更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。
更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1)
六、nextTick的实现原理
1)vue 用异步队列的方式控制 DOM 更新、nextTick 回调先后执行
2)microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
3)考虑兼容问题,vue 做了 microtask 向 macrotask 的降级方案
Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,才会采用 setTimeout(fn, 0) 代替
七、Vue中是如何实现对Array的方法重写
Vue通过对Array的原型拦截的方式,重写了Array的7个方法:push、pop、unshift、shift、sort、reverse、splice
1)首先获取到数组的:ob = this.__ob__(也就是它的Observer对象)
2)如果有新的值,就调用ob.observeArray,对新的值进行监听
3)接着手动调用ob.dep.notify(),通知render watch执行update
八、Vue中的组件data选项为什么必须是函数
在new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中data 必须是一个函数呢?
1)组件是可以复用的
2)js中对象是引用关系,如果组件中的data是对象,那么子组件中的data属性值会相互影响,产生副作用
因此,Vue中data选项必须是function,每个实例可以维护一份一份被返回对象的独立拷贝,而new Vue()实例是不会被复用的,因此不会哟以上问题
九、谈谈Vue中的事件机制,手写$on $off $once $emit实现代码
Vue中的事件机制本身就是一个订阅-发布模式的实现
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) { // event为数组,则循环遍历调用$on
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else { // event为string,则将监听事件和回调函数添加到事件处理中心_events对象中
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this
// 定义监听事件的回调函数
function on () {
vm.$off(event, on) // 从事件中心移除监听事件的回调函数
fn.apply(vm, arguments) // 执行回调函数
}
on.fn = fn
vm.$on(event, on) // 通过$on方法注册事件
return vm
}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// all,调用this.$off()没有传参数,则清空事件处理中心缓存的事件及其回调
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events,events为array,则循环遍历调用$off
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific event,从事件处理中心取出缓存的事件
const cbs = vm._events[event]
if (!cbs) { // 如果事件中心没有缓存该事件,直接返回
return vm
}
if (!fn) { // 如果调用$off时,没有传回调函数fn,则直接清空监听该事件的所有回调函数
vm._events[event] = null
return vm
}
// specific handler
let cb
let i = cbs.length
while (i--) { // 对监听的事件的回调函数进行循环遍历
cb = cbs[i]
if (cb === fn || cb.fn === fn) { // 如果入参fn === 缓存的回调函数,或者入参fn === 缓存的cb.fn,则剔除该缓存的回调函数
cbs.splice(i, 1)
break
}
}
return vm
}
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== 'production') {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(vm)} but the handler is registered for "${event}". ` +
`Note that html attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(event)}" instead of "${event}".`
)
}
}
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
}
// 实际是执行回调函数
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
res = args ? handler.apply(context, args) : handler.call(context)
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
十、说说Vue的渲染过程
1、new Vue()实例的时候,主要做了以下工作:
调用了内部方法_init()
1)给私有属性_uid自增1,每个组件每一次初始化时做的一个唯一的私有属性标识
2)合并options,并赋值给实例属性$options
3)定义实例私有属性vm._self = vm ,用于访问实例的数据和方法
4)调用initLifecycle(vm) :确认组件的父子关系;初始化实例属性vm.$parent、 vm.$root 、vm.$children、 vm.$refs;初识化内部相关属性
5)调用initEvents(vm) :将父组件的自定义事件传递给子组件;初始化实例内部属性_events(事件中心)、_hasHookEvent;
6)调用initRender(vm) :提供将render函数转为vnode的方法;初始化实例属性$slots、$scopedSlots;定义实例方法$createElement;定义响应式属性:$attrs、$listeners;
7)调用callHook(vm, 'beforeCreate') , 执行组件的beforeCreate钩子
8)调用initInjections(vm) ,resolve injections before data/props
9)调用initState(vm) ,对实例的选项props、data、computed、watch、methods初始化
10)调用initProvide(vm) , resolve provide after data/props
11)调用callHook(vm, 'created')
12)如果选项有提供挂载钩子,则执行挂载;$options.el:vm.$mount(vm.$options.el)
----------待完善----------
十一、keep-alive的实现原理、缓存机制
1、获取 keep-alive 包裹着的第一个子组件对象及其组件名
2、如果有 include/exclude,根据设定的 include/exclude进行条件匹配,决定是否缓存。不匹配,直接返回组件实例
3、根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果缓存过,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)
4、如果没有缓存过,则在 this.cache 对象中存储该组件实例并保存 key 值;之后检查如果设置缓了max,并且缓存的实例数量超过 max 的设置值,则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key)
5、最后将组件实例的 keepAlive 属性设置为 true,这个在渲染和执行被包裹组件的钩子函数会用到
LRU缓存淘汰算法
LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高。
keep-alive 的实现正是用到了 LRU 策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]
即最新访问的放到缓存的末尾,淘汰的永远是下标为0的对象
十二、vm.$set的实现原理
Vue 会在初始化实例的时候给data的属性添加 getter/setter ,因此属性在 data 对象上存在才能让 Vue 将它转换为响应式的;
对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(target, propertyName, value) 方法向嵌套对象添加响应式属性
1、如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式
2、如果目标是对象,判断属性存在,即为响应式,直接赋值
3、如果 target 本身就不是响应式,直接赋值
4、如果属性不是响应式,则调用 defineReactive 方法进行响应式处理,并调用ob.dep.notify()通知相应的watcher
十三、vm.$delete的实现原理
1、如果target是数组,使用 vue 实现的变异方法 splice 实现响应式,并删除元素
2、如果是对象,属性不是自身属性,直接返回
3、使员工delete语法删除对象属性
4、如果target是响应式对象,则调用ob.debp.notify()通知watcher做更新
参考文章:
computed源码解析:https://blog.csdn.net/qq_27460969/article/details/94873042
computed源码解析:https://www.cnblogs.com/vickylinj/p/14034645.html
Vue源码面试题:https://zhuanlan.zhihu.com/p/101330697
以上是关于vue中常问面试题的主要内容,如果未能解决你的问题,请参考以下文章