What does Vue’s nextTick function do: put the injected callback at the end of the event loop queue in the form of a micro-task, or the next event queue if not supported

This article uses the last version of Vue 2.x (2.6.14) to explain the source code

Step 1: Create the flushCallbacks callback function executor

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

The function flushCallbacks passed in by $nextTick is used to iterate through the execution callback function

Step 2: Create a microtask wrapper and determine whether the browser supports the microtask API assigned to timerFunc

// 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
Copy the code

Here we have an asynchronous delay wrapper that uses microtasks.

  1. Determine whether the browser supports promises, and if so, use promises to implement them
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
}
Copy the code

Here promise.resolve () is equivalent to the new Promise(resolve => resolve()) then function registration callback

  1. Check whether the browser supports MutationObserver. If so, use MutationObserver to implement it
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 = () = > {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} 
Copy the code

The MutationObserver API is described in MDN as providing the ability to monitor changes made to the DOM tree, which is called when the specified DOM changes.

Vue creates a text node and executes a MutationObserver registration function call by listening for changes in the text node. How does vue do this and how does the MutationObserver listen for DOM changes and microtasks? Please see my detailed explanation of MutationObserver (in writing)

  1. If the browser does not support either of the above microtask apis, use setImmediate/setTimeout to perform the callback asynchronously
else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically 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)}}Copy the code

Vue source says that setImmediate is a better choice than setTimeout because of its higher performance than setTimeout, but setImmediate currently only implements the method in the latest versions of Internet Explorer and Node.js 0.10+

  1. Once you figure out what apis the browser supports, the final step is to execute
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()
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}
Copy the code

The default value of pending in callbacks will be false if cb exists. If cb exists, the default value of pending in callbacks will be false if CB exists. The timerFunc microtask wrapper is then called to execute the callback we passed in

conclusion

  • Promise > MutationObserver > setImmediate > setTimeout (Tips: The order of the shim has been changed several times in the 2.x release and finally in the 2.x latest release.)