vue异步更新

108 阅读4分钟

渲染函数的异步更新流程

首先,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方法的回调函数不应该与内层nextTick 方法的回调函数不应该与内层 nextTick 方法的回调函数在同一个 microtask 任务中被执行,而是两个不同的 microtask 任务,虽然在结果上看或许没什么差别,但从设计角度就应该这么做。

我们来分析下这个例子的流程:

当name变化时,会触发渲染函数的更新,渲染函数的watcher被添加到queue里,然后调用nextTick创建异步任务,把flushSchedulerQueue方法放入到callbacks数组里等待更新,然后在再执行第一个nextTick,把第一个nextTick,把第一个nextTick的参数函数放入到callbacks里:

callbacks: [
  flushSchedulerQueue,
  () => {
    this.name = 'b'
    this.$nextTick(() => { console.log('第二个 $nextTick') })
  }
]

接下来主线程执行完毕,开始执行异步任务,就是回调callbacks里的方法了。执行到flushCallbacks函数时,会把pending设置为false,然后执行flushSchedulerQueue中的更新函数,这时存放renderWatcher的queue就会置空。

执行完毕后,会继续执行第一个nextTick参数的回调函数,这时又设置了name的值,又一次把renderWatch放入到queue中,这时waiting已经是还原为false了,所以会再次调用nextTick,而pending也再上一次的flushCallbacks里置为了false,所以又回创建一个异步任务,这样第一个nextTick参数的回调函数,这时又设置了name的值,又一次把renderWatch放入到queue中,这时waiting已经是还原为false了,所以会再次调用nextTick,而pending也再上一次的flushCallbacks里置为了false,所以又回创建一个异步任务,这样第一个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()其实就是在执行时把参数函数放到callbacks数组里,在异步中回调。在回调的时刻,页面其实在flushSchedulerQueue方法里已经执行完了,所以vue.nextTick()其实就是在执行时把参数函数放到callbacks数组里,在异步中回调。在回调的时刻,页面其实在flushSchedulerQueue方法里已经执行完了,所以vue.nextTick()都是在页面更新完毕后才调用的。

宏任务微任务选取方案

因为js是单线程的,所以为了异步操作能顺利执行,就出现了队列,可以有序的执行异步回调方法。

Vue默认是采用的微任务。因为主线程执行完后,会先获取微任务队列里的所有微任务进行执行,然后进行一次页面渲染(宏任务与宏任务之间会有一次页面渲染,主线程也是宏任务),然后再执行宏任务队列里的任务。这样在微任务中进行数据更新,然后再进行页面刷新,是最高效的。

如果浏览器不支持Promise,也有降级方案:首先使用setImmediate,如果不支持就用MessageChannel,如果还不支持就用setTimeout了。