lyhper blog

理解Vue.nextTick源码

前言

Vue.nextTick用于延迟执行一段代码,它接受2个参数(回调函数和执行回调函数的上下文环境),如果没有提供回调函数,那么将返回promise对象。

奉上源码(vue 2.4.4)

代码位于 src/core/util/env.js

/**
 * Defer a task to execute it asynchronously.
 */
export const nextTick = (function () {
  const callbacks = []
  let pending = false
  let timerFunc

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

  // the nextTick behavior leverages the microtask queue, which can be accessed
  // via either native Promise.then or MutationObserver.
  // MutationObserver has wider support, however it is seriously bugged in
  // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
  // completely stops working after triggering a few times... so, if native
  // Promise is available, we will use it:
  /* istanbul ignore if */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      // in problematic UIWebViews, Promise.then doesn't completely break, but
      // it can get stuck in a weird state where callbacks are pushed into the
      // microtask queue but the queue isn't being flushed, until the browser
      // needs to do some other work, e.g. handle a timer. Therefore we can
      // "force" the microtask queue to be flushed by adding an empty timer.
      if (isIOS) setTimeout(noop)
    }
  } else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    // use MutationObserver where native Promise is not available,
    // e.g. PhantomJS, iOS7, Android 4.4
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else {
    // fallback to setTimeout
    /* istanbul ignore next */
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  return function queueNextTick (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, reject) => {
        _resolve = resolve
      })
    }
  }
})()

解析

Vue.nextTick函数采用了闭包,用了3种方法来实现延迟执行,分别是:

  • promise
  • MutationObserver
  • setTimeout

MutationObserver

MutationObserver用于实例化一个观察者对象,当它观察的目标节点发生变化时将执行回调函数。MutationObserver有着更广泛的支持,但是它在iOS 9.3.3之后存在一些bug。

从头开始阅读代码

变量

首先声明3个变量,callbacks用来存储所有需要执行的回调函数,pending用来标志是否正在执行回调函数,timerFunc用来触发执行回调函数。

遍历执行所有回调函数

接着声明nextTickHandler函数,这个函数用来执行callbacks里存储的所有回调函数。

将触发方式赋值给timerFunc

接着判断是否原生支持promise,如果支持,则利用promise来触发执行回调函数;

如果支持MutationObserver,则实例化一个观察者对象,观察文本节点发生变化时,触发执行所有回调函数。每次调用timerFunc时,会对文本节点进行重新赋值

如果都不支持,则利用setTimeout设置延时为0

接受用户传入的参数

最后返回一个queueNextTick函数,用来往callbacks里存入回调函数,这里可以支持回调函数和promise两种形式。

接着判断如果没有在执行回调函数,则调用timerFunc来触发执行回调函数,从而执行用户传入的代码。

最后判断,如果用户使用的promise,则返回一个promise对象,并将resolve函数赋值给_resolve,从而在timerFunc调用时触发执行用户的代码