Chrome and the Node Event Loop
After using JavaScript for so long, I realized that I have always misunderstood the event loop mechanism of JavaScript.
First, the event loop mechanism is not implemented in V8. The V8 engine is only responsible for compiling JavaScript code, memory allocation, etc.
Second, Chrome event loop mechanism is implemented through the web API, while Node is libuv.
Third, before Node11, the principle of Node’s event loop was different from Chrome.
In response to these misunderstandings, I relearned the event loop of JavaScript.
Javascript is a single-threaded, non-blocking, asynchronous, interpreted scripting language. The concurrency model of js is based on the event loop, which is implemented by the js host environment such as the browser. v8 is the javascript runtime environment in the Chrome. There is no setTimeout/DOM/HTTP requests in the V8 source code. These asynchronous requests are handled by the webAPI in the browser, which is a thread created by the C++ implemented browser.
Chrome Browser with Node11 +
Browser
The following is a flowchart of the event loop mechanism in the browser. As long as there is no code executing in the execution stack, the microtask will be executed immediately after the callback.
Node11+
You can read this on my other blog: https://sunra.top/posts/5f68736a/
Node10- (Take Node8 as an example)
1. Introduction to Node
The Event Loop in Node is completely different from that in the browser. Node.js uses V8 as the parsing engine of js, and uses its own libuv for I/O processing. Libuv is an event-driven cross-platform abstraction layer that encapsulates some underlying features of different operating systems and provides a unified API to the outside world. The event loop mechanism is also implemented in it (described in detail below).
The operating mechanism of Node.js is as follows:
- V8 engine parses JavaScript scripts.
- After parsing the code, call the Node API.
- The libuv library is responsible for the execution of the Node API. It assigns different tasks to different threads, forming an Event Loop that asynchronously returns the execution results of the tasks to the V8 engine.
- The V8 engine then returns the results to the user.
2. Six stages
The event loop in the libuv engine is divided into 6 stages, which will be run repeatedly in order. Whenever you enter a certain stage, the function will be removed from the corresponding callback queue to execute. When the queue is empty or the number of callback functions executed reaches the threshold set by the system, it will enter the next stage.
In the above figure, you can roughly see the order of the event loop in node.
External input data – > polling phase (poll) – > check phase (check) – > close callback phase (close callback) – > timer detection phase (timer) – > I/O event callback phase (I/O callbacks) – > idle phase (idle, prepare) – > polling phase (run repeatedly in this order)…
- timers phase: This phase executes callbacks for timers (setTimeout, setInterval)
- I/O callbacks phase: handles a few unexecuted I/O callbacks from some previous loop
- idle, prepare phase: only used internally by nodes
- poll phase: fetch new I/O events, where node will block under appropriate conditions
- check phase: Callback for executing setImmediate ()
- close callbacks stage: perform the close event callback of the socket
Note: ** None of the above six stages include process.nextTick () ** (described below)
Next, we will introduce the three stages of’timers’, ‘poll’, and’check 'in detail, because most of the Asynchronous Tasks in daily development are processed in these three stages.
(1)
The timers phase executes setTimeout and setInterval callbacks and is controlled by the poll phase. Similarly, the time specified by the timer in Node is not an exact time and can only be executed as soon as possible.
(2)
Polling is a crucial phase, and in this phase, the system does two things
Go back to the timer phase and execute the callback
Perform I/O callbacks
And if the timer is not set when entering this stage, the following two things will happen
- If the poll queue is not empty, the callback queue is traversed and executed synchronously until the queue is empty or the system limit is reached
If the poll queue is empty, two things happen- If there is a setImmediate callback that needs to be executed, the poll phase will stop and enter the check phase to execute the callback
- If there is no setImmediate callback to execute, it will wait for the callback to be added to the queue and execute the callback immediately. There will also be a timeout setting to prevent waiting forever
Of course, if the timer is set and the poll queue is empty, it will determine whether there is a timer timeout, and if so, it will return to the timer stage to perform the callback.
(3)
The callback of setImmediate () will be added to the check queue. As you can see from the phase diagram of the event loop, the execution order of the check phase is after the poll phase. Let’s take a look at an example first:
1 | console.log('start') |
- After the synchronization task (which belongs to the macro task) that starts the stack is executed (print start end in turn, and put 2 timers in the timer queue in turn), the microtask will be executed first (** This is the same as the browser side), so print promise3
- Then enter the timers stage, execute the callback function of timer1, print timer1, and put the promise.then callback into the microtask queue, execute timer2 in the same step, print timer2; this is quite different from the browser side, ** timers stage There are several setTimeout/setInterval will be executed in turn, unlike the browser side, which executes a microtask after each macro task (the difference between Node and the browser’s Event Loop will be described in detail below).
3.Micro-Task
The asynchronous queues in the Node side event loop are also of these two types: macro (macro task) queues and micro (micro task) queues.
- Common macro-tasks such as: setTimeout, setInterval, setImmediate, script (overall code), I/O operations, etc.
- Common micro-tasks such as: process.nextTick, new Promise ().then (callback), etc.
Step 4 Pay attention
(1)
The two are very similar, the difference is mainly in the timing of the call.
- setImmediate design is executed when the poll stage is completed, that is, the check stage;
- setTimeout is designed to be executed when the poll phase is idle and the set time is reached, but it is executed in the timer phase
1 | setTimeout(function timeout () { |
- For the above code, setTimeout may be executed before or after.
- first setTimeout (fn, 0) = setTimeout(fn, 1),这是由源码决定的 进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调
- If the preparation time is less than 1ms, then the setImmediate callback is executed first
However, when both are called inside the asynchronous i/o callback, setImmediate is always executed first, followed by setTimeout.
1 | const fs = require('fs') |
In the above code, setImmediate is always executed first. Because the two codes are written in the IO callback, the IO callback is executed in the poll stage. When the callback is executed, the queue is empty, and it is found that there is a setImmediate callback, so it jumps directly to the check stage to execute the callback.
(2)
This function is actually independent of the Event Loop. It has its own queue. When each stage is completed, if there is a nextTick queue, all callback functions in the queue will be cleared and executed before other microtasks.
1 | setTimeout(() => { |
5. Comparison with browsers
1 | setTimeout(()=>{ |
Browser side running result: ‘timer1 = > promise1 = > timer2 = > promise2’
The running results on the Node side are divided into two cases:
- If the node11 version executes a macro task (setTimeout, setInterval and setImmediate) in a phase, it immediately executes the microtask queue, which is consistent with the browser run, and the final result is’ timer1 = > promise1 = > timer2 = > promise2 ’
- If it is node10 and previous versions: depends on whether the first timer has finished executing and whether the second timer is in the completion queue.
- If the second timer is not yet in the completion queue, the final result is’ timer1 = > promise1 = > timer2 = > promise2 ’
- If the second timer is already in the completion queue, the final result is’ timer1 = > timer2 = > promise1 = > promise2 ’ (the following procedure is explained based on this case)
Reference link:
https://www.jianshu.com/p/054cb77adadd
https://juejin.cn/post/6844903761949753352
https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/#what-is-the-event-loop