What is an event loop

Before we get into event loops, we need some prior knowledge of JS features.

JS engine is single-threaded, which means that JS engine can only do one thing at a time, while Java, a multithreaded language, can do several things at the same time.

JS tasks are divided into synchronous and asynchronous, the so-called “asynchronous”, simply speaking, a task is not completed continuously, first execute the first paragraph, and so on ready, and then go back to execute the second paragraph, the second paragraph is also called callback; Synchronization is done coherently.

Tasks such as reading files and network requests are asynchronous tasks: they take a long time, but the JS engine doesn’t need to do the intermediate operations itself, it just waits for someone else to give it the data when it is ready, and it continues to perform the callback part.

If there is no special processing, the JS engine should wait while it executes an asynchronous task, not doing anything else. Using a diagram to illustrate this process, you can see that a lot of free time is wasted when performing asynchronous tasks.

In fact, this is how most multithreaded languages handle it. However, for a single-threaded language like JS, such a long idle wait is unacceptable: Java can open another thread to handle other urgent tasks, while JS has to wait.

So the following “asynchronous task callback notification” mode is adopted:

The JS engine executes other synchronous tasks while waiting for the asynchronous task to be ready, and then executes the callback when the asynchronous task is ready. The advantage of this mode is that it takes much less time to complete the same task, which is also known as non-blocking.

The implementation of this “notification”, it is the event loop, the callback part of the asynchronous task to the event loop, such as the appropriate time to return to the JS thread execution. The event loop was not invented by JavaScript; it is a mechanism for running computers.

The event loop consists of a queue, the callback of asynchronous tasks follows the first-in, first-out (FIFO), and is fetched round after round when the JS engine is idle, so it is called a loop.

According to the different tasks in the queue, it can be divided into macro tasks and micro tasks.

Macro and micro tasks

The event loop consists of the macro task and all the microtasks that are generated during the execution of the macro task. Once the current macro mission is completed, all micromissions that are joined in the meantime will be executed immediately.

This is designed to give urgent tasks a chance to jump the queue, otherwise new tasks will always be at the back of the queue. By distinguishing between microtasks and macro tasks, the microtasks in this cycle are effectively cutting in line, so that state changes made in the microtasks can be synchronized in the next event cycle.

Common macro task: script code (whole)/setTimout/setInterval/setImmediate (unique) node/requestAnimationFrame browser (unique)/IO/UI render browser (unique)

Common micro tasks include: process. NextTick (unique) node/Promise. Then ()/Object. Observe/MutationObserver

Error in macro task setTimeout

The setTimeout callback may not be executed after the specified time. Instead, after a specified time, the callback function is placed in the queue of the event loop.

If the time is up and the JS engine is still performing the synchronization task, the callback function needs to wait; If there are other callbacks in the queue for the current event loop, wait for the other callbacks to finish executing.

In addition, setTimeout 0ms is not executed immediately, it has a default minimum time of 4ms.

So the output of this code is not necessarily:

// node
setTimeout(() = > {
  console.log('setTimeout')},0)
setImmediate(() = > {
  console.log('setImmediate')})Copy the code

Because the global Script is executed before the first macro task is fetched, if the time is greater than 4ms, then setTimeout callback has been queued, setTimeout is executed first. SetImmediate is used first if the lead time is less than 4ms.

Browser event loop

The browser event loop consists of one macro task queue + multiple microtask queues.

First, execute the first macro task: the global Script Script. The resulting macro and micro tasks are queued separately. After executing the Script, the current microtask queue is emptied. Complete an event loop.

It then fetches another macro task, which also enrolls callbacks generated in the meantime. Then empty the current microtask queue. And so on.

There is only one macro task queue, and each macro task has its own micro task queue, and each cycle is composed of one macro task + multiple micro tasks.

The following Demo shows the queue-jumping process for microtasks:

Promise.resolve().then(() = >{
  console.log('First callback: microtask 1')  
  setTimeout(() = >{
    console.log('Third callback: macro Task 2')},0)})setTimeout(() = >{
  console.log('Second callback function: macro task 1')
  Promise.resolve().then(() = >{
    console.log('Fourth callback: microtask 2')})},0)
// The first callback function: microtask 1
// The second callback function: macro task 1
// The fourth callback: microtask 2
// The third callback function: macro task 2
Copy the code

Instead of printing from 1 to 4, the fourth callback is executed first, then the third, because it is a microtask and has a higher priority than the third callback.

Node event loop

Node’s event loop is much more complex than a browser’s. It consists of 6 macro task queues +6 micro task queues.

Macro tasks in descending order of priority are:

The execution rule is as follows: after the execution of a macro task queue is complete, the task queue will be emptied once, and then the macro task queue at the next level will be emptied again and again. A macro task queue is paired with a micro task queue.

All six levels of macro tasks are completed, which is a cycle.

The Timers, Poll, and Check phases are of interest, as most of the code we write falls into these three phases.

  1. Timers: setTimeout/setInterval;
  2. Poll: Obtains new I/O events, such as reading files.
  3. The Check: setImmediate callback function executes here;

In addition, node microtasks also have priority:

  1. process.nextTick;
  2. Promise. Then, etc;

When the microtask queue is emptied, process.nexttick is executed first, followed by the rest of the microtask queue.

This code illustrates the difference between a browser and Node:

console.log('Script started')
setTimeout(() = > {
  console.log('First callback function, macro task 1')
  Promise.resolve().then(function() {
    console.log('Fourth callback function, microtask 2')})},0)
setTimeout(() = > {
  console.log('Second callback function, macro task 2')
  Promise.resolve().then(function() {
    console.log('Fifth callback function, microtask 3')})},0)
Promise.resolve().then(function() {
  console.log('Third callback function, microtask 1')})console.log('end of the Script)
Copy the code
The node side: Script start Script end third callback, micro task 1 first callback, macro task 1 second callback, macro task 2 fourth callback, micro task 2 fifth callback, micro task 3 browser Script start Script end third callback, Micro task 1 first callback, macro task 1 fourth callback, micro task 2 second callback, macro task 2 fifth callback, micro task 3Copy the code

The fourth callback function, microtask 2, is printed after two setTimeouts are complete.

Because browsers execute one macro task plus one microtask queue, node is a whole macro task queue plus one microtask queue.

Node11.x version difference

Before node11.x, the rule for the event loop was as described above: pull out all tasks in an entire macro task queue, and then execute a microtask queue.

But after 11.x, the Node side event loop becomes browser-like: a macro task is executed, followed by a queue of microtasks. However, the priority of macro and micro task queues is still retained.

This can be demonstrated by the following Demo:

console.log('Script started')
setTimeout(() = > {
  console.log('Macro Task 1 (setTimeout)')
  Promise.resolve().then(() = > {
    console.log('Microquest Promise2')})},0)
setImmediate(() = > {
  console.log('Macro Task 2')})setTimeout(() = > {
  console.log('Macro Task 3 (setTimeout)')},0)
console.log('end of the Script)
Promise.resolve().then(() = > {
  console.log('Microtask promise1')
})
process.nextTick(() = > {
  console.log('Microtask nextTick')})Copy the code

Run before node11.x:

Script Start Script End Microtask nextTick Microtask Macro task 1 (setTimeout) Macro task 3 (setTimeout) Microtask Macro task 2 (setImmediate)Copy the code

Run after node11.x:

Script Start Script End Microtask nextTick Microtask Promise1 Macro task 1 (setTimeout) Microtask Promise2 macro task 3 (setTimeout) Macro task 2 (setImmediate)Copy the code

It can be found that under different Node environments:

  1. In the microtask queue, process.nextTick has a higher priority and prints first even if it is later in the microtask queueMicro task nextTickagainMicro task promise1;
  2. The macro task setTimeout has a higher priority than setImmediate,Macro task 2 (setImmediate)Is the last of the three macro tasks to print;
  3. Before node11.x, the microtask queue prints after two settimeouts, waiting for all macro tasks of the current priority to complete firstMicro task promise2; After node11.x, the microtask queue only waits for the current macro task to finish first.

conclusion

Tasks in the event loop are divided into macro and micro tasks to give higher-priority tasks a chance to jump the queue: micro tasks have higher priority than macro tasks.

The Node side event loop is more complex than the browser, with macro tasks having six priorities and micro tasks having two priorities. Node is a macro task queue with a microtask queue, while browser is a single macro task with a microtask queue. But after Node11, node and browser rules converge.

If you find this article helpful, give me a thumbs up. It means a lot to me