源码调试地址
https://github.com/KingComedy/vue-debugger
什么是异步更新
在本轮宏任务内,组件内多个属性更新,或者一个属性更新多次,最终这个组件只会重新渲染一次,即组件里的dom只会做一次重新渲染 如:以下代码只会触发组件的一次更新渲染
this.count = 1
this.key = 'key'
this.count = 2
Vue为什么是异步更新
避免组件多次patch,引起dom多次重新渲染
nextTick(cb, ctx)方法源码详解(在src\core\util\next-tick.js中定义)
- 使用:$nextTick(cb)
- 在next-tick.js里 定义了callbacks回调函数数组,定义pending任务执行状态
- nextTick(cb), 将传入的cb回调函数push到callbacks,并且判断pending任务执行状态是否为false,即当前没有执行任务
- 设置pending=true,执行timeFunc()
- timeFunc函数的任务主要是异步执行 flushCallbacks函数,即Promise.resolve().then(flushCallbacks)
- flushCallbacks主要是 遍历执行callbacks里的每个回调
- timeFunc函数这边会做浏览器兼容的降级处理, 即浏览器不支持Promise, 依次降级为 MutationObserver(前提不是IE浏览器) => setImmediate => setTimeout
- 总结:nextTick会把传入的回调函数存入callbacks,flushCallbacks函数会遍历执行callbacks里的每个回调,nextTick会将flushCallbacks当做一个任务推入微任务队列。
异步更新过程
- 当触发属性更新时,属性的dep(在数据做响应式处理时的依赖收集),会执行dep.notify通知属性的观察者watcher更新,执行所有watcher的update方法
- watcher的update默认异步更新,执行queueWatcher(this), 将watcher推入watcher列表queue。通过watcher.id判断queue里是否已存在watcher,没有则加入到queue数组里
- 判断!waiting === true, 即目前是否属于空闲状态,是则执行nextTick(flushSchedulerQueue)
- flushSchedulerQueue: 对queue列表根据watcher.id进行排序,遍历执行执行queue列表里的所有watcher,先执行每一个watcher.before(即watcher所对应的组件的beforeUpdate生命周期函数) => 再执行watcher.run。遍历完成后,在重新遍历queue列表执行每个watcher所对应的组件的updated生命周期
- 总结:属性更新 => 将渲染watcher推入queue列表(如果当前watcher已经存在就不入列) => 将遍历执行watcher的函数 flushSchedulerQueue 推入微任务队列, nextTick(flushSchedulerQueue)
通过例子加深理解
例子一
countClick() {
this.count = 1 // 触发更新,watcher进入queue列表,queue.push(watcher), 此时更新任务已经进入微任务队列
this.count = 2 // 触发更新,当前组件的渲染watcher已经在queue列表里,所以不会进入列表
console.log('count:', this.$refs.count.innerHTML) // count: 0,微任务队列还未执行,dom还未更新
this.$nextTick(() => {
console.log('count:', this.$refs.count.innerHTML) // count: 2 推入微任务队列callbacks
})
}
流程解析:
- 打印的顺序是:count:0 => count:2
- count 第一次设置值时,触发更新,watcher进入queue列表,即queue.push(watcher), 此时更新任务已经进入微任务队列,即微任务队列为[flushCallbacks],且callbacks = [flushSchedulerQueue],只有flushSchedulerQueue执行完后,dom才会更新
- count 第二次更新时,因为当前组件的渲染watcher已经在queue列表里,所以不会进入queue列表
- 因为本轮宏任务还未执行完成,所以微任务队列也还没有执行,即更新任务还未执行,所以此时dom还没更新,dom上的count值仍旧为0
- 执行nextTick时,会把回调函数(cb1)推入callbacks列表,所以此时微任务队列为[flushCallbacks], 即[[...callbacks]], 且 callbacks = [flushSchedulerQueue, cb1]
- 当宏任务执行完成,开始遍历执行微任务,此时微任务就一个flushCallbacks,遍历执行callbacks
- flushSchedulerQueue先执行,执行完成后,此时dom已经更新
- cb1执行时,dom已经改变,所以此时dom的count值为2
例子二
countClick() {
this.count = 1 // 触发更新,watcher进入queue列表,queue.push(watcher), 此时更新任务已经进入微任务队列
this.count = 2 // 触发更新,当前组件的渲染watcher已经在queue列表里,所以不会进入queue列表
Promise.resolve().then(() => {
console.log('p1 count:', this.$refs.count.innerHTML) // count: 2
})
console.log('count:', this.$refs.count.innerHTML) // count: 0 属于宏任务,先执行,dom还未更新,所以count值还未改变为 0
this.$nextTick(() => {
console.log('cb1 count:', this.$refs.count.innerHTML) // count: 2
})
}
流程解析:
- 打印的顺序是:count:0 => cb1 count:2 => p1 count:2
- count 第一次设置值时,触发更新,watcher进入queue列表,即queue.push(watcher), 此时更新任务已经进入微任务队列,即微任务队列为[flushCallbacks],且callbacks = [flushSchedulerQueue],只有flushSchedulerQueue执行完后,dom才会更新
- count 第二次更新时,因为当前组件的渲染watcher已经在queue列表里,所以不会进入queue列表
- Promise会把回调函数p1推入微任务队列,所以此时微任务队列为:[flushSchedulerQueue, p1]
- 因为本轮宏任务还未执行完成,所以微任务队列也还没有执行,即更新任务还未执行,所以此时dom还没更新,dom上的count值仍旧为0
- 执行nextTick时,会把回调函数(cb1)推入callbacks列表,所以此时微任务队列为[flushCallbacks, p1],即[[...callbacks], p1] ,且 callbacks = [flushSchedulerQueue, cb1]
- 当宏任务执行完成,开始遍历执行微任务,此时微任务为[flushCallbacks, p1],会先执行flushCallbacks,即遍历执行callbacks = [flushSchedulerQueue, cb1]
- flushSchedulerQueue先执行,执行完成后,此时dom已经更新
- cb1执行时,dom已经改变,所以此时dom的count值为2,所以打印 cb1 count:2
- 然后执行第二个微任务p1,即 p1 count:2
例子三
countClick() {
Promise.resolve().then(() => {
console.log("p1 count:", this.$refs.count.innerHTML); // count: 0
});
this.count = 1; // 触发更新, flushSchedulerQueue 存入callbacks, flushCallbacks才进入微任务队列
this.count = 2;
console.log("count:", this.$refs.count.innerHTML); // count: 0 属于宏任务,先执行,dom还未更新,所以count值还未改变为 0
this.$nextTick(() => {
console.log("cb2 count:", this.$refs.count.innerHTML); // count: 2
});
}
流程解析:
- 打印顺序 count:0 => p1 count:0 => cb1 count:2
- 因为Promise在 count设值之前,已经进入了微任务队列,count设值时,flushSchedulerQueue 存入callbacks, flushCallbacks才进入微任务队列
- 所以最终的微任务队列为 [p1, flushCallbacks],所以p1 打印在dom更新前
例子四
countClick() {
this.$nextTick(() => {
this.count = 3 // 触发一次更新,但是没有进入queue队列
console.log("cb1 count:", this.$refs.count.innerHTML); // count: 2
});
Promise.resolve().then(() => {
console.log("p1 count:", this.$refs.count.innerHTML); // 进入微任务队列
});
this.count = 1;
this.count = 2;
this.$nextTick(() => {
console.log("cb2 count:", this.$refs.count.innerHTML); // count: 2
});
}
流程解析:
- 打印顺序 cb1 count:0 => cb2 count:3 => p1 count: 3
- 微任务队列为:[flushCallbacks, p1], 且callbacks = [cb1, flushSchedulerQueue, cb2], cb1在执行的时候,flushSchedulerQueue还未执行,所以dom还未更新,所以打印时dom的count的值还是 0。
- flushSchedulerQueue执行时,count的值已经在cb1更新为3,所以最终count的值为3
- 所以flushCallbacks执行后打印顺序为: cb1 count:0 => cb2 count:3
- 最后执行p1,打印p1 count:3
总结
- 每个组件都有自己对应的渲染watcher,用于执行dom的更新
- 组件内,在一个宏任务内,多个属性的更新或者同个属性的多次更新,watcher只会渲染一次
- 异步更新流程:属性更新 => 通知watcher更新 => queueWatcher(this) => nextTick(flushSchedulerQueue)
- 属性更新 只会将flushSchedulerQueue推入微任务队列,只有在flushSchedulerQueue执行完后,dom才会更新完成
相关文章
Vue源码解析——组件更新过程:https://blog.csdn.net/comedyking/article/details/115670343
Vue-Watcher观察者源码详解:https://blog.csdn.net/comedyking/article/details/117695761