Hello, everyone. I am Karsong.
It is common for front-end frameworks to combine updates triggered by multiple independent variable changes into a single batch execution, with different timing depending on the type of framework.
For example, the following Svelte code executes the onClick callback after clicking H1, triggering three updates. Because of batch processing, three updates are merged into one.
Then print the rendering results in the form of synchronous, microtask and macro task respectively:
<script>
let count = 0;
let dom;
const onClick = () = > {
// Merge three updates into one
count++;
count++;
count++;
console.log("Sync result:", dom.innerText);
Promise.resolve().then(() = > {
console.log("Microtask results:", dom.innerText);
});
setTimeout(() = > {
console.log("Macro task result:", dom.innerText);
});
}
</script>
<h1 bind:this={dom} on:click={onClick}>{count}</h1>
Copy the code
The same logic is implemented with a different framework, and the print result is as follows:
Vue3
: Synchronization result: 0 Microtask result: 3 Macro task result: 3Svelte
: Synchronization result: 0 Microtask result: 3 Macro task result: 3Legacy React
: Synchronization result: 0 Microtask result: 3 Macro task result: 3Concurrent React
: Synchronization result: 0 Microtask result: 0 Macro task result: 3
4 types of implementations: React
Vue3 Svelte
The underlying reason is that some frameworks use macro tasks for batch processing, while others use microtasks for batch processing.
The origins of macro and micro tasks and their relationship to batch processing will be explained in the rest of this article.
Welcome to join the Human high quality front-end framework research group, Band fly
How to Schedule tasks
First put the complete flow chart, convenient to have an overall impression:
By default, there is one render process for each Tab page in the browser (Chrome for example). The render process consists of the main thread, composite thread, IO thread, and other threads.
The main thread is very busy, handling the DOM, calculating styles, handling layouts, handling event responses, executing JS, and so on.
There are two issues that need to be addressed:
-
How do you schedule tasks that come not only from inside the thread, but also from outside?
-
How do new tasks participate in scheduling in the main thread?
The answer to the first question is: message queues
All scheduled tasks are added to the task queue. According to the first in, first out (FIFO) feature of queues, the earliest queued tasks are processed first. Pseudocode description is as follows:
// Fetch the task from the task queue
const task = taskQueue.takeTask();
// Execute the task
processTask(task);
Copy the code
Other processes send tasks via IPC to the renderer’s IO thread, which in turn sends tasks to the main thread’s task queue, for example:
-
After a mouse click, the browser process sends the “click event” via IPC to the IO thread, which sends it to the task queue
-
After the resource is loaded, the network process sends the “load completion event” via IPC to the IO thread, which then sends it to the task queue
How do I schedule new tasks
The answer to the second question is: the event loop
The main thread performs tasks in a loop statement. As the loop continues, new tasks are inserted at the end of the queue and old tasks are taken out for execution. Pseudocode description is as follows:
// Exits the event loop
let keepRunning = true;
/ / main thread
function MainThread() {
// Execute tasks in a loop
while(true) {
// Fetch the task from the task queue
const task = taskQueue.takeTask();
// Execute the task
processTask(task);
if(! keepRunning) {break; }}}Copy the code
Delayed tasks
In addition to the task queue, the browser also implements a delay queue according to the WHATWG standard, which is used to store tasks that need to be delayed (such as setTimeout). The pseudo-code is as follows:
function MainThread() {
while(true) {
const task = taskQueue.takeTask();
processTask(task);
// Execute the task in the delay queue
processDelayTask()
if(! keepRunning) {break; }}}Copy the code
When the task in this cycle is complete (that is, after processTask is executed), processDelayTask is executed to check if any of the delayed tasks have expired, and if any of them have expired, it is executed.
Because processDelayTask is executed after processTask, the execution time of the task is long, which may cause the delayed task to fail to execute on schedule. Consider the following code:
function sayHello() { console.log('hello')}function test() {
setTimeout(sayHello, 0);
for (let i = 0; i < 5000; i++) {
console.log(i);
}
}
test()
Copy the code
Even if the delay time of task sayHello is set to 0, it can be executed only after the task of test completes. Therefore, the delay time of sayHello is longer than the set time.
Macro and micro tasks
New tasks added to the task queue can be executed only after other tasks in the queue are completed, which is unfavorable for tasks that need to be executed first in an emergency.
In order to solve the timeliness problem, tasks in the task queue are called macro tasks. During the execution of macro tasks, micro tasks can be generated and stored in the micro task queue in the execution context of the task.
The part on the right of the flowchart:
Before the macro task is executed, the microtask queue is traversed and the microtasks generated during the execution of the macro task are executed in batches.
MutationObserver
How do microtasks solve the timeliness problem while simultaneously balancing performance?
Consider MutationObserver, a microtask API for monitoring DOM changes.
When multiple DOM changes occur in the same macro task, multiple MutationObserver microtasks will be generated, and their execution time is before the end of the macro task execution, which ensures timeliness compared with entering the queue for execution as a new macro task.
Also, because the microtasks in the microtask queue are executed in batches, performance is better than synchronous callback execution for every DOM change.
conclusion
The implementation nature of batch processing in the framework is very similar to that of MutationObserver. Using the asynchronous execution of macro and micro tasks, the update is packaged and executed.
However, different frameworks have different update granularity. For example, Vue3 and Svelte have very fine update granularity, so they use microtasks to realize batch processing.
React update granularity is coarse, but its internal implementation is complex, that is, there are macro task scenarios and micro task scenarios.