Vue之关于异步更新细节
作者:玉案轩窗
前言
Vue官网对于异步更新的介绍如下:
- Vue 在更新 DOM 时是异步执行的。
- 只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
- 如果同一个 watcher 被多次触发,只会被推入到队列中一次。
- 这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的
Vue使用Object.defineProperty对数据劫持后,当对对象进行set操作,就会触发视图更新。
更新逻辑
以下面实例来分析视图更新处理逻辑:
<div>{{ message }}</div> <button @click="handleClick">更新</button> new Vue({ data: { message: '' }, methods: { handleClick() { this.message = Date.now(); } } })
当点击更新按钮后会对已劫持的属性message做赋值操作,此时会触发Object.defineProperty的set操作。
Object.defineProperty set操作
Object.defineProperty的set函数的设置,实际上最核心的逻辑就是触发视图更新,具体代码逻辑如下:
set: function reactiveSetter (newVal) { // 其他逻辑 // 触发视图更新 dep.notify(); }
每个属性都会对应一个Dep对象,当对属性进行赋值时就会调用Dep的notify实例方法,该实例方法的功能就是是通知视图需要更新。
Dep notify实例方法
notify实例方法的代码逻辑如下:
Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice(); for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } };
subs中存储是watcher对象,每个Vue实例都存在一个与视图更新关联的watcher对象,该对象的创建是在$mount阶段,具体看查看之前的文章Vue实例创建整体流程。
代表属性的Dep对象与watcher对象的关联是在render函数调用阶段具体属性获取时建立的即依赖收集
notify方法会执行与当前属性关联的所有watcher对象的update方法,必然会存在一个视图更新相关的watcher。
watcher对象的按照分类实际上分为两类:
- 视图更新相关的,每一个Vue实例都存在一个此类的watcher对象
- 逻辑计算相关的,计算属性和watch监听所创建的watcher对象
Watcher update实例方法
update实例方法的代码逻辑具体如下:
Watcher.prototype.update = function update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); } };
lazy、sync都是Watcher的属性,分别表示:
- lazy:表示懒处理,即延迟相关处理,用于处理计算属性
- computedsync:表示同步执行,即触发属性更新就立即更新视图
从上面逻辑中可知,默认是queueWatcher处理即开启一个队列,并缓冲在同一事件循环中发生的所有数据变更,即视图是异步更新的。
这里需要注意的一点是:
queueWatcher中必然存在视图更新的watcher对象,不会存在计算属性computed对应的watcher(computed对应的watcher对象lazy属性默认为true),可能存在watch API对应的用户性质的watcher对象
queueWatcher执行逻辑
function queueWatcher (watcher) { var id = watcher.id; if (has[id] == null) { has[id] = true; if (!flushing) { queue.push(watcher); } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. var i = queue.length - 1; while (i > index && queue[i].id > watcher.id) { i--; } queue.splice(i + 1, 0, watcher); } // queue the flush if (!waiting) { waiting = true; nextTick(flushSchedulerQueue); } } }
实际上面逻辑主要分成3点:
- 对于同一个watcher对象,使用has对象结构+id为key来判断队列中是否已存在对应watcher对象,如果存在就不会将其添加到queue中
- 通过flushing标识区分当在清空队列过程中和正常情况下,如何向queue中添加watcher
- 通过waiting标识区分是否要执行nextTick即清空queue的动作
因为queue是全局变量,在此步骤之前就将watcher对象添加到queue,如果waiting为true就标识已经调用nextTick实现异步处理queue了,就不要再次调用nextTick
从上面整体逻辑可知,queueWacther的逻辑主要就两点:
- 判断是否重复watcher,对于不重复的watcher将其添加到queue中
- 调用nextTick开启异步处理queue操作即flushSchedulerQueue函数执行
nextTick + flushSchedulerQueue
nextTick函数实际上跟$nextTick是相同的逻辑,主要的区别就是上下文的不同,即函数的this绑定值的不同。
使用macroTask API还是microTask API来执行flushSchedulerQueue
而flushSchedulerQueue函数就是queue的具体处理逻辑,主要逻辑如下:
function flushSchedulerQueue () { flushing = true; var watcher, id; // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child) // 2. A component's user watchers are run before its render watcher (because // user watchers are created before the render watcher) // 3. If a component is destroyed during a parent component's watcher run, // its watchers can be skipped. queue.sort(function (a, b) { return a.id - b.id; }); for (index = 0; index < queue.length; index++) { watcher = queue[index]; id = watcher.id; has[id] = null; watcher.run(); } var activatedQueue = activatedChildren.slice(); var updatedQueue = queue.slice(); resetSchedulerState(); // call component updated and activated hooks callActivatedHooks(activatedQueue); callUpdatedHooks(updatedQueue); }
flushSchedulerQueue函数的主要逻辑可以总结成如下几点:
- 对队列queue中watcher对象进行排序
- 遍历queue执行每个watcher对象的run方法
- 重置控制queue的相关状态,用于下一轮更新
- 执行组件的updated和activated生命周期
这里就不展开了,需要注意的是activated是针对于keep-alive下组件的特殊处理,updated生命周期是先子组件再父组件的,队列queue的watcher对象是按照父组件子组件顺序排列的,所以在源码中updated生命周期的触发是倒序遍历queue触发的。
首先说说watcher对象的run实例方法,该方法的主要逻辑就是执行watcher对象的getter属性和cb属性对应的函数。
上面说过watcher对象的按照分类实际上分为两类:
- 视图更新相关的,每一个Vue实例都存在一个此类的watcher对象
- 逻辑计算相关的,计算属性和watch监听所创建的watcher对象
watcher对象的getter属性和cb属性就是对应着上面各类watcher的实际处理逻辑,例如watch API对应的getter属性就是监听项,cb属性才是具体的处理逻辑。
为什么需要对queue中watcher对象进行排序?
实际上Vue源码中有相关说明,这主要涉及到嵌套组件Vue实例创建、render watch和用户watch创建的时机。
每个组件都是一个Vue实例,嵌套组件创建总是从父组件Vue实例开始创建的,在父组件patch阶段才创建子组件的Vue实例。
而这个顺序决定了watcher对象的id值大小问题:
父组件的所有watcher对象id < 子组件的所有watcher对象id
render watch实际上就是与视图更新相关的watcher对象,该对象是其对应的Vue实例创建的末期即挂载阶段才创建的,是晚于用户watch即计算属性computed和watch API创建的watcher对象,所以:
render watch的id < 所有用户watch的id的
子组件可能是更新触发源,如果父组件也需要更新视图,这样queue队列中子组件的watcher对象位置会在父组件的watcher对象之前,对queue中watcher对象进行排序就保证了:
视图更新时 父组件 总是先于 子组件开始更新操作,而每个组件对应的视图渲染的watcher最后再执行(即用户watcher对象对应的逻辑先执行)
总结
Vue异步更新的过程还是非常清晰的:
- 对属性赋值触发Dep对象notify方法执行
- 继而执行Watcher对象的update方法将对象保存到队列queue中
- 继而调用mircoTask API或macroTask API执行queue中任务
- 对队列中watcher进行排序,保证顺序执行的正确性,调用其对应run方法来实现视图更新和相关逻辑更新操作
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。