Vue2 的 Watcher 执行队列 & nextTick 原理

983 阅读3分钟

Vue2 的数据响应式最终是触发 Watcher 更新视图,但 Watcher 并不是马上更新视图的,而是有一个更新队列。

从源码可以看出,dep 通过调用 Watcherupdate 方法通知其数据的更新。在 update 方法中,Watcher 会调用 queueWatcher 方法。

queueWatcher

queueWatcher 实际是把 watcher 放入一个队列。先从 has[id] 判断队列中是否有这个 watcher,有则不加入队列。因此短时间内多次改变数据,视图只会更新一次。

接下来是个标志位 flushingflushing 表示是否正在刷新队列,即执行队列中的 watcher,以更新视图。可以看出,当队列没有执行时,新的 watcher 会加入队列的末尾;而当队列正在执行时, watcher 会按 id 顺序插入。index 表示当前执行的 watcher 的队列下标。

然后是 waiting 标志位,表示当前是否有队列即将执行,没有的话在 nextTick 执行队列。

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }

    if (!waiting) {
      waiting = true

      // ...
      nextTick(flushSchedulerQueue)
    }
  }
}

watcher 队列有三种状态:

  1. flushing=falsewaiting=false:此时处于上一个队列刚执行完成,还没有 watcher 加入队列;或者程序初始化的时候。
  2. flushing=falsewaiting=true:此时处于队列即将执行。即第一个 watcher 进入队列之后,到 nextTick 回调执行这段时间。期间可能还会有多个 watcher 进入队列,这时 watcher 是通过 push 的方式进入队列。
  3. flushing=truewaiting=true:队列正在执行,实际上会逐个调用 watcherrun 方法更新视图。此时进入队列的 watcher 则会按 id 顺序插入到对应位置。

flushSchedulerQueue

flushSchedulerQueue 是队列的具体执行。这段注释比较重要所以没有删除。

首先是 watcher 按 id 排序,后面按顺序执行,原因是为了保证以下三点:

  1. 组件的更新顺序是从父组件到子组件(因为父组件总会比子组件先创建)
  2. 用户的 watcher 在渲染 watcher 之前执行(因为用户的 watcher 在渲染 watcher 创建)。这里的用户 watcher 应该是指 computedwatch 这些属性的 watcher,它们在 create 的阶段创建;而渲染 watcher 在 mount 阶段创建
  3. 当一个组件被销毁时其父组件的 watcher 正在执行,其 watcher 可以被跳过

上面的 queueWatcher 方法中也保证了在队列执行时新进入的 watcher 按照 id 的顺序。

接着就是遍历队列,按顺序执行 watcherrun 方法。需要注意的是不能缓存 queue.length 的值,执行过程中可能还会有 watcher 进入队列。而 run 方法最终会执行 vm._update() 方法。

最后是 resetSchedulerState 函数,重置标志位和队列等的状态。

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  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 = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    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)

  // ...
}

总结起来,这个队列的特点是:

  1. 队列中的 watcher 不重复
  2. 队列执行过程中,watcher 仍可进入队列
  3. 不过 watcher 何时进入队列,都会按顺序执行

nextTick

nextTick 其实就是异步执行,不过做了一些降级兼容的方案。从 PromiseMutationObserver,再到 setImmediate,最后是 setTimeout,从微任务到宏任务。

nextTick 也是先将所有要执行的函数先缓存起来,timerFunc 就是异步的部分,其会异步调用 flushCallbacks,可理解为将任务加入异步队列。

nextTick 函数体中的 _resolve 参数提供了 Promise 的调用方式,如 nextTick().then()

flushCallbacks 执行时会重置 callbacks,后续加入的回调函数又是一组新的 callbacks。和 pending 标志位相结合,即是说,在 flushCallbacks 进入异步队列,但是还没执行这期间加入的回调函数将作为一个异步任务一起执行;而当这个异步任务开始执行,新加入的回调函数将被当成另一个异步任务。

假设回调函数源源不断地加入,那这里的操作就是攒一波执行一次,相当于节流。

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }

  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

timerFunc 就是将任务加入异步队列的部分,总体没啥好说的。MutationObserver 的使用比较有意思。

其创建的一个文件节点(没有插入真实的文档中),用 MutationObserver 对这个节点进行监听,flushCallbacks作为回调函数。然后用 js 改变这个节点的值来触发 observer。

let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}