What is an event loop
Event Loop, namely Event Loop, is actually a process of JS management Event execution. The specific management method is determined by the environment in which JS runs. Currently, the main running environment of JS includes browser and Node.
Both browser and Node event loops initialize a loop and execute synchronous code. When an asynchronous operation is performed, it is handed over to the corresponding thread. The main thread continues to execute the asynchronous operation. Each time we execute a loop body, we call it a Tick.
Unlike browsers, Node loops are divided into several phases, each of which processes a different event. Let’s look at the stages of the Node event loop.
The flow of the Node event loop
The six stages of the event cycle
The Node event loop is divided into several phases, as shown below (except for incoming, where each box represents a phase) :
Let’s take a brief look at the function of each stage, i.e. the order in which nodes work within a Tick:
- timers: perform
setTimeout
,setInterval
The callback; - Pending callbacks: The pending callbacks from the previous Tick are executed at this stage.
- Idle, prepare: the Node is used internally.
- Poll: Callback that performs most asynchronous operations;
- check: perform
setImmediate
The callback; - close callbacks: Perform a close related callback, such as
socket.on('close', ...)
Each stage corresponds to a FIFO queue. After the cycle enters a certain stage, only in two cases will it jump out of this stage and enter the next stage. One is to complete the callback in its queue, and the other is that the number of executions reaches the upper limit of this stage.
Poll phase
Let’s focus on the poll phase. The poll phase does two things:
- Calculates the polling time
maxPollTime
; - Performs a callback in the poll queue
The following is the poll process:
- After the event loop enters the poll phase, the poll queue is first checked to see if it is empty.
- If the poll queue is not empty, the callback in the poll queue is iterated and executed synchronously (until all or the upper limit of the number of executions is reached). If the poll queue is empty, the callback in the poll queue is checked for pending executions
setImmediate
The callback. - If so, it enters the check phase, and if not, it waits in place for new callbacks to be pushed into the poll queue and executes those new pushed callbacks immediately (the upper limit of the wait time is calculated previously)
maxPollTime
).
Note: The poll phase is idle, i.e. the poll queue is empty. Once a new setImmediate callback is made, the loop ends the poll phase and goes to the Check phase. Or as soon as a new timer expires, the loop loops back to the Timers phase; SetImmediate has higher precedence if both callbacks occur simultaneously
The execution order of setImmediate and setTimeout(fn, 0)
SetImmediate is a special timer whose callback is performed during the Check phase, immediately following the poll phase. The setTimeout callback is executed as soon as possible after the delay is reached, provided that the timer ends and the callback is pushed into the Timers queue. It is also important to note that in addition to the event loop being able to enter the Timers phase normally, the Poll phase checks for an out-of-timer once it is idle and does not have a setImmediate callback waiting to be executed. If so, it loops back to the Timers phase. So there are actually two times when the setTimeout callback can be executed.
The sequence of setImmediate and setTimeout(fn, 0) callbacks can be broken down into the following two situations:
- If both are called from the main Module, the order in which the callbacks are executed is uncertain;
- Both are not called in the main module, but are called in the callback of an asynchronous operation, then
setImmediate
The callback to setTimeout(fn, 0) is executed before setTimeout
In the following examples, we assume that setImmediate’s callback is CB_IMMEDIATE and setTimeout(fn, 0) is CB_timeout.
Both are in the callback of I/O operations, and cb_IMMEDIATE is executed first
The code is as follows:
// test_timer.js
const fs = require('fs');
fs.readFile(__filename, () = > {
setTimeout(() = > {
console.log('timeout');
}, 0);
setImmediate(() = > {
console.log('immediate');
});
});
Copy the code
“Immediate” is always printed before “timeout” after several runs:
$ node test_timer.js
immediate
timeout
Copy the code
Cb_immediate is always executed first. First, the callback to the I/O operation is in the POLL queue and is performed in the POLL phase. As mentioned earlier, the poll phase, once idle, checks for setImmediate callbacks that are waiting to be executed. If so, the poll phase ends and the wait goes to the Check phase, so even if a new timer expires, The Tick can be executed only after the check and CLOSE end and a new Tick is entered. The poll check finds that cb_IMMEDIATE needs to be executed. Therefore, the cb_IMMEDIATE command is executed in the check phase, and cb_timeout is executed only on the next Tick at the earliest.
Both are in the setTimeout callback, with cb_IMMEDIATE executed first
The code is as follows:
setTimeout(() = > {
setTimeout(() = > {
console.log('timeout');
}, 0);
setImmediate(() = > {1
console.log('immediate');
});
}, 0);
Copy the code
After several runs, we find that “immediate” is always printed before “timeout”. The callback of the outer setTimeout is performed in the Timers phase. After the callback, the event loop continues to the poll phase. Once the poll is free, it checks and finds setImmediate callbacks waiting to be executed. If cb_IMMEDIATE exists, the cycle enters the check phase and cb_IMMEDIATE is executed first.
Combining the two examples above, we can see that setImmediate and setTimeout(FN, 0) are called simultaneously in the callback of the same asynchronous operation. SetImmediate always executes first because once the timer misses the Timers phase, The next time a callback is executed is in the Poll phase, and the Poll phase checks that setImmediate takes precedence over setTimeout. Once a setImmediate callback is found waiting to be executed, the loop continues down. So the setImmediate callback is always executed before the setTimeout callback.
Let’s then look at what happens when setImmediate and setTimeout(fn, 0) are called simultaneously in the main module.
Both are called in the main module in an indeterminate order of execution
The code is as follows:
setTimeout(() = > {
console.log('timeout');
}, 0);
setImmediate(() = > {1
console.log('immediate');
});
Copy the code
Running the above code, we can see that “timeout” is printed first, and “immediate” is printed first, meaning that the order of execution is uncertain.
Let’s take a look at the main process:
- The main thread executes synchronization code;
- will
setTimeout
To the timer thread processing; - perform
setImmediate
, that is, call an API provided by Libuv, which willcb_immediate
Put into the queue of the check phase; - After the synchronization code is executed, the event loop enters the Timers phase.
- Check the Timers queue. If it is not empty, perform a callback (i.e
cb_timeout
), if no, proceed; - Enter the pending Callbacks phase.
- The system enters idle and prepare.
- Enter the poll stage;
- After the poll is idle, the poll is found to be executed
setImmediate
The callback (i.ecb_immediate
), enter the check phase; - perform
cb_Immediate
The sequence of executing CB_IMMEDIATE and CB_TIMEOUT depends on whether cb_TIMEOUT exists in the Timers queue in step E. If cb_timeout exists, the cb_timeout is executed first. Otherwise, cb_IMMEDIATE is executed first.
In Node, setTimeout(fn, 0) is forced to setTimeout(fn, 1).
When delay is larger than 2147483647 or less than 1, the delay will be set to 1.
If by the time the loop enters the Timers phase, 1ms has passed since the setTimeout execution and cb_TIMEOUT has been pushed into the Timers queue, the loop retrieves cb_TIMEOUT and executes. On the other hand, if cb_TIMEOUT is not in the queue when the cycle enters the Timers phase, cb_TIMEOUT is not executed on the Tick and CB_IMMEDIATE is executed first.
When the cycle enters the Timers stage, whether it has passed 1ms will be affected in many ways, including the time taken to synchronize the code execution and the performance of the system. Machine state differences will also result in different results for each run. So setImmediate and setTimeout(fn, 0) are called simultaneously in the main module, and the order in which the two callbacks are executed is uncertain.
process.nextTick()
The timing of the process.nexttick () callback
You may notice that process.nexttick () does not appear at any of the stages we introduced in the event loop. This is because process.nexttick () does not belong to any phase; in fact, it is executed “between phases.” When process.nexttick () is called at any stage, the nextTick callback will be executed immediately after the current stage. After all the callbacks are executed, the event loop will move to the next stage, so too many or too many nextTick callbacks can block the entire event loop. So be careful with nextTick.
process.nextTick() VS setImmediate()
To review the timing of both callbacks, process.Nexttick () ‘s callback is executed immediately after the current phase ends, and setImmediate()’ s callback is executed during the Check phase of each loop. Careful readers will notice that Process.nexttick () seems more “immediate” and more immediate than setImmediate(). In fact official also noted that both name is more reasonable to should be the other way around, but modify the name, influence range is too big, all using these two methods on the NPM package will be affected, so even though there were some confusing names of the two, but for now, name is unrealistic and impossible.
Node EventLoop VS Browser EventLoop
There are two differences between the two:
- Node’s event loop is staged, with each stage executing a specific event callback, whereas browsers have no phases;
- Node microtasks are executed between phases, whereas browser microtasks are executed after each macro task
Like a browser, Node maintains a list of microtasks. Unlike the browser, the browser checks whether the list of microtasks corresponding to the macro task is empty when the macro task is about to end. If it is not empty, all the microtasks are executed and the next Tick is displayed. The timing of Node microtask execution is between each stage. After the end of a stage, the event loop will check whether the list of microtasks is empty. If not, all the microtasks will be executed before the next stage of the cycle is entered. Note that setImmediate is a Node specific macro task and Process. nextTick is a Node specific microtask.