先记住这个答案
常见简图包含 timers、pending callbacks、idle/prepare、poll、check 和 close callbacks:分别处理到期定时器、部分延后 I/O 回调、内部准备、I/O 获取与处理、setImmediate 和部分关闭回调。它是简化模型,不是所有任务严格每轮各执行一次的时间表。从 libuv 1.45 开始,Node 20 系列采用定时器主要在 poll 之后运行的调整,分析时应核对实际版本。nextTick 和 Promise 微任务也不是把图中某个阶段简单改名得到的,需要结合回调执行边界理解。
- 按回调来源理解阶段职责
- 阶段图不保证任意代码的绝对顺序
- Node 与 libuv 版本会影响调度细节
阶段职责比机械背诵六个名字更重要
读取文件、网络数据就绪后,需要在适当位置执行 JavaScript 回调;setImmediate 则与 check 阶段关联,定时器关注时间阈值。pending callbacks 处理一部分被延后的 I/O 回调,不能把所有异步操作都归进同一个队列。
close callbacks 只代表部分资源关闭回调,也不是任意名字叫 close 的事件都自动进入该阶段。用户自己调用 EventEmitter.emit 可以同步触发监听器,因此事件名称与底层调度来源需要分开判断。
版本变化会影响图的阅读方式
旧资料常把定时器描述成 poll 前后都检查,Node 官方说明 libuv 1.45 调整后主要在 poll 之后运行定时器。应在回答中注明版本背景,而不是拿旧图作为新运行时所有先后关系的证明。
这不意味着 setImmediate 在任何地方都先于 setTimeout。顶层注册时间、定时器阈值、当前执行位置和启动过程都会影响观察;同一 I/O 回调内注册的对照,与模块顶层的对照也不是同一实验条件。
微任务与长回调会改变实际等待时间
阶段中的 JavaScript 回调仍要运行完才能把执行机会交回调度器。一个长同步循环可以拖延后续 I/O 与定时器,阶段图不会在回调中间自动抢占业务代码,因此阈值已到不代表回调立刻执行。
nextTick 队列和 Promise 微任务在相应执行边界被处理,递归不断安排它们可能让 I/O 长时间得不到机会。分析输出时应先完成当前调用栈,再核对这些队列和所处上下文,不能只按阶段名字排序。
容易答错的地方
- 把阶段图当成每轮精确时间表
- 实际运行受就绪事件、内部限制和版本实现影响,空阶段也不等于必须等待固定时长。应围绕具体注册位置与数据就绪条件推理,再用对应版本实验验证,避免从简图推出过强结论。
- 把所有 close 事件都归到关闭阶段
- 事件名本身不决定调用时机,用户自定义事件可以同步发出,不同内置资源也有各自的关闭流程。应查具体 API 的契约,而不是看到 close 字符串就断言它一定排在某个定时器之后。
面试官还会怎么问?
为什么定时器到期了却还没有执行?
到期只是具备被调度的条件,当前回调、微任务和其他已就绪工作仍可能占用执行机会。应测量事件循环延迟并定位长任务,不能把定时器当作能够抢占 JavaScript 的硬实时机制。
Promise 回调属于 poll 阶段吗?
不能这样归类。Promise 反应通过微任务机制处理,可能由不同阶段的回调安排;其执行还受当前上下文影响。应区分任务的产生来源和微任务的消费边界,避免把所有异步回调都放进 poll。
如何验证面试题的输出而不过度概括?
固定 Node 版本、模块类型和注册位置,运行最小示例并记录实际观察。若文档没有保证唯一顺序,就保留不确定性;多次得到相同输出也不能证明另一种合法顺序永远不会发生。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。