NextTick in Vue involves the asynchronous updating of the DOM in Vue. Among them, the source code of nextTick involves a lot of knowledge, many do not understand, according to some of their own feelings introduced nextTick.

A, sample

Let’s start with an example of DOM updates in Vue and what nextTick can do.

The template

<div class="app">
  <div ref="msgDiv">{{msg}}</div>
  <div v-if="msg1">Message got outside $nextTick: {{msg1}}</div>
  <div v-if="msg2">Message got inside $nextTick: {{msg2}}</div>
  <div v-if="msg3">Message got outside $nextTick: {{msg3}}</div>
  <button @click="changeMsg">
    Change the Message
  </button>
</div>
Copy the code

Vue instance

new Vue({
  el: '.app',
  data: {
    msg: 'Hello Vue.',
    msg1: ' ',
    msg2: ' ',
    msg3: ' '
  },
  methods: {
    changeMsg() {
      this.msg = "Hello world."
      this.msg1 = this.$refs.msgDiv.innerHTML
      this.$nextTick(() => {
        this.msg2 = this.$refs.msgDiv.innerHTML
      })
      this.msg3 = this.$refs.msgDiv.innerHTML
    }
  }
})
Copy the code

Click on the former

After clicking on

As you can see from the figure, msG1 and MSG3 still display the content before the transformation, while MSG2 shows the content after the transformation. The root cause is that DOM updates in Vue are asynchronous (more on that later).

2. Application scenarios

The following describes the main application scenarios and causes of nextTick.

  • In the Vue life cyclecreated()DOM operations performed by hook functions must be placed inVue.nextTick()In the callback function of

The DOM doesn’t actually render at the time the Created () hook function executes, and DOM manipulation is useless, so make sure you put the DOM manipulation js code in the view.nexttick () callback. The mounted() hook function is the mounted() hook function, because it executes when all DOM operations are completed.

  • Any operation to be performed after the data changes that requires the use of a DOM structure that changes with the dataVue.nextTick()In the callback function of.

The specific reasons are explained in detail in the official documents of Vue:

Vue performs DOM updates asynchronously. Whenever a data change is observed, 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. This removal of duplicate data while buffering is important to avoid unnecessary computation and DOM manipulation. Then, in the next event loop, “TICK,” Vue refreshes the queue and performs the actual (de-duplicated) work. Vue internally tries to use native promise.then and MessageChannel for asynchronous queues, or setTimeout(fn, 0) instead if the execution environment does not support it.

For example, when you set vm.someData = ‘new value’, the component does not immediately re-render. When the queue is refreshed, the component updates with the next “tick” when the event loop queue is empty. In most cases we don’t need to worry about this process, but if you want to do something after a DOM status update, it can be tricky. While vue.js generally encourages developers to think in a “data-driven” way and avoid direct contact with the DOM, sometimes we do. To wait for Vue to finish updating the DOM after the data changes, use vue.nexttick (callback) immediately after the data changes. This callback will be called after the DOM update is complete.

Three,nextTickSource analyses

role

Vue.nexttick is used to delay the execution of a piece of code. It takes two parameters (the callback function and the context in which the callback was executed) and returns a Promise object if no callback function is provided.

The source code

/**
 * 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 inIOS >= 9.3.3 when triggeredin 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 = () => {
      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 && typeof MutationObserver ! = ='undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    // use MutationObserver where native Promise is not available,
    // e.g. 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 = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else {
    // fallback to setTimeout
    /* istanbul ignore next */
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  return functionqueueNextTick (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()
    }
    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

Used to trigger the execution of the callback function

Next, 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.

timeFunc()

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

You get a MutationObserver instance by passing a callback to the constructor of the 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:

Var domTarget = mo.observe(domTarget, {characterData:true// Listen for text content changes. })Copy the code

nextTick
MutationObserver

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 and other contents in depth, so I will not introduce it in depth if I do not understand it temporarily.