Node.js事件循环3组实验结果不符预期,请求原因解析
完成小型功能组件开发后,我针对Node.js事件循环开展了测试,但实验1、2、3的实际输出与预期不符,现寻求原因解析。
我参考了Node.js事件循环教程系列及事件循环阶段图示,编写了以下实验用函数库:
//functional programmed library for testing eventloop. import { readFile, createReadStream } from "fs"; type callback = () => void; function logger(txt: string) { console.log(txt); } function sleep(time?: number) { const start = Date.now(); while (Date.now() - start < (time ? time : 500)) { continue; } console.log("wake up! ⏰"); } function io(cb: callback): void { readFile(__filename, cb); }; function tick(cb: callback): void { process.nextTick(cb); } function promise(cb: callback): void { Promise.resolve().then(cb); } function close(cb: callback): void { const stream = createReadStream(__filename); stream.close(); stream.on("close", cb); } function timeout(cb: callback): void { setTimeout(cb, 0); } function checker(cb: callback): void { setImmediate(cb); } function bounce(set: Set<string>, source: string): () => void { return () => { console.log(`______${source}_______`); if (set.has("checker")) checker(() => { logger("checker"); }); if (set.has("timeout")) timeout(() => { logger("timeout"); }); if (set.has("close")) close(() => { logger("close"); }); if (set.has("tick")) tick(() => { logger("tick"); }); if (set.has("promise")) promise(() => { logger("promise"); }); if (set.has("io")) io(() => { logger("io"); }); sleep(1000); }; }
声明:所有实验均为独立单元,通过sleep函数阻塞调用栈1秒,确保所有回调在事件循环启动前被推入。
实验0(基准验证)
代码:
io(bounce(new Set(["checker", "close"]), "io")); timeout(() => logger("timeout")); tick(() => logger("tick")); promise(() => logger("promise"));
输出:
tick promise timeout io checker close
该输出符合事件循环基础顺序:微任务(process.nextTick/Promise)→ timers阶段(setTimeout)→ poll阶段(IO回调)→ check阶段(setImmediate)→ close callbacks阶段。
实验1
代码:
io(bounce(new Set(["checker", "close", "timeout"]), "io"));
预期输出:
______io_______ checker close timeout
实际输出:
______io_______ checker timeout close
解析
预期错误源于对close callbacks阶段的误解:Node.js的close callbacks阶段仅处理被动关闭资源的回调(如TCP连接被远端关闭),而主动调用stream.close()触发的close事件属于文件IO操作范畴,其回调会被放入poll阶段队列,而非close callbacks阶段。
执行流程:
- IO回调在poll阶段执行,触发
setImmediate(进入check队列)、setTimeout(进入timers队列)、主动关闭stream(回调进入poll队列),随后阻塞1秒使定时器到期。 - poll阶段结束后,优先进入check阶段执行
setImmediate回调(输出checker)。 - check阶段结束后,事件循环回到下一轮的timers阶段,执行
setTimeout回调(输出timeout)。 - 最后进入poll阶段,执行主动关闭stream的
close回调(输出close)。
实验2
代码:
close(bounce(new Set(["timeout", "checker", "io"]), "close"));
预期输出:
______close_______ timeout checker io
实际输出:
______close_______ checker timeout io
解析
预期错误在于对poll阶段后续流程的误解:当poll阶段处理完当前回调后,会优先检查是否存在setImmediate回调,若有则直接进入check阶段,而非立即回到timers阶段。
执行流程:
- 主动关闭stream的回调在poll阶段执行,触发
setTimeout(进入timers队列)、setImmediate(进入check队列)、readFile(进入poll队列),阻塞1秒使定时器到期。 - poll阶段结束后,发现存在
setImmediate回调,进入check阶段执行(输出checker)。 - check阶段结束后,回到timers阶段执行
setTimeout回调(输出timeout)。 - 最后进入poll阶段执行
readFile回调(输出io)。
实验3
代码:
checker(bounce(new Set(["close", "io", "timeout"]), "checker"));
预期输出:
______checker_______ close timeout io
实际输出:
______checker_______ timeout close io
解析
核心原因同实验1:主动关闭stream的close回调属于poll阶段,执行顺序晚于下一轮循环的timers阶段。
执行流程:
setImmediate回调在check阶段执行,触发主动关闭stream(回调进入poll队列)、readFile(进入poll队列)、setTimeout(进入timers队列),阻塞1秒使定时器到期。- check阶段结束后,事件循环回到timers阶段执行
setTimeout回调(输出timeout)。 - 随后进入poll阶段,依次执行主动关闭stream的
close回调、readFile回调(输出close、io)。
额外说明
实验中close函数的写法存在隐患:先调用stream.close()再注册close事件回调,可能导致事件触发后才绑定回调,Node.js为兼容会将回调延迟到poll阶段执行。正确写法应先注册回调再关闭:
function close(cb: callback): void { const stream = createReadStream(__filename); stream.on("close", cb); // 先注册回调 stream.close(); // 再执行关闭 }
但即使修正写法,主动关闭的streamclose回调仍属于poll阶段,这是Node.js的设计决定。
内容的提问来源于stack exchange,提问作者Rahat Bin Taleb

