本文分享12道高频vue原理面试题,覆盖了 vue 核心实现原理,其实一个框架的实现原理一篇文章是不可能说完的,希望通过这 12 道问题,让读者对自己的 vue 掌握程度有一定的认识(b 数),从而弥补自己的不足,更好的掌握 vue。
1. vue 响应式原理
核心实现类:
observer : 它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新
dep : 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个 dep 实例(里面 subs 是 watcher 实例数组),当数据有变更时,会通过 dep.notify()通知各个 watcher。
watcher : 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种
watcher 和 dep 的关系
watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。
依赖收集initstate 时,对 computed 属性初始化时,触发 computed watcher 依赖收集initstate 时,对侦听属性初始化时,触发 user watcher 依赖收集render()的过程,触发 render watcher 依赖收集re-render 时,vm.render()再次执行,会移除所有 subs 中的 watcer 的订阅,重新赋值。派发更新组件中对响应的数据进行了修改,触发 setter 的逻辑调用 dep.notify()遍历所有的 subs(watcher 实例),调用每一个 watcher 的 update 方法。原理
当创建 vue 实例时,vue 会遍历 data 选项的属性,利用 object.defineproperty 为属性添加 getter 和 setter 对数据的读取进行劫持(getter 用来依赖收集,setter 用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。
每个组件实例会有相应的 watcher 实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有 computed watcher,user watcher 实例),之后依赖项被改动时,setter 方法会通知依赖与此 data 的 watcher 实例重新计算(派发更新),从而使它关联的组件重新渲染。
一句话总结:
vue.js 采用数据劫持结合发布-订阅模式,通过 object.defineproperty 来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发响应的监听回调
2. computed 的实现原理
computed 本质是一个惰性求值的观察者。
computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。
其内部通过 this.dirty 属性标记计算属性是否需要重新求值。
当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,
computed watcher 通过 this.dep.subs.length 判断有没有订阅者,
有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)
没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)
3. computed 和 watch 有什么区别及运用场景?
区别
computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
watch 侦听器 : 更多的是「观察」的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。
运用场景
运用场景:
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算。
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 api ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
4. 为什么在 vue3.0 采用了 proxy,抛弃了 object.defineproperty?
object.defineproperty 本身有一定的监控到数组下标变化的能力,但是在 vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性(vue 为什么不能检测数组变动 )。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组
push();pop();shift();unshift();splice();sort();reverse();由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。
object.defineproperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。vue 2.x 里,是通过 递归 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。
proxy 可以劫持整个对象,并返回一个新的对象。proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
5. vue 中的 key 到底有什么用?
key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。)
diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.
更准确 : 因为带 key 就不是就地复用了,在 samenode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。
更快速 : key 的唯一性可以被 map 数据结构充分利用,相比于遍历查找的时间复杂度 o(n),map 的时间复杂度仅仅为 o(1),源码如下:
function createkeytooldidx(children, beginidx, endidx) { let i, key; const map = {}; for (i = beginidx; i <= endidx; i) { key = children[i].key; if (isdef(key)) map[key] = i; } return map;}6. 谈一谈 nexttick 的原理
js 运行机制
js 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:
所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。主线程不断重复上面的第三步。
主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。
for (macrotask of macrotaskqueue) { // 1. handle current macro-task handlemacrotask(); // 2. handle all micro-task for (microtask of microtaskqueue) { handlemicrotask(microtask); }}在浏览器环境中 :
常见的 macro task 有 settimeout、messagech
如何开网店:卖家如何走出网店前期成长误区订阅号阿里云服务器配置云服务器打不开网站刚由型主机升级成型主机-虚拟主机/数据库问题如何选择域名?新手如何购买合适的域名微信商城小程序和购物平台有什么区别阿里云服务器哪个镜像好些阿里云学生服务器优惠多久