Node.js 事件循环阶段
解释 Node.js 事件循环的阶段顺序、各阶段职责,以及 timers、poll、check 和微任务的配合方式。
[!info] related notes
- 所属 MOC: Node.js MOC, javascript-in-nodejs-moc
- 前置概念: Node.js 运行时架构, libuv, JavaScript事件循环
- 并列概念: JS 中的事件循环
- 关系笔记: libuv 事件循环与 Worker Pool, process.nextTick、Promise、setImmediate 与 setTimeout 的关系
- 面试问法: Event Loop、宏任务和微任务怎么理解
Node.js 事件循环阶段
一句话定义
Node.js 事件循环不是只分“宏任务和微任务”,而是由 libuv 组织成多个阶段,再配合 process.nextTick 和 Promise 微任务共同完成调度。
如果要继续追到底层“poll 为什么重要、为什么文件 I/O 常走线程池、loop 为什么退出”,继续看 libuv 事件循环与 Worker Pool。
核心机制 / 工作原理
经典阶段顺序可以先记成:
timers
↓
pending callbacks
↓
idle, prepare
↓
poll
↓
check
↓
close callbacks
timers
- 处理到期的
setTimeout/setInterval
pending callbacks
- 处理某些延后到下一轮的系统级回调
poll
- 等待并处理大部分 I/O 回调
- 决定这一轮是继续取 I/O,还是转去
check
这一步是最容易被忽略的核心:
- 如果此时已经有到期 timer,循环不会一直卡在
poll - 如果没有新的 I/O,但存在
setImmediate,也会更快转去check - 如果两边都没有,就可能在这里等待新的 I/O 事件
check
- 处理
setImmediate
close callbacks
- 处理连接关闭等收尾回调
这个阶段模型和 libuv 的关系
这篇更偏 Node.js 用户可见的阶段视图。
如果想理解下面这些更底层问题:
- loop alive 怎么判断
- poll timeout 怎么算
- 网络 I/O 和文件 I/O 为什么路径不同
- Worker Pool 和
worker_threads有什么边界
就要切到 libuv 事件循环与 Worker Pool。
为什么 Node.js 不适合只记“宏任务 / 微任务”
因为那种模型只能解释一半问题。
在 Node.js 面试里更高频的追问其实是:
- 为什么
setImmediate和setTimeout(fn, 0)顺序不稳定 - 为什么 I/O 回调里
setImmediate更常先执行 - 为什么
process.nextTick会饿死事件循环
这些都需要回到“阶段模型”才能讲稳。
最小例子 / 最小场景
如果只看顶层主模块:
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
这里 timeout 和 immediate 的先后不应该背死。
但如果它们出现在 I/O 回调里:
import fs from 'node:fs';
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
通常更容易先看到 immediate,因为 I/O 回调刚从 poll 阶段出来,下一步就更接近 check 阶段。
边界与易混淆点
微任务不属于上面这些阶段之一
Node.js 里还要额外记住两类高优先级队列:
process.nextTick- Promise 微任务
通常可先用这个心智模型:
同步代码
↓
process.nextTick
↓
Promise 微任务
↓
再继续事件循环阶段
process.nextTick 比 Promise 还高
这也是为什么下面这段代码:
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
通常更容易看到:
nextTick
promise
poll 不是“固定立刻结束的一步”
它既负责取 I/O,也决定事件循环接下来是不是该等、该跳还是该继续推进。
所以很多“为什么这个回调这次先执行”的题,本质都绕不开 poll。
饿死事件循环是什么意思
如果 process.nextTick 递归不断塞任务,事件循环就可能一直没机会进入后面的阶段:
function spin() {
process.nextTick(spin);
}
spin();
这时:
- timer 可能迟迟不执行
- I/O 回调可能迟迟得不到处理
- 整个服务看起来就像“活着但不干活”
不要把浏览器模型原封不动搬到 Node.js
浏览器常用“宏任务 / 微任务 / 渲染”心智模型。
Node.js 更适合记成:
多阶段事件循环 + nextTick + Promise 微任务。
最小面试回答模板
如果面试官问“Node.js 事件循环怎么理解”,可以先答:
Node.js 事件循环是 libuv 驱动的多阶段调度模型,不只是宏任务和微任务。主线程先执行同步代码,之后还要考虑
process.nextTick、Promise 微任务,以及timers、poll、check等阶段。像setTimeout属于timers,setImmediate属于check,所以它们的先后顺序要结合当前是不是刚处理完 I/O 来看。