A ue. NextTick DEMO triggered by learning (blood) case

In the Demo code

<div id="example">
    <div ref="test">{{test}}</div>
    <button @click="handleClick">click</button>
</div>
Copy the code
var vm = new Vue({
    el: '#example'.data: {
        test: 'begin',},methods: {
        handleClick() {
            this.test = 'end';
            console.log('1')
            setTimeout((a)= > { // macroTask
                console.log('3')},0);
            Promise.resolve().then(function() { //microTask
                console.log('promise! ')})this.$nextTick(function () {
                console.log('2')})}})Copy the code

What happens when you click the button on the console?

This code executes in order, some say, 1, 2, promise, 3. Others say 1, promise, 2, 3.

What is it??

The above demo involves two pieces of knowledge: 1. Js event loop mechanism 2. NextTick implementation mechanism in Vue JS event loop mechanism is the basis of this article we discuss today, but not the key point.

Developer.mozilla.org/zh-CN/docs/…

www.ruanyifeng.com/blog/2014/1…

zhuanlan.zhihu.com/p/33058983

When do I need vue.nexttick?

Go to the official documentation API

Vue.nexttick ([callback, context]) : {Function} [callback]
{Object} [context] usage: Executes a deferred callback after the next DOM update loop. Use this method immediately after modifying the data to get the updated DOM.// Modify the data
vm.msg = 'Hello'
// DOM has not been updated yet
Vue.nextTick(function () {
  // DOM is updated
})

// Use as a Promise (new since 2.1.0, see hints below)
Vue.nextTick()
  .then(function () {
    // DOM is updated
  })
Copy the code

oh my god! When does the next DOM update loop end?

The official vUE document has a magical feature, when you solve a problem, go back to the original document to find, always can find. But if you don’t find a solution, you can’t expect to find it in documentation. This is the translation of “having everything and finding nothing.” To a certain extent.

Without further ado: Look at the asynchronous update queue

Ok so that’s it! Vue implements responsiveness not by changing the DOM immediately after the data changes, but by updating the DOM according to a certain strategy (see the Deep responsiveness principle). You can use Vue.nexttick (callback) to do something DOM related after the data changes and wait for Vue to finish updating the DOM. This callback will be called after the DOM update is complete. In other words: Vue.nexttick () is used when an operation is to be performed after the data changes and the operation needs to use a DOM structure that changes with the data.

What does Vue.Nexttick actually do

Let’s take a look at the implementation of nextTick in Vue2.4.4.

/** * Defer a task to execute it asynchronously. */
export const nextTick = (function () {
/* We declare three variables, callbacks to store all callbacks that need to be executed, pending to indicate whether the callback is being executed, and timerFunc to trigger the execution of the callback. * /
  const callbacks = []
  let pending = false
  let timerFunc

// Declare the nextTickHandler function, which executes all callbacks stored in callbacks.
  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 */
  
  // Whether promises are natively supported, if they are, then use the promise to trigger the execution of the callback function;
  if (typeof Promise! = ='undefined' && isNative(Promise)) {
  The resolve Now Promise object is at the end of this event loop, not at the beginning of the next.
    var p = Promise.resolve()
    var logError = err= > { console.error(err) }
    timerFunc = (a)= > {
      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)
    }
    // Instantiate an observer object if MutationObserver is supported,
    // All callbacks are triggered when the text node changes.
    // Each time timerFunc is called, the text node is reassigned
  } 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
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = (a)= > {
      counter = (counter + 1) % 2
      // Bind the DOM with MutationObserver and specify a callback function,
      // A callback is triggered when the DOM changes, and the callback goes to the main thread (before the task queue).
      textNode.data = String(counter)
    }
  } else {
  // If neither is supported, use setTimeout to set the delay to 0
    // fallback to setTimeout
    /* istanbul ignore next */
    timerFunc = (a)= > {
      setTimeout(nextTickHandler, 0)}}QueueNextTick returns a queueNextTick function that stores callback functions in callbacks. Both callback functions and promises can be supported.
  return function queueNextTick (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)
      }
    })
    // Determine if the callback is not executing, call timerFunc to trigger the callback to execute the code passed in by the user.
    if(! pending) { pending =true
      timerFunc()
    }
    if(! cb &&typeof Promise! = ='undefined') {
      return new Promise((resolve, reject) = > {
        _resolve = resolve
      })
    }
  }
})()
Copy the code

So when vue2.4.4 executes, it’s 1, 2, promise, 3. Why does 2 refer to the event loop before promise

 if (typeof Promise! = ='undefined' && isNative(Promise)) {
  The resolve Now Promise object is at the end of this event loop, not at the beginning of the next.
    var p = Promise.resolve()
    var logError = err= > { console.error(err) }
    timerFunc = (a)= > {
      p.then(nextTickHandler).catch(logError)
         if (isIOS) setTimeout(noop)
    }
Copy the code

There is already a promise object in the resolve state at initialization. Executing nextTick currently executes the callback directly in this event loop

As you may have noticed, I highlighted the vue version number above, so what does that have to do with the version? This is why there are two orders of execution.

NextTick in VUe2.5 + (2.5.21 is used as an example)

What are the changes from the previous version?

1. Starting with Vue 2.5+, a single file, next-tick.js, was pulled out to execute it.

2. MicroTask and macroTask

There’s this comment in the source code, and these variables

  // 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).
  var microTimerFunc;
  var macroTimerFunc;
  var useMacroTask = false;
Copy the code

In versions prior to Vue 2.4, nextTick was almost always implemented based on microTask, but due to the high execution priority of microTask, it could even bubble up faster than events in some scenarios, causing some weird problems. However, if you change it all to macroTask, it will also have a performance impact on some scenes that are redrawn and animated. As a result, nextTick’s policy is to default to microTask and force macroTask out of some DOM interaction events, such as the v-ON-bound event callback handler.

By default, when Vue executes a bound DOM event, it wraps the withMacroTask method of the callback handler function to ensure that the data state changes during the execution of the callback function. The task of updating the view (DOM) as a result of these changes is pushed to macroTask. Let’s look at two functions

  function add$1 (event, handler, capture, passive) {
    handler = withMacroTask(handler);
    target$1.addEventListener(
      event,
      handler,
      supportsPassive
        ? { capture: capture, passive: passive }
        : capture
    );
  }
  
/** * Wrap a function so that if any code inside triggers state change, * The changes are queued using a (macro) task instead of a microtask
  function withMacroTask (fn) {
    return fn._withTask || (fn._withTask = function () {
      useMacroTask = true;
      try {
        return fn.apply(null.arguments)}finally {
        useMacroTask = false; }})}Copy the code

The onclick callback is actually triggered by apply within this function, useMacroTask = true, that’s the key thing

  function nextTick (cb, ctx) {
    var _resolve;
    callbacks.push(function () {
      if (cb) {
        try {
          cb.call(ctx);
        } catch (e) {
          handleError(e, ctx, 'nextTick'); }}else if(_resolve) { _resolve(ctx); }});// flag the bit to ensure that the following code will not be executed again if operations such as this.$nextTick occur later
    if(! pending) { pending =true;
      // Use microtask or macro task. In this example, the Vue has chosen macro task so far
      // All data changes directly generated by v-ON binding events are macro tasks
      // Since our bound callbacks are wrapped with withMacroTask, withMacroTask sets useMacroTask to true
      if (useMacroTask) {
        macroTimerFunc();
      } else{ microTimerFunc(); }}// $flow-disable-line
    if(! cb &&typeof Promise! = ='undefined') {
      return new Promise(function (resolve) { _resolve = resolve; }}})Copy the code

MacroTimerFunc () forces the (macro)task to be removed if useMacroTask == true

So what does version 2.5+ do when it’s not mandatory?

  if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
    macroTimerFunc = function () {
      setImmediate(flushCallbacks);
    };
  } else if (typeofMessageChannel ! = ='undefined' && (
      isNative(MessageChannel) ||
      // PhantomJS
      MessageChannel.toString() === '[object MessageChannelConstructor]'
    )) {
    var channel = new MessageChannel();
    var port = channel.port2;
    channel.port1.onmessage = flushCallbacks;
    macroTimerFunc = function () {
      port.postMessage(1);
    };
  } else {
    /* istanbul ignore next */
    macroTimerFunc = function () {
      setTimeout(flushCallbacks, 0);
    };
  }

  // Determine microtask defer implementation.
  /* istanbul ignore next, $flow-disable-line */
  if (typeof Promise! = ='undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    microTimerFunc = function () {
      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;
  }
Copy the code

For macroTask execution, Vue preferentially detects whether native setImmediate (supported by higher versions of IE and Edge) is supported, and then whether native MessageChannel is supported. If not, setTimeout(fn, 0) is used. Use Promise again for microTask execution.

Scrapping MutationObserver and using MessageChannel is also one of the changes introduced since 2.5

const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = (a)= > {
port.postMessage(1)}Copy the code

Create a MessageChannel object that detects the message through port1, which sends the message. The onMessage event of Port1 is triggered by port2’s active postMessage, and the callback function flushCallbacks is included in the event loop as a macroTask. You can see that the source code uses MessageChannel in preference to setTimeout. Why should MessageChannel create macroTask in preference to setTimeout? HTML5 stipulates that the minimum time delay of setTimeout is 4ms, which means that the asynchronous callback can be triggered at the earliest 4ms in an ideal environment.

Vue uses so many functions to simulate asynchronous tasks for the sole purpose of making callbacks asynchronous and called as soon as possible. The delay of MessageChannel is smaller than setTimeout.

conclusion

So back to the original question, the implementation of vue2.5+ is 1, promise, 2, 3; Vue2.4.4 is implemented with 1, 2, promise, 3

That’s the end of the article

So when you encounter this kind of problem, the best way is to look at the official documentation, and look at the source code, do not memorize concepts and order, because the standards will change.

You are welcome to point out any shortcomings.