preface

NextTick is a common and useful method in Vue, so here’s a full breakdown.

First take a look at the description of the nextTick API on the official website.

Vue.nexttick ([callback, context]), which executes a deferred callback after the next DOM update loop. Use this method immediately after modifying the data to get the updated DOM.

What does it mean to end the DOM update loop, and when does the DOM update loop end? How does nextTick perform delayed callbacks after DOM updates are complete? Let’s start with the asynchronous update queue in Vue.

Vue asynchronously updates the queue

Vue asynchronously updates the queue, also known as asynchronous rendering. There’s a quote on the official website

In case you haven’t noticed, Vue executes 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. 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 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. For example, when you set vm.someData = ‘new value’, the component does not immediately re-render. When the queue is refreshed, the component is updated in the next event loop “TICK”. In most cases we don’t need to worry about this process, but if you want to do something based on the updated DOM state, 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 have to. 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.

Here involved knowledge points, one is the Event loop (Event loop), one is Vue Dom update mechanism.

Event loop

Event Loop, each round is a ‘tick’. A brief overview of event loops in browsers

  1. A macroTask executes only one task at a time from the queue and then executes the tasks in the microtask queue
  2. All tasks in the microtask queue are fetched and executed until the MicroTask queue is empty
  3. UI render, however, may not be executed, it is up to the browser to decide, but as long as UI render is executed, its nodes are executed immediately after all microTasks are executed and before the next MacroTask. (One event loop ends)
  4. Execute the next macro task
  5. .

The DOM is updated in the Vue by firing the setter, which in turn fires the Update method of the Watcher object. Instead of updating immediately, the update method is called queueWatcher to place the currently triggered Watcher object in queueWatcher’s observer queue. Execute it on the next tick. The source code is here.

Summarize the steps of Vue asynchronous rendering

Depending on data modification — triggering setters — Update method of the Watcher object — queueWatcher — puts methods that update the view into the nextTick callback.

Vue updates DOM by calling nextTick to achieve asynchronous rendering, so users can only get the updated DOM by calling nextTick. So why does nextTick still get the updated DOM after so many changes? This is because if the same watcher is triggered multiple times, it will only be pushed into the queue once. Look at queueWatcher in the source code:

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if(! flushing) { queue.push(watcher) }else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1.0, watcher)
    }
    // queue the flush
    if(! waiting) { waiting =true

      if(process.env.NODE_ENV ! = ='production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
Copy the code

The has object is used to determine whether the triggered watcher is already in the queue, so that the reactive data can be modified multiple times, and the view can be updated only once.

Take a look at this code on the official website.

var vm = new Vue({
  el: '#example'.data: {
    message: '123'
  }
})
vm.message = 'new message' // Change the data
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})
Copy the code

This code is as analyzed above.

Look at this code again

var vm = new Vue({
  el: '#example'.data: {
    message: '123'
  }
})
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // false
})
vm.message = 'new message' // Change the data
vm.$el.textContent === 'new message' // false
Copy the code

Because the assignment to Message comes after the nextTick method, the nextTick callback asynchronously updates the first part of the queue and updates the DOM later, so the DOM is not the updated one.

NextTick source code implementation

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

There are two main things you do in the next-tick source code.

The first is to determine whether the callback is a microtask or a macro task based on the current execution environment, in the following order:

Promise > MutationObserver > setImmediate > setTimeout

The second is to execute the task queue method.

Take a look at what the nextTick function does. First declare a _resolve and return a promise if there is no callback, so you can use await when using this.$nextTick. In the event of a callback being passed in, place the callback in the Callbacks queue, and execute the timer function, the asynchronous method judged above, on each first use of nextTick in the event loop. In this event loop, Each time the nextTick function is called, only the callback function is placed in the Callbacks queue. Finally, all methods on the task queue are executed through the flushCallbacks method.

In flushCallbacks, a copy of the task queue callbacks is copied to prevent the nextTick callback from executing in an infinite loop.

Here is the source code with comments:

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

export let isUsingMicroTask = false
// Task queue
const callbacks = []
// Indicates whether micro (macro) tasks are enabled for each round of the task queue
let pending = false
// Execute the task queue method
function flushCallbacks () {
  pending = false
  // The reason why we need to slice a copy is because some CB execution processes add content to the callbacks
  $nextTick = $nextTick
  // These should be implemented in the next nextTick,
  // So copy the current one and iterate through it to avoid endless execution
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

TimerFunc inserts the flushCallbacks into the end of the event loop, waiting to be called.
// Which method to call based on what method is supported in the current environment
let timerFunc

if (typeof Promise! = ='undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () = > {
    p.then(flushCallbacks)
  }
  isUsingMicroTask = true
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  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)) {
  timerFunc = () = > {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () = > {
    setTimeout(flushCallbacks, 0)}}// When using nextTick, put the pending function at the end of the queue for execution
export function nextTick (cb? :Function, ctx? :Object) {
  let _resolve
  // Push the callback function to the queue
  callbacks.push(() = > {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')}}else if (_resolve) {
      _resolve(ctx)
    }
  })
  // Execute the asynchronous delay function timerFunc(identified as pending, only executed on the first call of each event loop)
  if(! pending) { pending =true
    timerFunc()
  }
  // Return a promise-like call when nextTick passes no function arguments
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}
Copy the code

conclusion

The important point is that Vue also calls nextTick method to update DOM to achieve asynchronous rendering, and then the user calling nextTick will naturally be ranked behind nextTick task queue, and can get the updated DOM.