Node timer detailed
JavaScript runs in a single thread, and asynchronous operations are particularly important.
As long as you use functions other than the engine, you need to interact with the outside to form an asynchronous operation. Because there are so many asynchronous operations, JavaScript has to provide a lot of asynchronous syntax. This is like, some people are always hit, and his anti-strike ability must become very strong, otherwise he will be finished.
Node’s asynchronous syntax is more complicated than that of browsers, because it can talk to the kernel, and has to build a special library libuv to do this. This library is responsible for the execution time of various callback functions. After all, asynchronous tasks still have to return to the main thread in the end, and queued for execution one by one.
In order to coordinate asynchronous tasks, Node unexpectedly provides four timers so that tasks can run at specified times.
- setTimeout()
- setInterval()
- setImmediate()
- process.nextTick()
The first two are language standards, and the latter two are unique to Node. They are written in the same way and have similar functions, so they are not easy to distinguish.
Can you tell the result of the following code?
// test.js setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); process.nextTick(() => console.log(3)); Promise.resolve().then(() => console.log(4)); (() => console.log(5))();
The results of the operation are as follows.
$ node test.js 5 3 4 1 2
If you can say it right, you may not need to read it anymore. This article explains in detail how Node handles various timers, or more broadly, how the libuv library arranges asynchronous tasks to be executed on the main thread.
1. Synchronous tasks and asynchronous tasks
First, synchronous tasks are always executed earlier than asynchronous tasks.
In the previous piece of code, only the last line is a synchronous task, so it is executed first.
(() => console.log(5))();
2. The current cycle and the second cycle
Asynchronous tasks can be divided into two types.
- Asynchronous tasks added in this round of loop
- Asynchronous tasks appended in the second round of the loop
The so-called “loop” refers to the event loop (event loop). This is how the JavaScript engine handles asynchronous tasks, which will be explained in detail later. As long as you understand here, the current cycle must be executed earlier than the next cycle.
Node provisions,
process.nextTick
and
Promise
the callback function, added in the current round of cycle, that synchronous task once the implementation is completed, they begin to execute them.
And
setTimeout
,
setInterval
,
setImmediate
the callback function, added in the second round cycle.
This means that the third and fourth lines of the code at the beginning of the text must be executed earlier than the first and second lines.
// 下面两行,次轮循环执行 setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); // 下面两行,本轮循环执行 process.nextTick(() => console.log(3)); Promise.resolve().then(() => console.log(4));
Three, process.nextTick()
process.nextTick
The name is a bit misleading. It is executed in this round of loop and is the fastest executed among all asynchronous tasks.
process.nextTick
The task queue that
Node will execute next after executing all synchronization tasks
.
Therefore, the following line of code is the second output.
process.nextTick(() => console.log(3));
Basically, if you want asynchronous tasks to execute as fast as possible, use them
process.nextTick
.
Four, micro tasks
According to the language specification,
Promise
the callback function of the object will enter the “microtask” queue in the asynchronous task.
The micro task queue is appended to
process.nextTick
the back of the queue, which also belongs to the current cycle.
Therefore, the following code is always output first
3
, then output
4
.
process.nextTick(() => console.log(3)); Promise.resolve().then(() => console.log(4)); // 3 // 4
Note that the next queue will only be executed after the previous queue is emptied.
process.nextTick(() => console.log(1)); Promise.resolve().then(() => console.log(2)); process.nextTick(() => console.log(3)); Promise.resolve().then(() => console.log(4)); // 1 // 3 // 2 // 4
In the above code, all
process.nextTick
callback functions will be executed earlier
Promise
.
At this point, the execution sequence of this round of loop is over.
- Synchronization task
- process.nextTick()
- Micro task
Five, the concept of event loop
Let’s start to introduce the execution sequence of the second round of loop, which must understand what is an event loop (event loop).
The official documentation of Node introduces it like this.
“When Node.js starts, it initializes the event loop, processes the provided input script which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop.”
This passage is very important and needs to be read carefully. It expresses three meanings.
First, some people think that in addition to the main thread, there is a separate event loop thread. This is not the case, there is only one main thread, and the event loop is completed on the main thread.
Secondly, when Node starts to execute the script, it will initialize the event loop first, but at this time the event loop has not yet started, and the following things will be completed first.
- Synchronization task
- Make an asynchronous request
- Plan the time when the timer takes effect
- Execution
process.nextTick()
etc.
Finally, all the above things are done, and the event loop officially begins.
Six, the six stages of the event loop
The event loop will execute indefinitely, round after round. Only when the callback function queue of the asynchronous task is emptied, the execution will stop.
Each round of the event loop is divided into six stages. These stages will be executed in sequence.
- timers
- I/O callbacks
- idle, prepare
- poll
- check
- close callbacks
Each stage has a first-in, first-out callback function queue. Only when the callback function queue of one stage is emptied and all the callback functions to be executed are executed, the event loop will enter the next stage.
The following is a brief introduction to the meaning of each stage. For a detailed introduction , you can refer to the official document or refer to the interpretation of libuv’s source code .
(1) timers
This is
the callback function
of the timer phase, processing
setTimeout()
and
setInterval()
.
After entering this stage, the main thread will check the current time and whether it meets the conditions of the timer.
If satisfied, execute the callback function, otherwise leave this stage.
(2) I/O callbacks
Except for the callback functions for the following operations, other callback functions are executed at this stage.
setTimeout()
AndsetInterval()
callback functionsetImmediate()
Callback function- The callback function used to close the request, such as
socket.on('close', ...)
(3) idle, prepare
This stage is only for internal call of libuv and can be ignored here.
(4) Poll
This stage is the polling time, which is used to wait for I/O events that have not yet returned, such as the server’s response, the user’s movement of the mouse, and so on.
This stage will take a long time. If there are no other asynchronous tasks to be processed (such as an expired timer), it will stay in this stage and wait for the I/O request to return a result.
(5) check
The
setImmediate()
callback function
executed
at
this stage
.
(6) close callbacks
This stage executes the callback function of the close request, for example
socket.on('close', ...)
.
Seven, an example of the event loop
Below is an example from the official documentation.
const fs = require('fs'); const timeoutScheduled = Date.now(); // 异步任务一:100ms 后执行的定时器 setTimeout(() => { const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms`); }, 100); // 异步任务二:文件读取后,有一个 200ms 的回调函数 fs.readFile('test.js', () => { const startCallback = Date.now(); while (Date.now() - startCallback < 200) { // 什么也不做 } });
The above code has two asynchronous tasks, one is a timer to be executed after 100ms, the other is file reading, and its callback function takes 200ms. What is the result of the operation?
After the script enters the first round of event loop, there is no expired timer, and there is no I/O callback function that can be executed, so it will enter the Poll stage and wait for the kernel to return the result of file reading. Since reading small files generally does not exceed 100ms, before the timer expires, the Poll stage will get the result, so it will continue to execute.
In the second round of the event loop, there is still no expired timer, but there is already an I/O callback function that can be executed, so it will enter the I/O callbacks stage and execute
fs.readFile
the callback function.
This callback function takes 200ms, that is to say, the 100ms timer will expire when it is halfway through.
However, you must wait until the callback function is executed before leaving this stage.
In the third round of event loop, there is already an expired timer, so the timer will be executed in the timers phase. The final output is about 200 milliseconds.
Eight, setTimeout and setImmediate
Because it is
setTimeout
executed in the timers phase, it is executed
setImmediate
in the check phase.
Therefore, it
setTimeout
will be
setImmediate
completed
earlier
.
setTimeout(() => console.log(1)); setImmediate(() => console.log(2));
The above code should be output first
1
, then output
2
, but when it is actually executed, the result is uncertain, and sometimes it will output first
2
and then output
1
.
This is because
setTimeout
the second parameter defaults to
0
.
But in fact, Node can’t do 0 milliseconds, and it takes at least 1 millisecond. According to
official documents
, the value of the second parameter ranges from 1 millisecond to 2147483647 milliseconds.
In other words, it is
setTimeout(f, 0)
equivalent to
setTimeout(f, 1)
.
In actual execution, after entering the event loop, it may be 1 millisecond or less than 1 millisecond, depending on the current situation of the system.
If it is less than 1 millisecond, the timers phase will be skipped, and
setImmediate
the callback function will
be executed first in the check phase
.
However, the following code must first output 2 and then output 1.
const fs = require('fs'); fs.readFile('test.js', () => { setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); });
The above code will first enter the I/O callbacks phase, then the check phase, and finally the timers phase.
Therefore, it
setImmediate
will be
setTimeout
implemented
earlier
.
Nine, reference link
- The Node.js Event Loop, Timers, and process.nextTick() , by Node.js
- Handling IO – NodeJS Event Loop , by Deepal Jayasekara
- setImmediate() vs nextTick() vs setTimeout(fn,0)-in depth explanation , by Paul Shan
- Node.js event loop workflow & lifecycle in low level , by Paul Shan
(over)