A brief introduction to $nextTick

We know that for Vue, the process from data change to DOM update is asynchronous and happens in the next tick.

It creates an update queue, in which watcher for each property is maintained, executing and updating them as needed.

A deferred callback is performed after the next DOM update loop ends. Use this method immediately after modifying the data to get the updated DOM.

Vue.nextTick()
    .then(function () {
    	// DOM is updated
	})
Copy the code

So how is the implementation of Vue 2.5 different from Vue 2.6 for such a core function?

You may need to take a quick look at the JS Event loop. Simulation implementation of THE JS engine: In-depth understanding of the JS mechanism and Microtask and Macrotask

Vue 2.5 nextTick implementation

In Vue 2.5, the implementation of nextTick is a combination of microTimerFunc and macroTimerFunc, see the source code.

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'

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]()
  }
}

// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we use microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false

// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
  macroTimerFunc = (a)= > {
    setImmediate(flushCallbacks)
  }
} else if (typeofMessageChannel ! = ='undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = (a)= > {
    port.postMessage(1)}}else {
  /* istanbul ignore next */
  macroTimerFunc = (a)= > {
    setTimeout(flushCallbacks, 0)}}// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise! = ='undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = (a)= > {
    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)
  }
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}

/** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a (macro) task instead of a microtask. */
export function withMacroTask (fn: Function) :Function {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true
    const res = fn.apply(null.arguments)
    useMacroTask = false
    return res
  })
}

export function nextTick (cb? : Function, ctx? : Object) {
  let _resolve
  callbacks.push((a)= > {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')}}else if (_resolve) {
      _resolve(ctx)
    }
  })
  if(! pending) { pending =true
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}
Copy the code

The key lines

let useMacroTask = false

if(! pending) { pending =true
    if (useMacroTask) {
    	macroTimerFunc()
    } else {
    	microTimerFunc()
    }
}
Copy the code

As noted here, Vue 2.5 will preferentially use microTimerFunc and demote to macroTimerFunc if there are compatibility issues.

Implementation of microTimerFunc: native Promise;

The realization of the macroTimerFunc: setImmediate | | MessageChannel | | setTimeout.

Vue 2.5’s Next-tick also exposes two functions: nextTick and withMacroTask, which are used to handle DOM interaction events, such as the v-ON-bound event callback, forcing macro Tasks out.

In addition, more details can be seen in the vue2.6 update study done by @Marsprince where nextTick always uses microTask.

Reading through the source code, I found that the logic is clear and refined, so what caused Vue to fix it in 2.6? Take a look at a note from Vue 2.6

// Here we have async deferring wrappers using microtasks.
// In 2.5 we use (macro) tasks In combination with microtasks.
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
Copy the code

Two main issues are clarified:

  • [Fixed] There are minor issues with state changes before redrawing
  • When using Macro Task to handle events, there are a number of weird issues that can’t be avoided.

To briefly describe these two problems:

For the first question, see the figure

Give it a try, described at github.com/vuejs/vue/i… In CSS, @media media query is defined. Js window listens for resize event, so when the fixed threshold is triggered, the state changes and the style needs to be redrawn, which causes problems.

The second problem, which can be summarized as the use of macroTask to process DOM operations, sometimes causes the gap between trigger and execution to be too large. For example, on the mobile end, the handler clicked is not in the same tick as the audio playback function.

Vue 2.6 nextTick implementation

Due to the above problems, in the implementation process of Vue 2.6, microtasks are used instead of the previous solution, see the source code for details.

/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

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]()
  }
}

// Here we have async deferring wrappers using microtasks.
// In 2.5 we use (macro) tasks In combination with microtasks.
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
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 = (a)= > {
    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)
  }
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  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 = (a)= > {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Techinically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = (a)= > {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = (a)= > {
    setTimeout(flushCallbacks, 0)}}export function nextTick (cb? : Function, ctx? : Object) {
  let _resolve
  callbacks.push((a)= > {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')}}else if (_resolve) {
      _resolve(ctx)
    }
  })
  if(! pending) { pending =true
    timerFunc()
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}
Copy the code

Vue 2.6 makes use of the two most typical microtasks, promise.then and Mutation observers, and adds setImmediate and setTimeout as degradation schemes.

Only one interface is exposed, next-tick, and microTask is used to handle event handlers. This approach solves the above problems, but it also has an obvious disadvantage. Due to the high priority of microTask, this causes problems when event events are fired continuously, as detailed in the comments.