The ancients said: know what you are, know why you are

preface

Most developers will first encounter $nextTick() in vue as a setTimeout() call. Is this understanding correct?

1. Pre-knowledge

This method is not difficult to understand, but the following concepts need to be known:

  • The call stack
  • Task queue
  • Event loop

So let’s assume that these concepts are pretty clear to you.

$nextTick()

1. The concept

Master the principle behind a knowledge point, we must be very familiar with its use. Here’s the official description.

vm.$nextTick([callback])

  • {function} [callback]
  • Usage: Defer the callback until next timeDOMAfter the update loop is executed. Use the data immediately after you modify it, and then waitDOMThe update. The this of the callback is automatically bound to the instance calling it.

new Vue({
  // ...
  methods: {
    // ...
    example: function () {
// Modify the data this.message = 'changed' // DOM has not been updated yet this.$nextTick(function () { // DOM is now updated// 'this' binds to the current instance this.doSomethingElse()  })  }  } }) Copy the code

The sentence above says “defer the callback until after the next DOM update loop”. This means:

Vue is executed asynchronously when updating the DOM. As long as it listens for data changes, Vue opens a queue and buffers all data changes that occur in the same event loop. If the same watcher is triggered more than once, it will only be pushed into the queue once. Then, in the next event loop, Vue flushs the queue and performs the actual work.

For example, when vm.someData = ‘new value’ is set, the component does not immediately rerender. When the queue is refreshed, the component is updated in the next event loop. But if you want to do something based on the updated DOM state, that’s a little tricky. So Vue pushes $nextTick(), which receives a callback that will be called when the DOM update is complete.

The Vue documentation also says:

Vue internally attempts to use native Promise.then, MutationObserver, and setImmediate for asynchronous queues, and setTimeout(fn, 0) instead if the execution environment does not support it.

So it’s not wrong to treat $nextTick() as a setTimeout() call. It’s just not as accurate.

2. Source code interpretation

$nextTick () is in/SRC/core/instance/render js defined in:

export function renderMixin (Vue: Class<Component>) {
.  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }
.} Copy the code

The $nextTick() method is mounted to the Vue prototype in the renderMixin function. You can see that $nextTick() is a simple wrapper around the nextTick function.

The nextTick function is defined in/SRC /core/util/next-tick.js. The body of the next-tick.js file is a 4-layer if else statement.

if () {
.} else if () {
.} else if () {
.} else { .} Copy the code

The first layer

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

First, determine whether the current environment supports promises, and if it does, use promises first.

As we all know, the task queue is not only a queue, generally can be divided into microtasks -microtask and macrotask -macrotask. When the call stack is idle, the event loop reads a task in the macro task message queue and executes it. During the execution of macro tasks, sometimes multiple microtasks are generated and stored in the microtask queue. That is, each macro task is associated with a microtask queue. After the main function completes, but before the current macro task completes, the event loop executes and empties the current microtask queue.

The other two macro tasks may have UI re-renders in between, so you just need to update all the data that needs to be updated before all the UI re-renders in the microtask, so that you can get the latest DOM with only one re-render.

For VUE, which is a data-driven framework, the ability to update all data states before the UI is rerendered is a big help in performance, so use microtasks to update data states in preference to macro tasks, which is why promise is preferred over setTimeout.

First define the constant p, whose value is an immediate Resolve Promise instance object.

We then define the variable timerFunc as a function whose execution will register the flushCallbacks function as a microtask.

Then,

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

The comment says that in some UIWebViews microtasks are not refreshed, the solution is to have the browser do something else, such as register a macro task, even if the macro task does nothing, so that it can touch the refresh of the micro-task.

The second floor

If the current environment does not support promises, go to the second else if statement.

else if(! isIE && typeof MutationObserver ! = ='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)  } } Copy the code

Determine whether the current environment supports MutationObserver.

MutationObserver is also a microtask that provides the ability to monitor changes made to the DOM tree. It is designed as a replacement for the older Mutation Events feature, which is part of the DOM3 Events specification.

We first pass the flushCallbacks into the MutationObserver constructor, which creates and returns a new MutationObserver that will be called whenever the specified DOM changes.

const observer = new MutationObserver(flushCallbacks)
Copy the code

We then create a text DOM node based on the counter variable and configure MutationObserver to subscribe to the DOM node, so that when the DOM node changes, the flushCallbacks are registered in the microtask queue.

let counter = 1
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
    characterData: true
})
Copy the code

Finally, register timeFun as a function, and when timeFun executes, immediately change the value of counter, causing a MutationObserver change, and register flushCallbacks in the microtask queue.

timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
}
Copy the code

The third layer

Then read the third else if statement

else if(typeof setImmediate ! = ='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)  } } Copy the code

The setImmediate function takes precedence over the setTimeout function. This is because setImmediate has better performance than setTimeout.

SetImmediate does not require setTimeout to repeatedly timeout a callback function before registering it in the macro task queue. But setImmediate has a significant flaw, and only IE implements it.

The fourth floor

And then finally the fourth else statement, setTimeout comes in.

else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
} Copy the code

3. Non-browser environment

Digging through the source code, it turned out that there was more than one definition of the nextTick function. It is also defined in Packages/Weex-VUE-framework /factory.js.

Weex runs in a Node environment. Weex runs in a Node environment.

Let’s also see how this nextTick definition differs from the one above.

The main body is around mircoTask and MarcoTask, that is, defining macro and micro tasks respectively.

var macroTimerFunc;
if () {
    macroTimerFunc = function() {... }} else if () {
    macroTimerFunc = function() {... }} else{... } var microTimerFunc; if () {  microTimerFunc = function() {... }} else {  microTimerFunc = macroTimerFunc } Copy the code

You can see that macroTimerFunc has three layers of if judgments and microTimerFunc has two layers.

macroTimerFunc

Look at the macroTimerFunc

if(typeof setImmediate ! = ='undefined' && isNative(setImmediate)) {
  macroTimerFunc = function () {
    setImmediate(flushCallbacks);
  };
}
Copy the code

SetImmediate determines whether the environment supports setImmediate first, and the last else uses setTimeout.

else {
  /* istanbul ignore next */
  macroTimerFunc = function () {
    setTimeout(flushCallbacks, 0);
  };
} Copy the code

MessageChannel

The else if in the middle is to determine whether a MessageChannel is supported

else if(typeof MessageChannel ! = ='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);  }; } Copy the code

If you know Web Workers, the internal implementation of Web Workers uses MessageChannel. A MessageChannel instance object has two properties, port1 and port2. Simply have Port1 listen for the onMessage event and then use Port2’s postMessage to send a message to Port1. The onMessage callback of Port1 is then registered as a macro task, which also performs better than setTimeout because it does not require any detection work.

The macroTimerFunc function registers flushCallbacks as a macro task.

microTimerFunc

The microTimerFunc function is used to register flushCallbacks as microtasks.

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

If Promise is not supported, then microTimerFunc = macroTimerFunc; .

4. nextTick

Finally, take a real look at the body of the nextTick function

export functionnextTick (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)  }  }) / / ignore} Copy the code

NextTick adds a new function to the Callbacks array defined in the header of the file: const callback = []. Note that instead of adding the CB callback function directly to the Callbacks array, a new function wraps the callback function and adds the new function to the Callbacks array. But the function execution added to the callbacks array indirectly calls the CD callback, and you can see cb using the. Call method that sets the scope of cb to CTX, which is the second argument to nextTick. In the case of the $nextTick method, the scope of the callback passed to the $nextTick method is the current component instance object, as long as the callback cannot be an arrow function. In normal use, it is ok to use the arrow function, as long as the purpose is achieved.

Keep looking at the source code

export functionnextTick (cb? : Function, ctx? : Object) {.  if(! pending) {    pending = true
    timerFunc()
 } .} Copy the code

The pending variable is defined in the header of the file: let Pending = false, which is an identifier that indicates whether the callback queue is waiting for a refresh. An initial value of false indicates that the callback queue is empty and does not need to be refreshed. If the $nextTick method is called somewhere at this time, the code in the if block will be executed. In the if block, the value of the variable pending will be set to true, indicating that the callback queue is not empty and is waiting for a refresh. Since you are waiting to refresh, you must of course refresh the callback queue. This is where the previous timerFunc function comes in. In week it is micTimeFunc or marcoTaskFunc. Regardless of the task type, they will wait for the call stack to clear before executing.

For example:

created () {
    this.$nextTick( () => {console.log(1)})
    this.$nextTick( () => {console.log(2)})
    this.$nextTick( () => {console.log(3)})
}
Copy the code

Call the $nextTick method three times in a row in the Created hook function. The timerFunc function will register the flushCallbacks as a microtask only when the $nextTick method is called for the first time. The flushCallbacks function will not execute at that time. Because it waits until the next two calls to the $nextTick method have been executed, or, more precisely, until the call stack has been emptied. That is, when the flushCallbacks function is executed, the Callbacks callback queue will contain all callbacks that were collected in the event loop and registered with the $nextTick method. The next task is to execute and flush all callbacks in the flushCallbacks function.

The following isflushCallbacksThe source of

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

Set the pending variable to false, and then execute the callbacks. Note that the callbacks in the callbacks queue are not iterated through the callbacks array. Instead, copies of the callbacks are stored using copies of the callbacks constant. The copies array is then iterated through and the Callbacks array is emptied before iterating through the copies values: callbacks.length = 0.

Why do you do that? “For example”

created () {
    this.age = 20
    this.$nextTick(() = > {        this.age = 21
        this.$nextTick(() => { console.log('Second nextTick')})
 }) } Copy the code

The outer $nextTick callback should not be executed in the same microtask as the inner $nextTick callback, but in two different microtasks, although the result might not be different. But it should be done from a design perspective.

Vue updates the DOM asynchronously, adding the flushSchedulerQueue to the Callbacks array. Vue updates the DOM asynchronously, adding the flushSchedulerQueue to the Callbacks array

callbacks = [
    flushSchedulerQueue
]
Copy the code

Also register the flushCallbacks function as a microtask, so the microtask queue is

// Microtask queue[
    flushCallbacks
]
Copy the code

Then call the first $nextTick method. $nextTick adds the callback function to the callbacks array, which looks like this:

callbacks = [
    flushSchedulerQueue,
() = > {        this.age = 21
        this.$nextTick(() => {console.log('Second $nextTick')})
 } ] Copy the code

FlushCallbacks will execute the functions in the callbacks array in sequence, starting with the flushSchedulerQueue function. This function will iterate over all the observers in the queue and reevaluate. Then execute the following function:

() = > {    this.age = 21
    this.$nextTick(() => {console.log('Second $nextTick')})
}
Copy the code

This function is the first $nextTick callback, and because the reevaluation is done before the callback is executed, the code in this callback is able to access the updated value. The nextTick function will also be called to add the flushSchedulerQueue to the Callbacks array after the age property is changed again. However, the pending value of the flushCallbacks function is set to false before the flushCallbacks function is executed. So the nextTick function registers the flushCallbacks function as a new microtask. That’s it. The queue contains two microtasks.

// This is the microtask queue[
FlushCallbacks,    flushCallbacks
]
Copy the code

The flow of the second flushCallbacks function is identical to that of the first flushCallbacks.

That concludes $nextTick().

At the end

For more articles, please go to Github, if you like, please click star, which is also a kind of encouragement to the author.