Vue 中的 nextTick

By赵的拇指At2019-07-16In5Views274

要说Vue中的nextTick,首先需要讲一下Vue中的异步更新策略,在Vue中数据发生变化之后不是立即反应到真实的DOM上去的,虽然视觉上可能是这样的,但是实际内部变化不是这样的。Vue中数据发生变化之后,会开启一个队列用来缓冲同一事件循环中的发生的所有的数据变化,然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际(已去重的)工作。(感觉和React的setState类似)(文档

数据变化后DOM不是立即变化的,所以开发人员想在DOM变化后立即做一些事情的时候就需要依靠nextTick的协助了(或者setTimeout,但是这显然不是一个优雅的方式)。

Vue中的nextTick是一个全局的api,在实例中可以直接通过this.$nextTick调用,他的作用是在修改数据之后,立即使用这个回调函数,可以获取更新后的 DOM。(文档)(ps: 如果当前环境支持promise则会返回一个promise对象,否则需要传入callback

看下Vue中关于nextTick核心源码

let timerFunc

// 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 next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // 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)
  }
  isUsingMicroTask = true
} 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
  // (#6466 MutationObserver is unreliable in IE11)
  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)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Techinically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

可以看到这块nextTick会优先使用Promise(es6+才支持)、MutationObserver(新api)这些microtask,如果没有再降级使用setImmediate(ie node 才支持)、setTimeout(兜底方案)这些macrotask。

  1. microtask因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
  2. 因为兼容性问题,vue不得不做了microtask向macrotask的降级方案