Event Loop microtasks and macrotasks
JavaScript Event Loop: microtasks and macrotasks
The execution flow of browser JavaScript relies on an event loop. It is essential for optimization and the correct architecture. (浏览器JavaScript的执行流程依赖于事件循环。这对于优化和正确的架构至关重要。)
In this cover, you will first find the theory and then the practical applications. (在本封面中,您将首先找到理论,然后是实际应用。)
Event Loop
Event Loop (事件循环)
Event loop has a straightforward concept. There exists an endless loop when the engine of JavaScript waits for tasks, runs them and then sleeps expecting more tasks. (事件循环有一个简单的概念。当JavaScript引擎等待任务,运行它们,然后在等待更多任务时休眠时,会存在一个无穷无尽的循环。)
The engine’s primary algorithm is as follows:
Once there are tasks: executing them, beginning with the oldest one. Sleep till a task turns out, then get to 1.
However, the JavaScript engine doesn’t do anything most of the time. It only runs when a handler, script, or an event triggers. (但是, JavaScript引擎在大多数情况下不会执行任何操作。它仅在处理程序、脚本或事件触发时运行。)
The examples of tasks can be the following:
Once an external script <script src="…"> loads, the task is executing it. Once the user moves the mouse, the task is dispatching the mousemove event, executing handlers. Once the time has come for setTimeout, the task is running its callback.
More examples can be counted. (可以统计更多示例。)
Once the tasks are set, the engine handles them, expecting more tasks while sleeping. (设置任务后,引擎会处理它们,期望在睡眠时执行更多任务。)
A task may come while the engine is busy, but it will be enqueued. As illustrated above, when the engine is busy running a script, the user can move the mouse, leading to mousemove , and setTimeout can be due. Also note, that while the engine is executing a task, rendering will not happen. Only after the task is complete, the changes are painted in the DOM. In case a tasks lasts too long, the browser is not able to carry out other tasks. After a long time it will alert “Page Unresponsive” offering to kill the task. That takes place once there are multiple complex calculations or a programming error that can bring an infinite loop.
So, you got acquainted with the theory, now let’s see what happens in practice. (所以,你已经熟悉了这个理论,现在让我们看看在实践中会发生什么。)
Splitting CPU-hungry Tasks
Splitting CPU-hungry Tasks (拆分占用CPU的任务)
Imagine having a CPU-hungry task. (想象一下,任务耗费CPU。)
For instance, syntax- highlighting is considered CPU-hungry. For highlighting the code, it implements analysis, generates multiple colored elements, places them to the document. (例如,语法-突出显示被认为是占用CPU的。为了突出显示代码,它实现分析,生成多个彩色元素,并将它们放置到文档中。)
When JavaScript engine is busy with highlighting, it can’t do any DOM-related tasks, process user events, and so on. (当JavaScript引擎忙于突出显示时,它无法执行任何与DOM相关的任务、处理用户事件等。)
Start at highlighting 100 lines, then schedule setTimeout for the following 100 lines. (从突出显示100行开始,然后为以下100行安排setTimeout。)
For illustrating that approach, let’s consider a function, which counts from 1 to 1000000000:
let t = 0;
let start = Date.now();
function countDate() {
for (let j = 0; j < 1e9; j++) {
t++;
}
console.log("Done in " + (Date.now() - start) + 'ms');
}
countDate();
After running the code above, the code will hang for a while. (运行上述代码后,代码将挂起一段时间。)
The browser can even demonstrate “ the script takes too long” alert. (浏览器甚至可以显示“脚本耗时太长”的警报。)
You have the option of splitting the job with nested setTimeout calls like this:
let t = 0;
let start = Date.now();
function countDate() {
do {
t++;
} while (t % 1e6 != 0);
if (t == 1e9) {
console.log("Done in " + (Date.now() - start) + 'ms');
} else {
setTimeout(countDate); // schedule the new call
}
}
countDate();
After that, the browser interface will be completely functional. (之后,浏览器界面将完全正常工作。)
In case a new side task turns out at the time the engine is busy with the part 1, it will be queued and then run when the 1st part is completed. (如果在引擎忙于部件1时出现新的副任务,它将被排队,然后在第一部分完成时运行。)
The periodic returns to the event loop between the countDate executions provide time for the engine to react to other user actions. (在countDate执行之间定期返回事件循环,为引擎提供对其他用户操作做出反应的时间。)
The most interesting thing is that both of the options (with and without split) are comparable in speed. (最有趣的是,这两个选项(带拆分和不带拆分)的速度相当。)
For making them closer, you can initiate an improvement. For that purpose, it’s necessary to move the scheduling to the countDate() start, like this:
let t = 0;
let start = Date.now();
function countDate() {
// move scheduling to the beginning
(//将调度移至开头)
if (t < 1e9 - 1e6) {
setTimeout(countDate); // new call scheduling
}
do {
t++;
} while (t % 1e6 != 0);
if (t == 1e9) {
cosnole.log("Done in " + (Date.now() - start) + 'ms');
}
}
countDate();
Running it will make you save time, as it takes significantly less time. (运行它将节省您的时间,因为它花费的时间要少得多。)
Progress Indication
Progress Indication (进度指示)
The next advantage of splitting large tasks for browser scripts is that it’s possible to demonstrate progress indication. (为浏览器脚本拆分大型任务的下一个优点是可以演示进度指示。)
Usually, the rendering takes place after the currently running code is complete. (通常,渲染在当前运行的代码完成后进行。)
On the one hand, it’s very convenient, as the function can create multiple elements, adding them one by one and changing their styles. (一方面,它非常方便,因为该函数可以创建多个元素,逐个添加它们并更改它们的样式。)
Let’s see a demo:
<!DOCTYPE html>
<html>
<head>
<title>Title of the Document</title>
</head>
<body>
<div id="progressId"></div>
<script>
function count() {
for(let i = 0; i < 1e6; i++) {
i++;
progressId.innerHTML = i;
}
}
count();
</script>
</body>
</html>
While splitting a large task into pieces with setTimeout, the changes are painted out between them. So, the code below will look better:
<!DOCTYPE html>
<html>
<head>
<title>Title of the Document</title>
</head>
<body>
<div id="divId"></div>
<script>
let i = 0;
function count() {
// do some hard work
(//做一些艰苦的工作)
do {
i++;
divId.innerHTML = i;
} while (i % 1e3 != 0);
if(i < 1e7) {
setTimeout(count);
}
}
count();
</script>
</body>
</html>
Acting After the Event
Acting After the Event (活动结束后采取行动)
Inside the event handler, you can delay some actions till the event has bubbled up and handled. It can be implemented by wrapping the code into zero delay setTimeout. (在事件处理程序中,您可以延迟某些操作,直到事件冒泡并处理完毕。可以通过将代码包装到零延迟setTimeout中来实现。)
Let’s see an example where a custom menuOpen event is dispatched into setTimeout in a way that it takes place after the click event is handled completely:
menu.onclick = function () {
// ...create a custom event with the data of the menu item that was clicked
(//...使用单击的菜单项的数据创建自定义事件)
let customEvent = new CustomEvent("menuOpen", {
bubbles: true
});
// dispatch the custom event asynchronously
(//异步分派自定义事件)
setTimeout(() => menu.dispatchEvent(customEvent));
};
You can learn more about it in chapter Dispatching custom events. (您可以在“分派自定义事件”一章中了解更多相关信息。)
Macrotasks and Microtasks
Macrotasks and Microtasks (宏任务和微任务)
Along with macrotasts, there exist microtasks that come exclusively from your code. (除了macrotasts之外,还有完全来自您的代码的微任务。)
As a rule, promises create them. The execution of .then/catch/finally transforms into a microtask. (一般来说,承诺会创建它们。.then/catch/finally的执行转换为微任务。)
There exists a unique queueMicrotask(func) function, which queues func for running the microtask queue. (存在唯一的queueMicrotask (func)函数,该函数将func排队以运行微任务队列。)
Right after each macrotask, the engine performs all the tasks from the queue of microtasks before running any other task or rendering something, and so on. (在每个宏任务之后,引擎在运行任何其他任务或渲染某些内容之前执行微任务队列中的所有任务,依此类推。)
For example:
setTimeout(() => console.log("timeout"));
Promise.resolve()
.then(() => console.log("promise"));
console.log("code");
In the picture below, you can see a richer event loop where the order is from top to bottom. In other words, the script comes first, then microtasks, rendering, and so on. (在下图中,您可以看到一个更丰富的事件循环,顺序是从上到下。换句话说,首先是脚本,然后是微任务、渲染等。)
All the microtasks should be completed prior any other event handling or other actions. (在任何其他事件处理或其他操作之前,应完成所有微任务。)
It plays an essential role, because it assures that the application environment is similar between the microtasks. (它起着至关重要的作用,因为它确保微任务之间的应用环境相似。)
In case you want to execute an asynchronous function before the changes are rendered or new events are handled, you should schedule it using queueMicrotask(func). (如果要在呈现更改或处理新事件之前执行异步函数,则应使用queueMicrotask (func)对其进行计划。)
Let’s take a look at an example similar to the previous one. But, in this example, we use queueMicrotask(func) rather than setTimeout :
<!DOCTYPE html>
<html>
<head>
<title>Title of the Document</title>
</head>
<body>
<div id="divId"></div>
<script>
let t = 0;
function count() {
// do some hard work
(//做一些艰苦的工作)
do {
t++;
divId.innerHTML = t;
} while (t % 1e3 != 0);
if(t < 1e6) {
queueMicrotask(count);
}
}
count();
</script>
</body>
</html>
Summary
Summary (概要)
In this chapter, we took a detailed look at the event loop algorithm in JavaScript. (在本章中,我们详细介绍了JavaScript中的事件循环算法。)
The algorithm is the following:
Dequeuing and running the oldest one from the macrotask queue. Implementing all the microtasks. If there are changes, rendering them. In case the macrotask queue is empty, waiting until a macrotask turns out. Going to step 1. (从宏任务队列中取消排队并运行最旧的宏任务队列。 执行所有微任务。 如果有更改,则渲染它们。 如果宏任务队列为空,请等待宏任务出现。 转到第1步。)
For scheduling a macrotask a zero delayed setTimeout(f) is used. (对于调度宏任务,使用零延迟setTimeout (f)。)
For scheduling a new microtask queueMicrotask(f) is used. (对于调度,使用新的微任务队列Microtask (f)。)