Usage Scenarios:

When we are developing a project, we often encounter situations where we need to do something with the new DOM after updating it with a VUE operation, but we do not get the new DOM. Since the DOM has not been re-rendered at this point, we use the vm.$nextTick method.

Usage:

NextTick takes a callback function as an argument, and its effect is to defer the callback until after the next DOM and a new cycle.

methods:{
example:function(){// modify data this.message='changed'// The dom is not yet up to date.$nextTick(function(){this.dosomething ()}) {this.dosomething ()}}Copy the code

Think about:

In usage, we find out what happens after the next DOM update cycle and when, so we need to understand what the DOM update cycle is. In Vue, Watcher is notified when the state of the view changes and triggers the rendering process of the virtual DOM, which is asynchronous rather than synchronous. There is a queue in Vue, and watcher is pushed to this queue every time it renders, and in the next event loop, Watcher triggers the rendering process.

Why does Vue use asynchronous update queues?

In simple terms, it is to improve performance, improve efficiency. As we know, Vue2.0 uses the virtual DOM for rendering. Notification of change detection is only sent to the component. Any change on the component is notified to a Watcher. If there are two data in the same event loop is changed, then the components of the watcher will receive a notification twice, and twice rendering (synchronous with the new rendering) twice, in fact we don’t need to apply colours to a drawing so many times, need only after all status changes, such as disposable DOM rendering the whole components to the latest.

How to solve an event loop component with multiple state changes requiring only one render update?

In fact, it is simple to queue the received Watcher instance and cache it, and check whether the queue already has the same Watcher before adding the queue. The Watcher instance is added to the queue only if it does not exist. Then in the next event loop, Vue tells the Watcher in the queue to trigger render and clear the queue. This ensures that a single event loop component requires only one render update for multiple state changes.

What is an event loop?

We know that JS is a single-threaded non-blocking scripting language, meaning that when js code is executed, there is only one main thread to handle all tasks. Non-blocking means that the main thread is pending when the code needs to process an asynchronous task. When the asynchronous task is finished, the main thread executes the callback according to certain rules. In fact, when the task completes, JS enlists the event to a queue (event queue). An event placed on the queue does not execute its callback immediately, but after all tasks in the current execution stack have executed, the main thread looks for any tasks in the event queue. There are two types of asynchronous tasks, microtasks and macro tasks. Different types of tasks are assigned to different task queues. After all the tasks in the stack are completed, the main thread will look for any tasks in the event queue, and if so, execute all the callbacks in the queue until empty. Then fetch an event from the macro task queue and add the corresponding callback to the current execution stack. After all tasks in the current execution stack are executed, check whether there are events in the micro task queue. This process is called an event loop.

Common microtasks

  • Promise.then
  • Object.observe
  • MutationObserver

Common macro tasks

  • setTimeout
  • setInterval
  • setImmediate
  • UI interaction events

When we get a fresh post-DOM using vm.$nextTick, be sure to register the callback with nextTick after changing the data.

methods:{
example:function(){// modify data this.message='changed'// The dom is not yet up to date.$nextTick(function(){this.dosomething ()}) {this.dosomething ()}}Copy the code

If you register the callback with nextTick first, then modify the data, execute the callback with nextTick first in the microtask queue, and then execute the callback with the new DOM, you don’t get the new DOM in the callback because it hasn’t been updated.

methods:{
example:function(){// The dom has not been updated yet.$nextTick(function(){this.dosomething ()}) {this.message= this.message'changed'}}Copy the code

We know that the task execution mechanism in adding a microtask queue is higher than the macro task execution mechanism (the following code must be understood)

methods:{
example:function(){// Try firstsetTimeout registers callbacks to macro taskssetTimeout(()=>{// Now the DOM is up to date, you can get the latest DOM}) // Then modify the data this.message='changed'}}Copy the code

SetTimeout is a macro task. Using setTimeout to register callbacks will be added to the macro task. The macro task is executed later than the micro task.

Now that you understand what nextTick does, let’s take a look at how it works

Analysis of implementation principle:

Because nextTick adds callbacks to the task queue to delay execution,Vue does not add callbacks to the task queue if nextTick is used repeatedly before the callback is executed, only one task is added. Vue has an internal list to store the callbacks provided in the nextTick parameter. When a task fires, it executes all of the callbacks in the list and clears the list as follows (simplified version) :

const callbacks=[]
let pending=false

function flushCallBacks(){
  pending=false
  const copies=callbacks.slice(0)
  callbacks.length=0
  for(leti=0; i<copies.length; i++){ copies[i]() } }let microTimeFun
const p=Promise.resolve()
microTimeFun=()=>{
  p.then(flushCallBacks)
}

export function nextTick(cb,ctx){
  callbacks.push(()=>{
    if(cb){
      cb.call(ctx)
    }
  })
  if(! pending){ pending=true
    microTimeFun()
  }
}
Copy the code

Understand relevant variables:

  • Callbacks: the callback function used to store the user registration (what happens to the DOM after getting the update)
  • Pending: indicates whether to add tasks to the task queue. If pending is false, it indicates that there is no nextTIck task in the task queue and it needs to be added. When adding a nextTIck task, Pending is true. Tasks are not repeatedly added to the task queue while nextTick is still pending before the callback is executed. When the callback function starts executing, pending is flase, and a new event loop is executed.
  • FlushCallbacks: flushCallbacks are tasks registered in the task queue. When this function executes, all the callbacks are executed in sequence, and then the callbacks are emptied and the pending is reset to false. FlushCallbacks are executed only once.
  • MicroTimerFunc: It adds flushCallbacks to the microtask queue using Promise. Then.

The following figure shows the internal registration process and execution process of nextTick.

this.$nextTick().then(function(){//dom is new})Copy the code

To do this, simply determine in nextTIck that if no callback is provided and promises are currently supported, return a Promise, and add a function to the callbacks that, when executed, executes the Promise’s resolve, as shown below

  function nextTick (cb, ctx) {
    var _resolve;
    callbacks.push(function () {
     if (cb) {
        cb.call(ctx);
      } else if(_resolve) { _resolve(ctx); }});if(! pending) { pending =true;
      timerFunc();
    }
    if(! cb && typeof Promise ! = ='undefined') {
      return new Promise(function(resolve) { _resolve = resolve; }}})Copy the code

NextTick source view

At this point, the nextTick principle is basically done. Now we can look at the source code of nextTick in the real VUE, probably we can understand it, the source code is as follows.

var timerFunc;

  // 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 next, $flow-disable-line */
  if(typeof Promise ! = ='undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = 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); }}; isUsingMicroTask =true;
  } else if(! isIE && typeof MutationObserver ! = ='undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    // Use MutationObserver wherePhantomJS, iOS7, Android 4.4 // (#6466 MutationObserver is unreliable in IE11)
    var counter = 1;
    var observer = new MutationObserver(flushCallbacks);
    var textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
      characterData: true
    });
    timerFunc = function () {
      counter = (counter + 1) % 2;
      textNode.data = String(counter);
    };
    isUsingMicroTask = true;
  } else if (typeof setImmediate ! = ='undefined' && isNative(setImmediate)) {
    // Fallback to setImmediate.
    // Technically it leverages the (macro) task queue,
    // but it is still a better choice than setTimeout.
    timerFunc = function () {
      setImmediate(flushCallbacks);
    };
  } else {
    // Fallback to setTimeout.
    timerFunc = function () {
      setTimeout(flushCallbacks, 0);
    };
  }

  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); }});if(! pending) { pending =true; timerFunc(); } / /$flow-disable-line
    if(! cb && typeof Promise ! = ='undefined') {
      return new Promise(function(resolve) { _resolve = resolve; }}})Copy the code

conclusion

This article took about two days to write, fully referring to the book “Simple vue. Js”, fully understanding every sentence in the book about VM.$nextTick, but also have a further understanding of js event cycle, js operation mechanism is further deepened. As a front end, I don’t want to be limited to calling various apis, but to know its principle and make progress every day. I hope you can discuss with me more.