Node事件循环
Node 事件循环
当 Node.js 启动后,它会初始化事件循环,处理已提供的输入脚本,它可能会调用一些异步的 API、调度定时器,或者调用 process.nextTick(),然后开始处理事件循环。

事件循环分为六个阶段,每个阶段都有一个 FIFO 队列来执行回调。
通常情况下,当事件循环进入给定的阶段时,将执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段。
阶段概述
- timers:本阶段执行已经被
setTimeout()和setInterval()的调度回调函数。 - pending:系统级回调
- idle, prepare:仅系统内部使用
- poll:执行与 I/O 相关的回调(除了关闭的回调函数,那些由计时器和
setImmediate()调度的之外的几乎所有回调) - check:
setImmediate()回调函数在这里执行。 - close:一些关闭的回调函数,如:
socket.on('close', ...)。
在每次运行的事件循环之间,Node.js 检查它是否在等待任何异步 I/O 或计时器,如果没有的话,则完全关闭。
阶段详情
timers
在 Node 中,计时器分为 Immediate 和 Timeout 两类,这两种计时器都是一个 Node 对象。Timeout 类计时器的回调在该阶段执行。
一旦计时器过期,在下一轮事件循环的 timers 阶段就会调用回调。
Node 中 setInterval 和 setTimeout 都可创建该类计时器,在创建该计时器时, delay 参数是可选的,如果没有提供值或指定的值为 0,那么该参数值默认情况下为 1 毫秒。
和浏览器类似,计时器也是不够准确的,因为不是计时器一过期就会执行,只有到达该阶段才会执行。
pending
一些系统级回调将会在此阶段执行。例如,TCP 套接字在尝试连接时接收到 ECONNREFUSED,则某些 *nix 的系统希望等待报告错误。
poll
I/O 回调在此阶段执行,例如 fs.readFile、http.createServer,而且该阶段是 Node 事件循环中最常呆的阶段。
轮询阶段所做的事:
- 如果该阶段队列不为空,则循环访问回调队列并同步执行它们,直到队列已用尽或者达到了与系统相关的硬性限制
- 如果该阶段的队列为空且其他阶段队列不为空,则该阶段结束
- 如果该阶段的队列为空且其他阶段队列也为空阻塞
1 | const fs = require("fs"); |
check
只有 setImmediate() 回调会在该阶段中执行。这使能够在 poll 阶段变得空闲时立即执行一些代码。
setImmediate 和 setTimeout 的区别:
-
两者所属队列不同
-
setImmediate会立即将回调加入 checks 队列,而setTimeout会开启计时器线程,等待计时器过期 -
setImmediate比setTimeout效率高1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function test(fn, name) {
let i = 0;
console.time(name);
const run = () => {
i++;
if (i < 1000) {
fn(run);
} else {
console.timeEnd(name);
}
};
run();
}
test(setTimeout, "setTimeout");
test(setImmediate, "setImmediate");
// setImmediate: 6.696ms
// setTimeout: 1.698s -
计时器受进程性能的约束,二者的回调运行顺序非确定,取决于系统当时的状况
1
2
3
4
5
6
7setTimeout(() => {
console.log("timeout");
}, 0);
setImmediate(() => {
console.log("immediate");
});
nextTick
process.nextTick() 是异步 API 的一部分,但从技术上讲不是事件循环的一部分。
每次执行一个事件循环中每个阶段的每一个回调之前,必须要清空 nextTick 队列和 microtask 队列。
根据语言规范,Promise 对象的回调函数,会进入异步任务里面的 microtask 队列。
但是在 Node 中微任务队列追加在 process.nextTick 队列的后面,而且只有一个队列清空完毕才会清空另一个。
1 | process.nextTick(() => { |








