NextTick in nextTick Vue involves the asynchronous updating of the DOM in Vue. Among them, the source code of nextTick involves a lot of knowledge. NextTick is a core implementation of Vue. Before introducing The nextTick of Vue, in order to facilitate everyone’s understanding, I first briefly introduce the running mechanism of JS.

JS runtime mechanism

JS execution is single threaded and is based on event loops. The event cycle can be roughly divided into the following steps:

(1) All synchronization tasks are executed on the main thread, forming an execution context stack.

(2) In addition to the main thread, there is a task queue. Whenever an asynchronous task has a result, an event is placed in the “task queue”.

(3) Once all synchronization tasks in the “execution stack” are completed, the system reads the “task queue” to see what events are in it. Those corresponding asynchronous tasks then end the wait state, enter the execution stack, and start executing.

(4) The main thread repeats step 3 above.

The execution of the main thread is a tick, and all asynchronous results are scheduled through the “task queue”. Message queues hold individual tasks. According to the specification, tasks fall into two categories, macro task and Micro Task. After each Macro task is finished, all micro tasks should be cleared.

Macro Task and Micro Task: Macro Task and Micro Task

for (macroTask of macroTaskQueue) {
    // 1. Handle current MACRO-TASK
    handleMacroTask();
      
    // 2. Handle all MICRO-TASK
    for (microTask ofmicroTaskQueue) { handleMicroTask(microTask); }}Copy the code

In the browser environment, common Macro tasks include setTimeout, MessageChannel, postMessage, and setImmediate. Common micro Tasks are MutationObsever and Promise.then.

Vue implementation (Vue source 2.5+)

After Vue source 2.5+, nextTick’s implementation has a separate JS file to maintain it, and it’s not much source code, just over 100 lines in all. SRC /core/util/next-tick.js: SRC /core/util/next-tick.js

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

Next-tick. js declares two variables microTimerFunc and macroTimerFunc, which correspond to micro task functions and Macro task functions respectively. With macro Task implementations, native setImmediate is a feature supported only by advanced VERSIONS of IE and Edge, and native MessageChannel is tested first. If it is not supported, it is degraded to setTimeout 0; For micro Task implementations, we check if the browser natively supports Promise, and point directly to macro Task implementations if it doesn’t.

NextTick (flushSchedulerQueue) ¶ nextTick(flushSchedulerQueue) ¶ nextTick(flushSchedulerQueue) ¶ nextTick(flushSchedulerQueue) ¶ The logic is very simple. It presses the cb callback into the callbacks array and finally executes macroTimerFunc or microTimerFunc according to the useMacroTask condition. They all execute flushCallbacks on the next tick. The logic for flushCallbacks is very simple: iterate through the callbacks and execute the corresponding callback function.

The reason for using callbacks instead of executing callbacks directly on nextTick is to ensure that executing nextTick multiple times does not start multiple asynchronous tasks, but instead pushes them into a single synchronous task that will be executed on the nextTick.

The nextTick function ends with a bit of logic:

 if(! cb &&typeof Promise! = ='undefined') {
  return new Promise(resolve= > {
    _resolve = resolve
  })
}
Copy the code

This provides a promise-like call when nextTick does not pass cb arguments, such as:

nextTick().then((a)= > {})
Copy the code

When the _resolve function executes, it jumps to the logic of then.

Next-tick. js also exposes the withMacroTask function, which wraps the function to ensure arbitrary data modification during function execution, triggering changes and forcing macroTimerFunc to be removed when nextTick is executed. For example, for some DOM interaction events, such as v-ON-bound event callback functions, macro Task will be forced.

Vue.js provides two ways to call nextTick: the global API vue.nexttick and the instance method vm.$nextTick. No matter which one we use, You end up calling the nextTick method implemented in next-tick.js.

Take a look at the source code before VUe2.5

/** * Defer a task to execute it asynchronously. */
export const nextTick = (function () {
  const callbacks = []
  let pending = false
  let timerFunc

  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 */
  if (typeof Promise! = ='undefined' && isNative(Promise)) {
    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)
    }
  } 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
      textNode.data = String(counter)
    }
  } else {
    // fallback to setTimeout
    /* istanbul ignore next */
    timerFunc = (a)= > {
      setTimeout(nextTickHandler, 0)}}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)
      }
    })
    if(! pending) { pending =true
      timerFunc()
    }
    if(! cb &&typeof Promise! = ='undefined') {
      return new Promise((resolve, reject) = > {
        _resolve = resolve
      })
    }
  }
})()

Copy the code

First, take a look at the three important variables defined in nextTick.

callbacks

Store all callbacks that need to be executed

pending

Used to indicate whether a callback function is being executed

timerFunc

Next, take a look at the nextTickHandler() function.

function nextTickHandler () {
    pending = false
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
}
Copy the code

This function is used to execute all the callbacks stored in the Callbacks. Next, assign the trigger mode to timerFunc.

Determine if promise is supported natively, and if so, use the promise to trigger the execution of the callback function. Otherwise, if MutationObserver is supported, an observer object is instantiated and all callbacks are triggered when the text node changes. If neither is supported, setTimeout is used to set the delay to 0.

Finally, the queueNextTick function. Since nextTick is an immediate function, queueNextTick is the function that returns, accepts arguments passed in by the user, and stores the callback function in the callbacks.

Above is the whole process. The key is timeFunc(), which delays execution. From the above, you can see that there are three implementations of timeFunc().

Promise MutationObserver setTimeout

Promise and setTimeout are well understood as asynchronous tasks that call back specific functions after synchronization and asynchronous tasks that update the DOM. Here is a brief introduction to MutationObserver. MutationObserver is a new API in HTML5 that monitors DOM changes. It can listen for child node deletions, property changes, text content changes, and so on on a DOM object. The call is simple, but a little unusual: you need to bind the callback first:

var mo = new MutationObserver(callback)
Copy the code

Copy code gets a MutationObserver instance by passing a callback to the constructor of MutationObserver, which is triggered when the MutationObserver instance listens for changes. At this point you are just binding the callback to the MutationObserver instance. It is not set which DOM it listens for, node deletion, or property modification. This is accomplished by calling his Observer method:

varDomTarget = the DOM node you want to listen on mo.observe(domTarget, {characterData: true // Listen for text content changes.
})
Copy the code

The role of MutationObserver in nextTick is shown in the figure above. After listening for DOM updates, the callback function is called. The reason for using MutationObserver was that nextTick wanted an asynchronous API to execute the asynchronous callbacks I wanted to execute, including Promise and setTimeout, after the current synchronous code had finished executing. It also involves microtask in depth.

conclusion

$nextTick was using setImmediate > MessageChannel > setTimeout

$nextTick was using Promise > MutationObserver > setTimeout

Through the analysis of nextTick in this section, combined with setter analysis in the previous section, we learned that the change of data to DOM re-rendering is an asynchronous process that occurs in the nextTick. This is what happens during development, such as when we get data from the server interface, and the data is modified. If some of our methods rely on DOM changes after the data modification, we have to execute it after nextTick. For example, the following pseudocode:

getData(res).then((a)= >{
  this.xxx = res.data
  this.$nextTick((a)= > {
    // Here we can get the changed DOM})})Copy the code

reference

Vue. Js technology revealed

Briefly understand nextTick in Vue