Analysis of nexttick principle

Time:2022-1-14

What is nexttick

$ Nexttick: according to the explanation of the official document, it can execute a callback function after the DOM is updated and return a promise (if supported)

//Modify data
vm.msg = "Hello";

//The DOM has not been updated
Vue.nextTick(function() {
  //DOM updated
});

You should understand the EventLoop at first glance. In fact, you should start updating the DOM at the beginning of the next event loop to avoid frequent operations in the middle causing page redrawing and backflow.

This document refers to official documents:

Maybe you haven’t noticed that Vue updates the domAsynchronous executionof Whenever a data change is heard, Vue will open a queue and buffer all data changes that occur in the same event loop. If the same watcher is triggered multiple times, it will only be pushed into the queue once.
This de duplication in buffering is important to avoid unnecessary calculations and DOM operations. Then, in the next event loop “tick”, Vue refreshes the queue and performs the actual (de duplicated) work. Vue internally attempts to use native for asynchronous queuesPromise.thenMutationObserverandsetImmediate, if the execution environment does not support it, thesetTimeout(fn, 0)Replace.

Column if setvm.text = 'new value'The component will not be re rendered immediately. When the queue is refreshed, the component will be updated in the next event loop ‘tick’,

<div id="example">{{message}}</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm. Message = 'new message' // change data
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})

It’s usually set inthis.xx='xx'It is only used when the latest DOM data is obtained immediately after the data$nextTick, because the DOM update is done asynchronously, this method is needed for obtaining.

Update process (source code analysis)

  1. When the data is modified, the Watcher will listen to the changes and queue the changes:
/*
 * Subscriber interface.
 * Will be called when a dependency changes.
 */
Watcher.prototype.update = function update() {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};
  1. And add a flushschedulequeue callback using the nexttick method
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
function queueWatcher(watcher) {
  var 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.
      var 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 (!config.async) {
        flushSchedulerQueue();
        return;
      }
      nextTick(flushSchedulerQueue);
    }
  }
}
  1. Flushschedulequeue is added to the callback array and executed asynchronously
function nextTick(cb, ctx) {
  var _resolve;
  callbacks.push(function() {
    if (cb) {
      try {
        cb. call(ctx); // !!  CB is the callback added
      } catch (e) {
        handleError(e, ctx, "nextTick");
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  if (!pending) {
    //See timerfunc for asynchronous operation
    pending = true;
    timerFunc();
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== "undefined") {
    return new Promise(function(resolve) {
      _resolve = resolve;
    });
  }
}
  1. The timerfunc operation is executed asynchronously, and it is judged in turn. Use: promise then=>MutationObserver=>setImmediate=>setTimeout
var timerFunc;

if (typeof Promise !== "undefined" && isNative(Promise)) {
  var p = Promise.resolve();
  timerFunc = function() {
    p.then(flushCallbacks);
    // 1. Promise.then
    if (isIOS) {
      setTimeout(noop);
    }
  };
  isUsingMicroTask = true;
} else if (
  !isIE &&
  typeof MutationObserver !== "undefined" &&
  (isNative(MutationObserver) ||
    MutationObserver.toString() === "[object MutationObserverConstructor]")
) {
  // 2.  MutationObserver
  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)) {
  // 3. setImmediate
  timerFunc = function() {
    setImmediate(flushCallbacks);
  };
} else {
  //4. setTimeout
  timerFunc = function() {
    setTimeout(flushCallbacks, 0);
  };
}
  1. Flushcallbacks iterates through all callbacks and executes
function flushCallbacks() {
  pending = false;
  var copies = callbacks.slice(0);
  callbacks.length = 0;
  for (var i = 0; i < copies.length; i++) {
    copies[i]();
  }
}
  1. Among them, the flushschedulequeue added earlier uses the run method of the watcher in the queue to update the components
for (index = 0; index < queue.length; index++) {
  watcher = queue[index];
  watcher.run();
}

summary

The above is the implementation principle of Vue’s nexttick method. The summary is as follows:

  1. Vue uses asynchronous queue to control DOM update and nexttick callback execution successively
  2. Because of its high priority, microtask can ensure that the micro tasks in the queue are executed before an event cycle
  3. Because of compatibility problems, Vue had to make a scheme to downgrade microtask to macrotask

reference resources