为什么需要事件循环?
Node.js 是单线程运行的,但它却可以处理成千上万个并发连接。它之所以能做到这点,是因为它使用了事件驱动、非阻塞的 I/O 模型。
关键点:
- 单线程负责执行 JavaScript 代码。
- 异步任务 (如定时器、网络请求、文件 I/O)通过回调函数在事件循环中排队执行。
- 不会像传统阻塞式模型那样等待任务完成。
事件循环的核心结构
Node.js 的事件循环是基于 libuv 实现的。libuv 是一个跨平台的异步 I/O 库,为事件循环、线程池、网络等提供支持。 事件循环可以理解为一个不断执行的阶段轮转系统:
js
while (there are events in the event queue) {
execute next event from the queue
}
但在 Node.js 中,事件循环是分阶段执行的,每个阶段都有各自的任务队列。
Node.js 的事件循环大致可以分为以下几个阶段:
timers
:执行setTimeout
和setInterval
的回调。pending callbacks
:执行某些系统操作(如 TCP 错误)的回调。idle, prepare
:内部使用阶段(不暴露给开发者)。poll
:获取新的 I/O 事件,执行与 I/O 相关的回调(如读取文件、网络请求等)。check
:执行setImmediate
的回调。close callbacks
:执行如socket.on('close', ...)
这种回调。
图示:

每次进入一个阶段,Node 会从对应的任务队列中取出所有任务并执行,执行完后进入下一个阶段。
NodeJS事件循环各阶段解读
Timers
阶段
在 Timers 阶段,Node.js 会执行所有已到期的定时器回调(例如 setTimeout()
和 setInterval()
的回调)。如果设置的定时器时间已经到期,这些回调会被推入执行队列,等待下一次事件循环执行。
Pending Callbacks
阶段
处理一些底层 I/O 操作的回调。
Idle、Prepare
阶段
用于内部操作,通常不需要关注。
Poll
阶段
在 Node.js 的事件循环中,Poll 阶段 是一个非常关键的阶段,它负责处理大部分的 I/O 操作 。在事件循环的这个阶段,Node.js 会检查 I/O 操作是否完成并执行相应的回调函数。如果没有待处理的 I/O 操作,Poll
阶段会决定是否需要进入下一阶段,或是继续等待新的事件。
Poll
阶段的主要作用
-
执行 I/O 回调:
- 在
Poll
阶段,Node.js 会处理已经完成的 I/O 操作的回调,比如网络请求的响应、文件读取的完成、数据库查询结果的返回等。 - 如果有挂起的 I/O 操作回调,Node.js 会在这个阶段执行它们。
- 在
-
检查是否有 I/O 事件:
- 如果在
Poll
阶段没有需要执行的回调,Node.js 会检查 I/O 事件队列。如果有新的 I/O 事件(比如文件系统操作、网络操作等),事件循环会继续停留在这个阶段,直到有新的事件被加入队列,当然其它阶段的回调需要被执行时,会再进行一次循环,比如Timers
阶段计时器延时到期,Check
阶段中存在setImmediate回调。
- 如果在
-
决定是否阻塞或继续执行:
- 如果没有 I/O 回调需要执行且没有新的 I/O 事件,
Poll
阶段会决定是否进入 Check 阶段 或者继续等待新的 I/O 事件。默认情况下,Node.js 会选择阻塞在Poll
阶段,直到有新的 I/O 事件。
- 如果没有 I/O 回调需要执行且没有新的 I/O 事件,
-
执行轮询超时:
Poll
阶段还会管理轮询超时行为。这个行为涉及到在没有 I/O 操作完成时,等待新的 I/O 操作的发生。实际上,这个阶段会维持阻塞状态,直到有 I/O 操作被触发,或者Poll
阶段时间到达上限,进入下一个阶段。
Check
阶段
执行 setImmediate
的回调。
close callbacks
阶段
执行如 socket.on('close', ...)
这种回调。
对于上述所有NodeJS事件循环阶段,我们只需要关心Timers
、Poll
、check
三个阶段即可。
微任务队列
在NodeJS事件循环中所有任务队列都属于宏任务,而这些宏任务队列在执行之前都需要清空微任务队列。
微任务队列是什么?
微任务是指 process.nextTick()
和 Promise.then()
产生的任务。Node.js 中有两个微任务队列:
process.nextTick()
队列(优先级高于 Promise)Promise
的.then/.catch/.finally()
回调
举例:
js
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise');
});
process.nextTick(() => {
console.log('nextTick');
});
输出顺序:
bash
nextTick
promise
setTimeout
setImmediate
解释:
- 同步代码完成后,先清空
process.nextTick
队列 → 输出nextTick
- 然后清空
Promise
微任务队列 → 输出promise
- 然后进入
timers
阶段,执行setTimeout
→ 输出setTimeout
总结
行为 | 是否会在每个阶段前清空微任务? |
---|---|
清空 process.nextTick 队列 |
✅ 是的(优先级最高) |
清空 Promise 微任务队列 |
✅ 是的(在 nextTick 之后) |
每个阶段开始前是否清空微任务? | ✅ 是的,确保所有微任务都处理完才进入下一个阶段 |
这也是为什么在 Node.js 中,微任务(尤其是 process.nextTick
)可以"插队"于事件循环的各个阶段之间,被用作高优先级的异步处理机制。
总结
在 Node.js 程序执行时,会从入口函数开始执行同步代码。遇到异步任务时,这些任务会被注册,随后进入事件循环(Event Loop)。事件循环会依次执行各个阶段中的任务队列。
在每个阶段执行完后,Node.js 会立即清空当前的微任务队列(如 Promise.then
和 process.nextTick
)。
其中最核心的阶段是 Poll 阶段:
- 如果有 I/O 回调需要执行,就会执行这些回调;
- 如果没有待执行的回调,但有挂起的 I/O 操作,事件循环会在此阶段 阻塞等待 , 这个阻塞并不是说一直卡在这,而是当前其它阶段有待执行的任务时,会先去执行这些任务,然后回到
Poll
阶段继续阻塞等待; - 如果既没有回调也没有挂起的 I/O,且后续阶段没有待执行的任务(如
setImmediate
),则程序会退出。
常见问题
1. setTimeout
的延时时间在严格意义上来说是不是取不到0?
是的,setTimeout
的延迟时间在严格意义上来说 不能 取到 0 毫秒。虽然你可以传递 0
作为延迟时间,但它并不是立即执行的。原因在于 JavaScript 的事件循环机制和最小时间延迟的限制。
- 事件循环的机制: JavaScript 是单线程执行的,所有的异步操作(如
setTimeout
、Promise
等)都依赖于事件循环。即使你指定0
毫秒延迟,回调函数也不会在当前执行栈中立即执行,而是会被放入事件队列,等待当前任务栈清空后执行。 - 最小延迟: 大多数浏览器(特别是现代浏览器)对
setTimeout
有一个最小延迟限制。即便你传递0
作为延迟,浏览器通常会将延迟时间设置为 4毫秒 或 1毫秒 。这意味着即使你指定0
,回调也不会立即执行,而是最少会等几毫秒才执行。 - 渲染与系统负载: 由于浏览器的渲染机制及系统的负载,实际执行的时间可能比你期望的要稍微延迟一些,尤其是在高负载情况下。
2. setTimeout(0)
和setImmediate
哪个会先执行?
在 Node.js 的事件循环中,setTimeout(0)
和 setImmediate()
的执行顺序不是固定的,取决于调用时的上下文环境。以下是详细解释:
- 主模块中的执行顺序(不确定)
javascript
// 示例代码
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
-
结果可能随机 :可能是
setTimeout
先输出,也可能是setImmediate
先输出。 -
原因:
- 事件循环启动需要时间。如果准备阶段耗时超过 1ms,定时器阈值(1ms)已满足,则先执行
setTimeout
。 - 否则事件循环直接进入
check
阶段,先执行setImmediate
。
- 事件循环启动需要时间。如果准备阶段耗时超过 1ms,定时器阈值(1ms)已满足,则先执行
- I/O 回调中的执行顺序(固定)
javascript
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
});
-
结果固定 :
setImmediate
总是先于setTimeout
。 -
原因:
- I/O 回调在
poll
阶段执行。 setImmediate
会在当前poll
阶段后的check
阶段立刻执行。setTimeout
需等待下一次循环的timers
阶段执行。
- I/O 回调在