Node.js 是单线程的 JavaScript 运行时,但它能高效处理大量并发 I/O 操作(如网络请求、文件读写),核心机制就是事件循环 。事件循环由底层库 libuv 实现,它允许 Node.js 在不阻塞主线程的情况下处理异步任务。
为什么需要事件循环?
- JavaScript 是单线程的,同步代码会阻塞执行。
- 但大多数操作(如网络 I/O)是耗时的,如果阻塞主线程,程序就无法响应其他请求。
- Node.js 将异步操作"卸载"到系统内核或线程池,完成后通过回调通知主线程。
- 事件循环负责不断检查是否有完成的异步任务,并执行对应的回调函数,从而实现非阻塞 I/O。
简单来说:主线程执行同步代码 → 遇到异步操作 → 交给 libuv 处理 → libuv 完成时将回调放入队列 → 事件循环轮询队列并执行回调。
事件循环的阶段(Phases)
Node.js 的事件循环分为 6 个主要阶段(基于 libuv),它们按顺序循环执行。每轮循环(称为一个 "tick")会依次进入这些阶段:
-
timers(定时器阶段)
执行已到期的
setTimeout()和setInterval()回调。注意:定时器不是精确的,只保证"至少"在指定时间后执行(可能因其他任务延迟)。
-
pending callbacks(待定回调阶段)
执行某些系统级 I/O 回调(如 TCP 错误报告)。
-
idle, prepare(闲置/准备阶段)
Node.js 内部使用,仅用于准备下一个阶段。
-
poll(轮询阶段)
最重要、最复杂的阶段:
- 检索新的 I/O 事件(网络、文件等)。
- 执行 I/O 相关的回调(几乎所有异步 I/O 回调在这里处理)。
- 如果没有定时器,会在这里阻塞等待新事件到来。
- 如果有
setImmediate(),会尽快进入 check 阶段。
-
check(检查阶段)
执行
setImmediate()回调。 -
close callbacks(关闭回调阶段)
执行关闭事件的回调(如 socket.close())。
循环流程简图:
sql
timers → pending callbacks → idle/prepare → poll → check → close callbacks → (返回 timers)
每轮循环结束后,Node.js 会检查是否还有待处理的异步任务。如果没有,进程会优雅退出。
微任务(Microtasks)和 nextTick 的特殊处理
- 与浏览器不同,Node.js 的微任务 (如
Promise.then()、queueMicrotask())和process.nextTick()在每个阶段结束后、进入下一个阶段前执行。 - 优先级:
process.nextTick()>Promise(微任务队列)。 - 这意味着微任务会"插队"在宏任务(阶段回调)之间执行,确保更高优先级。
注意变化(从 Node.js 20+ / libuv 1.45.0 开始):定时器回调现在只在 poll 阶段后执行(以前可能在 poll 前后都检查)。
执行顺序示例
考虑以下代码:
javascript
console.log('start');
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
console.log('end');
输出通常是:
arduino
start
end
nextTick
promise
immediate // 或 timeout(取决于 poll 阶段)
timeout // 或 immediate
- 同步代码先执行:
start→end。 - 微任务立即执行:
nextTick→promise。 - 然后进入事件循环:先 timers(timeout),或先 check(immediate),具体取决于 poll 阶段的行为。
与浏览器事件循环的区别
- 浏览器:宏任务(macrotask,如 setTimeout)和微任务(microtask,如 Promise)交替执行,一个宏任务后清空所有微任务。
- Node.js :有多个阶段的宏任务队列,微任务在每个阶段之间执行,更复杂,适合服务器端 I/O 密集场景。
实际建议
- 避免在事件循环中执行 CPU 密集任务(如大循环计算),会阻塞其他回调,导致延迟。
- 使用
process.nextTick()或 Promise 处理高优先级异步逻辑。 - 监控事件循环延迟(可用
perf_hooks)以优化性能。
理解事件循环是掌握 Node.js 异步编程的关键,能帮助你调试回调顺序、避免阻塞和构建高并发应用。