Vue's asynchronous update mechanism
/ 8 min read
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?
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.
Related Variables
Before we formally analyze the asynchronous queue principle of Vue, let’s take a look at these variables used to implement 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
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).
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
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 intocallbacks
, which is an asynchronous running queue and is different from the watcher queue. - Based on the condition
!pending
, whenpending
is true, thetimerFunc
execution is skipped until the previousnextTickHandler
is completed.
By the way, the $nextTick
that we usually use is actually the same as the nextTick
function:
Summary:
There are two queues:
- The
queue
inqueueWatcher
- The
callbacks
innextTick
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 thequeueWatcher
function. - When idle, the
queueWatcher
function runsnextTick(flushSchedulerQueue)
to schedule the execution of thequeue
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.