Vue的异步更新和nextTick详解

95 阅读3分钟

我们先来分析下面这段代码的执行顺序:

import { ref, watch, nextTick, onMounted } from "vue"
const count = ref(0)

watch(count, (val) => {
  console.log(val)
})

const changeCount1 = () => {
  nextTick(() => {
    count.value = 10 // 同步任务下的微任务
  })
  count.value = 20 // 同步任务下的微任务
}

const changeCount2 = () => {
  setTimeout(() => {
    // 宏任务
    nextTick(() => {
      // 当前宏任务下的微任务
      count.value = 100
    })
    count.value = 200 //对应的watchers也是当前宏任务下的微任务
    nextTick(() => {
      // 当前宏任务下的微任务
      count.value = 300
    })
  }, 0)
}

onMounted(() => {
  changeCount1()
  changeCount2()
})

上面代码最终的输出顺序是: 20、10、100、300

  1. 在这个页面之外路由currentRoute,路由currentRoute变更,新增一个微任务1到微任务队列中。微任务1执行的函数,会遍历watcher数组,执行所有的watcher

  2. 执行nextTick(count.value=10),新增微任务2到微任务队列

  3. 执行count.value=20,新增一个watcher到watcher数组

  4. 执行完同步任务后,遍历微任务队列

  5. 执行微任务1,遍历watcher数组,执行所有watcher,watcher数组变为空,此时输出count为20

  6. 执行微任务2,输出count=10

  7. 执行完所有微任务后,开始执行宏任务队列中的第一个宏任务,即此处的setTimeout的回调函数

  8. 执行nextTick(count.value=100),新增微任务3到微任务队列中

  9. 执行count.value=200,修改count=200,新增微任务4到微任务队列,微任务4同微任务1的回调函数,遍历watcher数组,执行watcher

  10. 执行nextTick(count.value = 300),新增微任务5到微任务队列中

  11. 宏任务中的同步脚本执行完,后遍历微任务队列

  12. 执行微任务3,修改count为100,修改count为100,新增一个watcher到watcher数组中

  13. 执行微任务4,遍历watcher数组,执行wach回调函数,此时输出count为100

  14. 执行微任务5,修改count=300,此时输出count为300

Vue中有三种副作用,都是通过new ReactiveEffect()创建的

  • watcher 副作用, 一个组件中有多个watcher
  • computed 副作用,一个组件中有多个computed
  • render 副作用,一个组件中只有一个render

这些副作用都是通过new ReactiveEffect实现的

class ReactiveEffect {
  constructor(fn, trigger, scheduler, scope) {
    this.fn = fn;
    this.trigger = trigger;
    this.scheduler = scheduler;
    this.active = true;
    this.deps = [];
    /**
     * @internal
     */
    this._dirtyLevel = 4;
    /**
     * @internal
     */
    this._trackId = 0;
    /**
     * @internal
     */
    this._runnings = 0;
    /**
     * @internal
     */
    this._shouldSchedule = false;
    /**
     * @internal
     */
    this._depsLength = 0;
    recordEffectScope(this, scope);
  }
  }

创建render副作用,源码如下:

const effect = instance.effect = new ReactiveEffect(
      componentUpdateFn,
      _vue_shared__WEBPACK_IMPORTED_MODULE_1__.NOOP,
      () => queueJob(update),
      instance.scope
    );

状态count的值修改后

count对应一个Dep,Dep是一个Map,Dep存放的是count对应的1个或多个ReactiveEffect(副作用)。

Vue中有一个异步队列,状态修改后,先将状态对应的副作用添加到异步队列中,再异步执行

  1. 执行triggerRefValue
  2. 执行triggerEffects,遍历Dep,将每个effect.scheduler加入到queueEffectSchedulers数组中
  3. 执行resetScheduling
  4. 遍历queueEffectSchedulers, 执行queueEffectSchedulers.shift()
  5. 执行queueJob,queue中push传入的job
  6. 执行queueFlush, 创建微任务,异步执行flushJobs
  7. 同步任务执行完,执行flushJobs, 遍历queue执行job
  8. 执行job即是执行watch传入的函数,computed传入的函数,或是render函数。