The content of this article is about analyzing the event loop mechanism of Node.js. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.
The event loop mechanism and some related concepts have been introduced in detail in the browser article, but it is mainly for research on the browser side. Is the same for the Node environment? Let’s take a look at a demo first:
setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') })}, 0)setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') })}, 0)
Compile and run it with the naked eye. The result in the browser is as follows. You already know the truth, so I won’t go into details here.
timer1 promise1 timer2 promise2
Then execute it under Node, eh. . . Strange, the running result is different from the browser~
timer1 timer2 promise1 promise2
The example shows that the event loop mechanism of the browser and Node.js is different, let’s take a look~
Event processing of Node.js
Node.js uses V8 as the parsing engine of js, and uses its own designed libuv for I/O processing. libuv is an event-driven cross-platform abstraction layer that encapsulates Some underlying features of different operating systems provide a unified API to the outside world. The event loop mechanism is also implemented in it. Core source code reference:
int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; r = uv__loop_alive(loop); if (!r) uv__update_time(loop); while (r != 0 && loop->stop_flag == 0) { uv__update_time(loop); // timers阶段 uv__run_timers(loop); // I/O callbacks阶段 ran_pending = uv__run_pending(loop); // idle阶段 uv__run_idle(loop); // prepare阶段 uv__run_prepare(loop); timeout = 0; if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv_backend_timeout(loop); // poll阶段 uv__io_poll(loop, timeout); // check阶段 uv__run_check(loop); // close callbacks阶段 uv__run_closing_handles(loop); if (mode == UV_RUN_ONCE) { uv__update_time(loop); uv__run_timers(loop); } r = uv__loop_alive(loop); if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) break; } if (loop->stop_flag != 0) loop->stop_flag = 0; return r; }
According to the official introduction of Node.js, each event loop contains 6 stages. , corresponding to the implementation in the libuv source code, as shown in the figure below
timers stage: This stage executes the callback of timer (setTimeout, setInterval)
I/ O callbacks phase: Execute some system call errors, such as network communication error callback
idle, prepare phase: Only used internally by node
poll phase: Get new I/O events, appropriate Under the conditions, the node will be blocked here
check stage: execute the callback of setImmediate()
close callbacks stage: execute the close event callback of the socket
We focus on timers and poll , Just check these three stages, because most asynchronous tasks in daily development are processed in these three stages.
timers phase
timers is the first phase of the event loop, Node Will check whether there is an expired timer, and if so, push its callback into the timer's task queue to wait for execution. In fact, Node There is no guarantee that the timer will be executed immediately when the preset time is reached, because Node's expiration check of the timer is not necessarily reliable. It will be affected by other running programs on the machine, or the main thread is not idle at that time. For example, in the following code, the execution order of setTimeout() and setImmediate() is uncertain.
setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') })
But if you put them in an I/O callback, setImmediate() must be executed first, because the poll phase is followed by the check phase.
poll phase
The poll phase mainly has two functions:
Processing events in the poll queue
When there is a timer that has timed out, execute its callback Function
even loop will synchronously execute the callbacks in the poll queue until the queue is empty or the executed callbacks reach the system upper limit (the upper limit is unknown), then the even loop will check whether there is a preset setImmediate( ), divided into two situations:
If there is a preset setImmediate(), the event loop will end the poll phase and enter the check phase, and execute the task queue of the check phase
If there is no preset setImmediate(), the event loop will block and wait at this stage
Note one detail, no setImmediate() will cause an event The loop is blocked in the poll phase, so wouldn't the previously set timer not be able to be executed? So, in the poll phase event The loop will have a checking mechanism to check whether the timer queue is empty. If the timer queue is not empty, the event loop starts the next round of event loop, that is, re-enters the timer phase.
check phase
The callback of setImmediate() will be added to the check queue. From the event loop phase diagram, we can know that the execution order of the check phase is after the poll phase.
Summary
Each stage of the event loop has a task queue
When the event loop reaches a certain stage, the task queue of that stage will be executed until the queue is cleared. Or the executed callback reaches the system upper limit before it will move to the next stage
When all stages are executed sequentially once, the event loop is said to have completed a tick
It makes sense. But without the demo, I still don’t understand it completely. I’m anxious, now!
const fs = require('fs')fs.readFile('test.txt', () => { console.log('readFile') setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') }) })
There should be no doubt about the execution results
readFile immediate timeout
Differences between Node.js and the browser’s Event Loop
Recall the previous article, microtask tasks in the browser environment The queue is executed after each macrotask is executed.
In Node.js, microtask will be executed between various stages of the event loop. That is, after a stage is executed, the tasks in the microtask queue will be executed.
demo review
Review the demo at the beginning of the article, the global script (main()) is executed, and the two timers are sequentially Put it into the timer queue, main() is executed, the call stack is idle, and the task queue starts executing;
首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;
至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1、promise2。
对比浏览器端的处理过程:
process.nextTick() VS setImmediate()
In essence, the names should be swapped. process.nextTick() fires more immediately than setImmediate()
来自官方文档有意思的一句话,从语义角度看,setImmediate() 应该比 process.nextTick() 先执行才对,而事实相反,命名是历史原因也很难再变。
process.nextTick() 会在各个事件阶段之间执行,一旦执行,要直到nextTick队列被清空,才会进入到下一个事件阶段,所以如果递归调用 process.nextTick(),会导致出现I/O starving(饥饿)的问题,比如下面例子的readFile已经完成,但它的回调一直无法执行:
const fs = require('fs')const starttime = Date.now()let endtime fs.readFile('text.txt', () => { endtime = Date.now() console.log('finish reading time: ', endtime - starttime)})let index = 0function handler () { if (index++ >= 1000) return console.log(`nextTick ${index}`) process.nextTick(handler) // console.log(`setImmediate ${index}`) // setImmediate(handler)}handler()
process.nextTick()的运行结果:
nextTick 1 nextTick 2 ...... nextTick 999 nextTick 1000 finish reading time: 170
替换成setImmediate(),运行结果:
setImmediate 1 setImmediate 2 finish reading time: 80 ...... setImmediate 999 setImmediate 1000
这是因为嵌套调用的 setImmediate() 回调,被排到了下一次event loop才执行,所以不会出现阻塞。
总结
1、Node.js 的事件循环分为6个阶段
2、浏览器和Node 环境下,microtask 任务队列的执行时机不同
Node.js中,microtask 在事件循环的各个阶段之间执行
浏览器端,microtask 在事件循环的 macrotask 执行完之后执行
3、递归的调用process.nextTick()会导致I/O starving,官方推荐使用setImmediate()
The above is the detailed content of Analyzing the event loop mechanism of Node.js. For more information, please follow other related articles on the PHP Chinese website!