系列文章:
- Vue2.6x源码解析(一):Vue初始化过程
- Vue2.6x源码解析(二):组件初始化
- Vue2.6x源码解析(三):深入响应式原理
- Vue2.6x源码解析(五):Vue应用加载流程【多图预警!推荐收藏】
本节将深入理解Vue的异步更新原理。
根据Vue
官方文档:Vue在更新dom
的时候是异步执行的,但其实Vue中很多的回调【比如watch
的回调、组件更新、nextTick
】都是异步执行的,下面我们先理解一下整体的异步更新流程,然后我们再详细的讲解每个步骤:
- 修改响应式数据。
- 在响应式数据的
setter
中触发dep.notify()
。 - 循环
dep
的subs
属性列表,通知subs
中的每个watcher
实例执行自身的update
更新方法。 watcher
实例的update
方法会默认执行异步操作queueWatcher(this)
,将自身实例推入到一个queue
队列中。然后会立即执行一次nextTick(flushSchedulerQueue)
方法,将刷新 queue 队列的flushSchedulerQueue
方法添加到callbacks
数组。注意: 一次tick
任务,刷新队列的调度方法只会被推入callbacks
一次。只有在本轮任务执行完成之后,才会重置waiting
状态,在下轮dom更新任务中重新添加一次刷新队列的方法。每一轮的dom更新nextTick(flushSchedulerQueue)
只会执行一次。- 在
nextTick
内部将cb
回调任务推入callbacks
数组后,会立即执行timerFunc()
方法,将flushCallbacks
函数添加到浏览器的微任务队列【默认采用的Promise.then
】。注意: 同上面一样,一轮dom更新中只会执行一次timerFunc()
方法,微任务队列也只会有一个flushCallbacks
函数,需要处理的所有回调任务都存储在callbacks
数组中,在本次宏任务中,会一直向callbacks
数组中添加任务,直到本次宏任务结束,开始执行微任务队列,即执行flushCallbacks
方法处理任务。 - 最后在执行
flushCallbacks
方法的时候,就会循环执行callbacks
数组中的每个cb函数,即冲刷队列的flushSchedulerQueue
方法和用户传入的nextTick
回调。用户传入的nextTick
回调比较简单,重点关注flushSchedulerQueue
方法。注意:callbacks
数组中只会存在一个刷新队列flushSchedulerQueue
任务和多个用户传入的nextTick
任务。并且flushSchedulerQueue
默认都是第一个开始执行,并且负责dom更新的watcher
实例存储在queue
中,而flushSchedulerQueue
方法就是专门用于处理queue队列。所以用户传入的nextTick
任务才会在 DOM 更新完成后被调用。 flushSchedulerQueue
方法会循环调用queue
队列中的每个watcher
实例的run
方法,实际上最终就是在run
内部执行每个watcher的cb/getter
函数,比如用户定义的watch
的回调函数,组件更新render watcher的回调函数即updateComponent
方法。
下面我们将仔细分析每一个步骤,深入理解每个执行过程:
1,notify
修改一个响应式数据:
this.count = 1;
在setter
中触发依赖:
// defineReactive 修改响应式数据,触发依赖
set: function reactiveSetter (newVal) {
dep.notify()
}
继续查看notify
源码:循环subs
依赖列表,执行每个watcher
实例自身的update
更新方法:
notify () {
// 复制一份依赖列表(subs里面都是watcher)
const subs = this.subs.slice()
// 所有依赖:循环触发watcher实例的更新方法
for (let i = 0, l = subs.length; i < l; i++) {
// 也就是Watcher中的update方法。
subs[i].update()
}
}
继续查看update
源码:
update () {
if (this.lazy) {
// 计算属性使用
this.dirty = true
} else if (this.sync) {
// 同步执行回调
this.run()
} else {
// 异步执行回调:将当前watcher实例推送到队列
# vue默认都是异步执行回调,可以查看前面的解析
queueWatcher(this)
}
}
2,queueWatcher
继续查看queueWatcher
源码:
// watcher队列
// 推入一个watcher实例到queue队列,跳过has[id]重复的watcher
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 这里用has[id]第一次是undefined, 非全等的情况下:Boolean(undefined == null)为true
if (has[id] == null) {
// 给has对象定义一个属性,每一个watcher.id都是唯一的
has[id] = true
if (!flushing) {
# 将watcher实例添加到queue队列
queue.push(watcher)
} else {
let 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函数放进一个callbacks数组中
nextTick(flushSchedulerQueue)
}
}
}
根据queueWatcher
源码可知,这个方法主要有两个作用:
- 第一个就是将传入的
watcher
实例推入到queue
队列。 - 第二个作用就是执行
nextTick(flushSchedulerQueue)
方法,而要执行第二个方法,得根据waiting
变量的状态。
flushSchedulerQueue
方法:专门用于处理queue队列中的任务。
// 默认为false
let waiting = false
if (!waiting) {
waiting = true
// 在一轮dom更新中只会被执行一次
nextTick(flushSchedulerQueue)
}
根据上面的代码我们可以看出:第一次执行queueWatcher(this)
后,waiting
状态被锁住,后续执行queueWatcher(this)
,将不会再执行nextTick
方法,只会向queue
队列中继续添加watcher
实例。
而waiting
的重置在flushSchedulerQueue
方法中处理,后面我们再分析。
3,nextTick
下面我们继续查看nextTick
源码:
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 推送任务:调用一次nextTick,就会像callbacks中推入一个任务
// 这里将传入的cb回调函数,包装了一层,成为一个可以处理error的函数
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
# 添加到微任务队列
timerFunc()
}
}
callbacks
是一个回调任务数组,前面我们执行nextTick(flushSchedulerQueue)
后,就会将flushSchedulerQueue
函数添加到callbacks
数组。注意: nextTick
不是直接将cb
回调函数添加到数组中,而是包裹了一层箭头函数,这是为了方便执行回调函数的异常处理。
同时根据上面nextTick
的源码,我们可以发现它和queueWatcher
的源码思想非常类似,其内部都是两个作用:
- 将参数推入到目标数组。
- 根据状态锁执行一次目标方法。
所以这里的timerFunc
同样只会执行一次:
// 第一次执行
# 在一次宏任务中nextTick/timerFunc都只会被执行一次,在dom更新完成后解锁状态
queueWatcher(this) => nextTick(flushSchedulerQueue) => timerFunc()
timerFunc
方法非常重要,是Vue
异步执行回调的核心,根据外部环境来进行设置:
// 默认是采用Promise.then的微任务队列实现异步更新
timerFunc = () => {
Promise.resolve().then(flushCallbacks)
timerFunc()
一调用就会触发Promise.then()
,将flushCallbacks
方法添加到【微任务队列】。
按照环境的支持程度优先级:
- Promise 微任务
- MutationObserver 微任务
- setImmediate 宏任务
- setTimeout 宏任务
timerFunc()
方法的执行目的:是将flushCallbacks
添加到微任务队列。
4,flushCallbacks
下面我们继续查看flushCallbacks
源码:
function flushCallbacks () {
pending = false
// 复制callbacks数组:slice(0)可简写为.slice()
const copies = callbacks.slice(0)
callbacks.length = 0
# 循环执行copies中的每个cb回调任务
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
可以看出flushCallbacks
方法内部很简单,就是循环执行callbacks
数组中的每个回调任务。
其实到这里我们就已经可以总结出Vue的异步更新原理: 一句话理解就是利用Promise.then()
将flushCallbacks
方法添加到微任务队列,执行flushCallbacks
方法,循环callbacks
数组处理所有的回调任务。
根据前面我们也知道callbacks
数组中只有两种回调任务:flushSchedulerQueue
以及用户自定义的nextTick
回调。用户传入的回调函数比较简单,下面我们深入分析flushSchedulerQueue
源码:
5,flushSchedulerQueue
下面我们继续查看flushSchedulerQueue
源码:
# 冲刷queue队列
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// flush队列前先排序
// 队列中watcher根据watcher.id 从小到大排序
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
// 取出每个watcher
watcher = queue[index]
if (watcher.before) {
// 如果是组件watcher,触发组件的beforeMount钩子
watcher.before()
}
id = watcher.id
// 清除标记
has[id] = null
# 执行watcher实例中的cb/getter函数
watcher.run()
}
// keep copies of post queues before resetting state
// 保留队列副本
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
# 重置状态
resetSchedulerState()
// call component updated and activated hooks
// 触发生命周期钩子
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
// 组件更新完成后,刷新开发者工具
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
flushSchedulerQueue
方法非常重要:用户定义的watch
回调以及组件更新的updateComponent
方法都在flushSchedulerQueue
方法内部执行,flushSchedulerQueue
方法核心作用就是处理queue队列中的任务。
根据上面的源码可知,flushSchedulerQueue
方法内部首先将queue
队列任务按照watcher.id
从小到大排序,然后循环队列中的watcher
调用watcher.run()
方法。
注意:
watcher.id
一定是renderWatcher
最大,因为renderWatcher
是组件初始化最后才创建的,也就是说负责组件更新渲染的回调函数是最后才执行的。
我们继续回到watcher.run()
方法:
// class Watcher
run () {
if (this.active) {
// 执行get方法,内部会调用watcher.getter【组件的updateComponent函数就存储在getter中】
const value = this.get()
if (value !== this.value || isObject(value) || this.deep) {
// 取出旧值
const oldValue = this.value
// 设置新值
this.value = value
// 判断是不是用户定义的watcher,比如watch ,如果是就需要用可以处理error的方法来处理cb
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
// 内部生成的watcher,直接调用cb
this.cb.call(this.vm, value, oldValue)
}
}
}
}
可以看到run
方法的作用:就是调用watcher
自身的cb/getter
函数,所以最终用户定义watch
监听回调或者组件渲染更新的回调都会在这里执行。
在处理完所有queue
队列任务后,还有一个比较重要的处理就是重置几个变量的状态,这里为了下一次的更新使用:
// 重置调度程序的状态
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}
最后触发updated
生命周期钩子函数,本轮的更新任务就此完成。
到这里,我们也就知道了为啥要将操作dom
的nextTick
放在数据修改之后?
vm.message = 'new message' // 更改数据
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
})
结论: 因为只要我们修改了任何一个数据,就会触发组件的renderWatcher
【当然这个响应式数据必须在template
模板中被引用过】,flushSchedulerQueue
方法会被第一个推入callbacks
数组,操作dom的nextTick
回调会排在后面,等到操作dom的nextTick
回调执行时,flushSchedulerQueue
已经执行完成,此时组件内已经最新的dom
结构,这时我们才会获取到正确的dom数据。如果我们将操作dom的nextTick回调放在修改数据之前,就将无法获取到所需要的dom数据。
原文链接:https://juejin.cn/post/7221434573829079095 作者:江北__张小凡