skip to content
usubeni fantasy logo Usubeni Fantasy

Vue's asynchronous update mechanism

/ 8 min read

This Post is Available In: CN EN ES JA

This article mainly introduces the principles of Vue’s asynchronous updates. The core understanding of this article is not difficult without the understanding of the reactive principle of Vue. The key point is the word “queue”.

Asynchronous Updates

Why do we need asynchronous updates?

this.a = 1;
this.b = 2;
this.c = 3;
this.a = 5;
this.a = 6;

Think about it, according to the previous analysis of the reactive principle of Vue, when reactive data is assigned a value, the setter is executed, which triggers the view update. But as you can see from the example above, the scenario of consecutive assignments is quite common. In React, you can write them in a setState method, but Vue does not have such a method.

You can’t refresh the page immediately every time you update the data. If you run the render and patch methods for each assignment, the whole page will be very slow, right?

The key to solving this problem is to first store the update functions that need to be executed in a queue (and when adding to the queue, the same function will not be added repeatedly), and then execute the queue asynchronously.

Before we formally analyze the asynchronous queue principle of Vue, let’s take a look at these variables used to implement the queue:

var circular = {}; // Detect circular references in development environment
var queue = []; // Watcher queue
var activatedChildren = [];
var has = {}; // Whether it has been added to the queue
var waiting = false; // Can be understood as whether waiting for flushing
var flushing = false; // Whether the queue processing has started
var index = 0; // The current position of the queue

You may be curious why there are two similar variables, waiting and flushing?

At first, I was also puzzled. Whether it is flushing or waiting for flushing, although there are subtle differences:

  • When waiting, the asynchronous task has not started yet.
  • When flushing, it has been determined that the queue task has started.

Although there is a slight difference in the starting time, they are reset at the same time (resetSchedulerState), so it seems that there is no need to have two variables. So for now, I understand it as a way to make the code more self-documenting.

Speaking of the queue, this system has two queues. Since the previous article mentioned that queueWatcher will put the watcher that needs to be updated into the queue and update them together next time. Now let’s talk about the watcher queue and analyze what operations queueWatcher actually performs.

queueWatcher

function queueWatcher(watcher) {
var id = watcher.id;
if (has[id] == null) {
has[id] = true; // Mark the watcher as added to the queue
if (!flushing) {
// If the queue has not started updating, just push the watcher into the queue
queue.push(watcher);
} else {
// When the queue is being processed, Vue's approach is to directly insert the new watcher into the latest position it has reached
// This implies hidden logic
// The watcher that has already been executed before will be executed immediately in the next run
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;
nextTick(flushSchedulerQueue);
}
}
}

The final nextTick(flushSchedulerQueue) is to put the watcher queue into the nextTick queue (although no arguments are passed, flushSchedulerQueue can read the queue processed by queueWatcher).

In this short function call, two key points are connected: flushSchedulerQueue is the core of processing the watcher queue, and nextTick is the core of Vue’s asynchronous rendering.

flushSchedulerQueue

According to the official comments, queue.sort sorts the queue to ensure:

  • The parent component is processed before the child component.
  • User-defined watcher functions are processed before the render watcher.
  • If a component is destroyed by its parent component, the watcher can be skipped (by checking the active property of the watcher).
function flushSchedulerQueue() {
flushing = true;
var watcher, id;
queue.sort(function (a, b) {
return a.id - b.id;
});
// The length of the queue can change, so we can't cache queue.length
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
id = watcher.id;
has[id] = null; // Remove the processed watcher from the has object
watcher.run();
}
// Keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
resetSchedulerState(); // Reset queue-related variables
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
}

In simple terms, flushSchedulerQueue is used to process the queue arranged by queueWatcher.

Here’s a practical question: Can we access the latest DOM in the callback function of a watched value?

The answer is hidden in the above code: during the loop processing of queue in the flushSchedulerQueue function, queue still accepts watchers pushed by queueWatcher. This is mainly used for user-defined watch functions or updates triggered between components. They are not scheduled for the next loop, but are directly inserted into the queue for sequential updates.

So, don’t assume that when watching a value change, you can access the latest DOM in the callback function, because they are running in the same tick (and the component rendering function always runs last due to the sort).

nextTick

var nextTick = (function() {
var callbacks = []
var pending = false
var timerFunc
function nextTickHandler() {
pending = false
var copies = callbacks.slice(0)
callbacks.length = 0
for (var i = 0; i < copies.length; i++) {
copies[i]()
}
}
Translate into English:
```javascript
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = function() {
setImmediate(nextTickHandler)
}
} else if (
typeof MessageChannel !== 'undefined' &&
(isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]')
) {
var channel = new MessageChannel()
var port = channel.port2
channel.port1.onmessage = nextTickHandler
timerFunc = function() {
port.postMessage(1)
}
} else if (typeof Promise !== 'undefined' && isNative(Promise)) {
// use microtask in non-DOM environments, e.g. Weex
var p = Promise.resolve()
timerFunc = function() {
p.then(nextTickHandler)
}
} else {
// fallback to setTimeout
timerFunc = function() {
setTimeout(nextTickHandler, 0)
}
}
return function queueNextTick(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()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function(resolve, reject) {
_resolve = resolve
})
}
}
})()

Let me explain how Vue creates asynchronous execution:

According to the official comments, in the earlier versions of Vue (pre 2.4), microtasks such as Promise/MutationObserver were used for the asynchronous deferring mechanism. However, it was found that microtasks had a higher priority and would fire in between supposedly sequential events or even between the bubbling of the same event. Technically, setImmediate would be the ideal choice, but it is not available everywhere. The only polyfill that consistently queues the callback after all DOM events triggered in the same loop is by using MessageChannel.

In this case, Vue provides four ways to create asynchronous calls:

  • setImmediate
  • MessageChannel, which can be used in IE 10 or above. Surprisingly, MessageChannel has a wider range of availability than Promise.
  • Promise, which is used for Weex according to the comments.
  • setTimeout 0, for IE 10 and below.

So, what is nextTick all about?

From the code, it can be seen that nextTick is an immediately invoked function that returns a queueNextTick function. So, in reality, we are using queueNextTick(cb, ctx). (However, I’m not sure why the variables callbacks, timerFunc, etc. are wrapped in a closure instead of being placed outside.)

In the case of nextTick(flushSchedulerQueue), flushSchedulerQueue is the callback function cb.

  • First, cb is pushed into callbacks, which is an asynchronous running queue and is different from the watcher queue.
  • Based on the condition !pending, when pending is true, the timerFunc execution is skipped until the previous nextTickHandler is completed.

By the way, the $nextTick that we usually use is actually the same as the nextTick function:

Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this);
};

Summary:

There are two queues:

  • The queue in queueWatcher
  • The callbacks in nextTick

The queue is more flexible and allows for timely addition of components that need to be modified during component updates.

Although callbacks appears to be a queue and an array, it does not add anything to callbacks when waiting is true. callbacks should always maintain a state where only one function is in the queue. (This is how it should work in theory, but I’m not sure how to verify it. Please correct me if I’m wrong.)

The process is as follows:

  • The purpose of asynchronous updates is to prevent the update function from running repeatedly and to make the execution smoother.
  • After the setter triggers the watcher, the callback function will not run immediately, but will be added to the queue through the queueWatcher function.
  • When idle, the queueWatcher function runs nextTick(flushSchedulerQueue) to schedule the execution of the queue in the next tick.
  • The flushSchedulerQueue function runs the functions in the watcher queue.
  • nextTick is just like the $nextTick commonly used, it schedules the execution of the queue in the next tick.
  • Vue has 4 methods to create the next tick: setImmediate, MessageChannel, Promise, setTimeout.
评论组件加载中……