Vue - nextTick原理

105 阅读2分钟

几个变量、函数

callbacks

callbacks数组用于保存nextTick创建的函数(在这个函数的内部会执行nextTick传入的函数)

flushCallbacks

flushCallbacks方法用于遍历、执行callbacks中的函数。首先会通过callbacks.splice复制回调函数数组,并清空callbacks

function flushCallbacks() {
  // 表示正在执行callbacks中的函数
  pending = false
  // 复制callbacks中的函数,并清空callbacks
  const copies = callbacks.slice(0)
  callbacks.length = 0
  // 遍历、执行函数
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

timerFunc

timerFunc方法用于触发flushCallbacks方法的执行。它会根据运行环境采取不同的触发方式,优先级按照先后顺序如下:

  • 支持原生的Promise:通过Promise.resolve().then(flushCallbacks)实现异步
  • 支持原生的MutationObserver:通过new MutationObserver(flushCallbacks)实现异步
  • 支持原生的setImmediate:通过setImmediate(flushCallbacks)实现异步
  • 以上都不支持:通过setTimeout(flushCallbacks)实现异步
let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // Cond: 支持原生的Promise
  const p = Promise.resolve()
  // timerFunc的内部通过Promise.resolve().then()触发flushCallbacks
  timerFunc = () => {
    p.then(flushCallbacks)
  }
} else if (
  typeof MutationObserver !== 'undefined' &&
  (isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
  // Cond: 支持原生的MutationObserver
  let counter = 1
  // 创建一个MutationObserver实例
  // 当观察的节点变化时,触发flashCallbacks
  const observer = new MutationObserver(flushCallbacks)
  // 创建观察的文本节点
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  // timerFunc的内部通过修改观察的节点的数据,触发flushCallbacks
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Cond: 支持原生的setImmediate
  // timerFunc的内部通过setImmediat触发flushCallbacks
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Cond: 以上条件都不满足
  // timerFunc的内部通过setTimeout触发flushCallbacks
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

nextTick

nextTick用于在vue的数据更新之后,执行传入的函数。

为何能保证函数在状态更新之后被执行呢?

A:因为状态的更新是同步的,nextTick会将传入的函数放在微任务(根据运行环境的不同,也可能是宏任务)中执行。这样就能保证函数在数据更新之后被执行

function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
  // 如果执行nextTick没有传入函数,将返回一个Promise
  // _resolve就会用于保存Promise的resolve方法,用于改变Promise的状态
  let _resolve
  // 创建一个新的函数,并放进callbacks中
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e: any) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      // 将Promise的状态改为fulfilled
      // 值为更新状态后的上下文
      _resolve(ctx)
    }
  })

  if (!pending) {
    // Cond: 没有在执行flushCallbacks
    // 则执行timerFunc
    pending = true
    timerFunc()
  }

  // 如果没有传入函数,则返回一个Promise实例
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      // 创建Promise实例,并将resolve指向_resolve
      _resolve = resolve
    })
  }
}