You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js事件循环3组实验结果不符预期,请求原因解析

Node.js事件循环实验结果解析

完成小型功能组件开发后,我针对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阶段。

执行流程:

  1. IO回调在poll阶段执行,触发setImmediate(进入check队列)、setTimeout(进入timers队列)、主动关闭stream(回调进入poll队列),随后阻塞1秒使定时器到期。
  2. poll阶段结束后,优先进入check阶段执行setImmediate回调(输出checker)。
  3. check阶段结束后,事件循环回到下一轮的timers阶段,执行setTimeout回调(输出timeout)。
  4. 最后进入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阶段。

执行流程:

  1. 主动关闭stream的回调在poll阶段执行,触发setTimeout(进入timers队列)、setImmediate(进入check队列)、readFile(进入poll队列),阻塞1秒使定时器到期。
  2. poll阶段结束后,发现存在setImmediate回调,进入check阶段执行(输出checker)。
  3. check阶段结束后,回到timers阶段执行setTimeout回调(输出timeout)。
  4. 最后进入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阶段。

执行流程:

  1. setImmediate回调在check阶段执行,触发主动关闭stream(回调进入poll队列)、readFile(进入poll队列)、setTimeout(进入timers队列),阻塞1秒使定时器到期。
  2. check阶段结束后,事件循环回到timers阶段执行setTimeout回调(输出timeout)。
  3. 随后进入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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 10:19:55