Why does Vue have an API called nextTick?

Pre-knowledge: the browser’s event loop mechanism

First of all, we need to understand the browser’s event loop mechanism, which I summarized in this javascript event loop mechanism parsing, and this in-depth browser event loop. The main processes and responsibilities of the browser are shown below:

Among them, and Vue’snextTickIt is closely relatedJS engine threadandEvent trigger thread.

  • After the first rendering of the browser page,JS engine threadandEvent trigger threadThe workflow is as follows:
    • The synchronization task is executed on the JS engine (main thread), forming the execution stack
    • Outside of the main thread, the event-triggering thread manages a task queue, and when an asynchronous task has a result, an event is placed in the task queue.
    • When the synchronous task in the execution stack is completed, the task in the task queue is read and added to the execution stack of the main thread and the corresponding asynchronous task is executed.

In the browser environment, the common methods for creating macro tasks are:

  • SetTimeout and setInterval
  • Network requests, I/O
  • Page interaction: DOM event callbacks, mouse, keyboard, scroll events
  • Page rendering

Common ways to create micro tasks are:

  • Promise.then
  • MutationObserve (DOM listening)
  • process.nextTick

The nextTick function uses these methods to process functions passed in with cb as asynchronous tasks.

Vue nextTick source analysis

NextTick function

  • NextTick function
var callbacks = [];
var pending = false;
function nextTick(cb, ctx) {  //cb? : Function, ctx? : Object
    var _resolve;
    callbacks.push(function() { 
        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(function(resolve) { _resolve = resolve; }}})Copy the code

The function passed in with the cb argument in the nextTick function is wrapped and pushed into the Callbacks array. The variable pending is used to ensure that the timerFunc() function is executed only once in an event loop. Finally the if (! cb && typeof Promise ! == ‘undefined’), returns a Promise instance if the cb parameter does not exist and the browser supports Promise. For example, nextTick().then(() => {}), when _resolve is executed, then logic is executed.

How is the function passed in with the cb argument wrapped?

When cb has a value, cb.call(CTX) is executed in the try statement. CTX is the argument passed to the function. If the execution fails, the error is caught by a catch. If cb has no value. Execute _resolve(CTX), because a Promise instance object will be returned if cb does not exist, and execute _resolve(CTX) to execute then logic.

  • TimerFunc () function

Take a look at the timerFunc() function, just the ptimerFunc function that uses Promise to create an asynchronous execution. The timerFunc function simply calls the flushCallbacks function with various asynchronously executed methods.

let timerFunc;
if (typeof Promise! = ='undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () = > {   
    p.then(flushCallbacks)  // execute the method call flushCallbacks asynchronously
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} 
Copy the code
  • FlushCallbacks function.
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

Execute pending=false to call timerFunc from nextTick in the next event loop. Execute const copies = callbacks. Slice (0) copies callbacks into constant copies and deletes callbacks. Copies are used to execute each function.

Summarize the logic of the nextTick function

A callbacks array is defined to simulate an event queue. A function passed in with the parameter cb is wrapped in a function that executes the passed function, handles the failure of execution, and the non-presence of the parameter cb, and then adds it to the Callbacks array. Call timerFunc, which calls the flushCallbacks function in a variety of asynchronously executed methods, copies each function in the callbacks, and executes it in flushCallbacks. A variable pending is defined to ensure that the timerFunc function is called only once in an event loop.

So the key here is how to define the timerFunc function. Since there are different ways to create asynchronous execution functions in different browsers, there are several ways to make them compatible.

Promise creates asynchronous execution functions

if (typeof Promise! = ='undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () = > {   
  The timerFunc function calls the flushCallbacks function with various asynchronously executed methods.
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} 
Copy the code

Firstly, isNative method is used to determine whether browser supports Promise. Typeof Promise supports function, so this condition is met and a Promise object in resolved state is returned directly. In timerFunc, p.chen (flushCallbacks) executes flushCallbacks directly, iterating through each function passed in by nextTick, Because the Promise is a micro Task type, these functions are executed asynchronously. Execute if (isIOS) {setTimeout(noop)} to add an empty timer in the IOS browser to force the refresh of the microtask queue.

/native code/.test(ctor.tostring ()) if Ctor is a function Check if the string after toString contains a fragment of native code. Function Promise() {[native code]} returns a Function Promise() {[native code]}.

function isNative(Ctor) {
    return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}
Copy the code

The timerFunc function is called from p.chen (flushCallbacks), which executes the flushCallbacks function directly, iterating through each function passed in by nextTick. Because the Promise is a micro Task type, these functions are executed asynchronously.

MutationObserver creates asynchronous execution functions

if(! isIE &&typeofMutationObserver ! = ='undefined' &&
    (isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]')) {var counter = 1;
    var observer = new MutationObserver(flushCallbacks);
    var textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
        characterData: true
    });
    timerFunc = function() {
        counter = (counter + 1) % 2;
        textNode.data = String(counter);
    };
    isUsingMicroTask = true;
}
Copy the code

Create and return a new MutationObserver, and pass in the flushCallbacks as a return function, which is called when the specified DOM changes and in which each nextTick passed in function is iterated to execute. Because MutationObserver is a micro Task type, these functions are executed asynchronously.

SetImmediate creates an asynchronous execution function

if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
  timerFunc = () = > {
    setImmediate(flushCallbacks)
  }
} 
Copy the code

SetImmediate is only compatible with IE10 and beyond, and not with any other browsers. It is a macro task and consumes relatively small resources

SetTimeout creates an asynchronous execution function

timerFunc = () = > {
    setTimeout(flushCallbacks, 0);
  }
Copy the code

Compatible with Internet Explorer 10 or less, create asynchronous tasks, which are macro tasks and consume large resources.

NextTick source in vue (vue\ SRC \core\util\next-tick.js)

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

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

export let isUsingMicroTask = false

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

// In version 2.5 we used a combination of microtasks and Macrotasks, but there were some minor issues with redrawing, and there were some very strange behaviors in the task queue with MacroTasks, So we went back to the previous state. Prioritize microTasks everywhere.
// 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 */


// Task execution priority
// Promise -> MutationObserver -> setImmediate -> setTimeout

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 &&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
} 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 = () = > {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () = > {
    setTimeout(flushCallbacks, 0)}}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

Ref 1: Do you really understand $nextTick

Reference 2: Vue source code — The implementation of nextTick