渲染函数的异步更新流程
首先,watcher.update触发更新,然后调用queueWatcher(watcher),主要作用是把watcher添加入更新队列中: 判断watcher.id是否在has.id中或值是否是true,如果有就证明该watcher已添加到更新队列里了,不需要再添加了。
如果has.id不是true,就要把当前watcher放入到更新队列中。在放入之前,还要判断这个更新队列是否正在执行更新,如果没有正在执行更新,那么直接把当前watcher放到队列末尾就可以;如果是正在执行中,就从队列最后一个开始往前找,遇到第一个比新增的这个watcher的id小的watcher,就把这个新增的watcher放到这个watcher的后面。
接下来是一个判断语句,判断变量waiting是不是false,如果是false,证明之前没走过if里面的内容,因为if里会把waiting设置为true。那么这个if里做什么呢?其实是调用nextTick函数。也就是说,nextTick函数只会被调用一次,原因就是waiting。为什么要执行一次呢?因为更新的逻辑是在nextTick的参数flushSchedulerQueue函数里的,调用nextTick函数主要是为了把这个flushSchedulerQueue函数放到异步里,所以调用一次nextTick就达到目的了。
我们发现,nextTick的入参是个名为flushSchedulerQueue的函数,这个函数是啥?其实就是renderWatcher们执行更新的函数,这个函数在执行中,会遍历queue里的所有renderWatcher,先排序,然后挨个执行renderWatcher的run方法进行更新。同时把has.id设置为null,目的是把执行更新了的watcher在缓存中清除,flushing设置为true表示正在执行renderWatcher们的更新。后面还会调用activated和updated钩子函数。这个在vue的生命周期里再讲。
我们讲完了flushSchedulerQueue函数的实现逻辑,但是它是什么时候执行的呢?那就需要看看nextTick函数了。
nextTick就是把入参方法封装到一个函数中,然后放到callbacks数组中。入参方法是通过call来执行。然后会根据pending的值判断是否调用异步方法。为何要加这个判断呢?因为所有的异步更新,放到一个异步任务里就可以了,所以不需要多次调用。
当主线程执行完毕,调用异步方法时,会执行flushCallbacks函数,这个方法会执行callbacks数组里的所以方法,这些方法就是在调用nextTick时,添加进来的参数。
其实在flushCallbacks函数中,也不是直接进行callbacks数组遍历回调的,是把callbacks复制了一份,然后把callbacks清空了(callbacks.length=0),然后遍历副本进行回调的。为什么这么做呢。因为是这种场景:
created () {
this.name = 'a'
this.$nextTick(() => {
this.name = 'b'
this.$nextTick(() => { console.log('第二个 $nextTick') })
})
}
理论上外层 nextTick 方法的回调函数在同一个 microtask 任务中被执行,而是两个不同的 microtask 任务,虽然在结果上看或许没什么差别,但从设计角度就应该这么做。
我们来分析下这个例子的流程:
当name变化时,会触发渲染函数的更新,渲染函数的watcher被添加到queue里,然后调用nextTick创建异步任务,把flushSchedulerQueue方法放入到callbacks数组里等待更新,然后在再执行第一个nextTick的参数函数放入到callbacks里:
callbacks: [
flushSchedulerQueue,
() => {
this.name = 'b'
this.$nextTick(() => { console.log('第二个 $nextTick') })
}
]
接下来主线程执行完毕,开始执行异步任务,就是回调callbacks里的方法了。执行到flushCallbacks函数时,会把pending设置为false,然后执行flushSchedulerQueue中的更新函数,这时存放renderWatcher的queue就会置空。
执行完毕后,会继续执行第一个nextTick中的函数的更新,会在一个新的异步任务中呈现。
// microtask 队列
[
flushCallbacks, // 第一个 flushCallbacks
flushCallbacks // 第二个 flushCallbacks
]
callbacks: [
flushSchedulerQueue,
() => {
this.name = 'b'
this.$nextTick(() => { console.log('第二个 $nextTick') })
}
]
callbacks: [
flushSchedulerQueue,
() => {
console.log('第二个 $nextTick') }
}
]
调用vue.$nextTick()的异步更新流程
调用vue.nextTick()都是在页面更新完毕后才调用的。
宏任务微任务选取方案
因为js是单线程的,所以为了异步操作能顺利执行,就出现了队列,可以有序的执行异步回调方法。
Vue默认是采用的微任务。因为主线程执行完后,会先获取微任务队列里的所有微任务进行执行,然后进行一次页面渲染(宏任务与宏任务之间会有一次页面渲染,主线程也是宏任务),然后再执行宏任务队列里的任务。这样在微任务中进行数据更新,然后再进行页面刷新,是最高效的。
如果浏览器不支持Promise,也有降级方案:首先使用setImmediate,如果不支持就用MessageChannel,如果还不支持就用setTimeout了。