Node.js 连续等待事件:第一个 await 已返回,为什么第二个事件却等不到

前天 3阅读

一个任务先发 ready,再发 done。消费端顺序写两次 await once,看起来严格遵守事件顺序,却在第二次等待处停住。发布端的日志明明显示 done 已经发射,问题是消费端直到第一个 await 恢复后才注册第二个监听;同一轮同步发射可能早已走过那个时刻。

EventEmitter 不是保存历史事件的消息队列,后来注册的监听器不会自动补领之前的通知。下面使用两个全新的发射器对照错误与修复,并主动取消错误分支的等待,避免教学程序永久悬挂。代码保存为 demo.mjs 后用 node demo.mjs 运行。

一次发射回调中,两个事件都可能先完成

Node.js 连续等待事件:第一个 await 已返回,为什么第二个事件却等不到

AI概念示意图:两个铃铛连续响起,接收杯在事件发生前就分别放好。图片不是运行截图。

import assert from "node:assert/strict";
import {EventEmitter, once} from "node:events";

const lateEmitter = new EventEmitter();
process.nextTick(() => {
  lateEmitter.emit("first", 1);
  lateEmitter.emit("second", 2);
});
assert.deepEqual(await once(lateEmitter, "first"), [1]);
const controller = new AbortController();
const late = once(lateEmitter, "second", {signal: controller.signal});
let resolved = false;
late.then(() => { resolved = true; }, () => {});
await new Promise(resolve => setImmediate(resolve));
assert.equal(resolved, false);
controller.abort();
await assert.rejects(late, {name: "AbortError"});
assert.equal(lateEmitter.listenerCount("second"), 0);
console.log("late listener missed second; cancelled cleanly");

const readyEmitter = new EventEmitter();
const first = once(readyEmitter, "first");
const second = once(readyEmitter, "second");
process.nextTick(() => {
  readyEmitter.emit("first", 1);
  readyEmitter.emit("second", 2);
});
const results = await Promise.all([first, second]);
assert.deepEqual(results, [[1], [2]]);
assert.equal(readyEmitter.listenerCount("first"), 0);
assert.equal(readyEmitter.listenerCount("second"), 0);
console.log("registered first:", results);

第一组把 first 和 second 放在同一个 nextTick 回调中连续发射。first 的 Promise 可以变为完成状态,但异步函数不会插入两个 emit 之间立即恢复,发布端会继续发射 second。等消费端从第一个 await 返回并调用第二次 once 时,second 已经过去。

程序通过 setImmediate 让执行走过一次明确边界,再检查迟到等待尚未完成。这里不靠一个很短的毫秒超时赌机器速度;已知只有那一次 second 发射,而它发生在监听注册之前。检查后调用 AbortController 取消等待,并断言得到 AbortError、监听数量回到零。

先把接收入口全部建立,再开始等待

修复组在任何 await 之前,分别调用 once 得到 first 和 second 的 Promise,然后安排发布。两个事件发射时,两个监听入口都已经存在。Promise.all 最终返回 [[1], [2]],外层顺序对应传入的 Promise 顺序,内层数组则保存各个事件的参数。

关键修复点是注册时机,并不是 Promise.all 有能力重放旧事件。如果先让发布端同步执行完,再创建这两个 Promise,仍然会错过。调用可能立即发出结果的启动函数时,应先建立所需监听,再调用启动函数,最后统一等待它们完成。

一次通知不能代替长期状态

若调用方本来就允许晚加入,应考虑暴露当前状态查询,或提供能代表整个操作完成的 Promise,而不只发布一次瞬时事件。事件适合通知变化,状态适合回答现在是什么;把两个角色分开后,晚到的消费者才有明确方式确认工作是否已经完成。

每次 once 只接收之后的一次匹配事件。本例用两个不同事件名,不能据此推断对同一个名字同时注册两次 once 就会分别接到第一条和第二条:它们可能都被同一次发射满足。需要持续消费事件流时,应另外设计队列或迭代协议,并定义积压与停止条件。

失败路径也要结束监听生命周期

实际任务如果只发出了 first,却因错误永远不发 second,提前注册也无法创造缺失通知。调用方仍应按协议处理错误事件、取消或截止条件,确保不再需要的监听被移除。取消等待也只是结束当前消费者的订阅,不能自动取消生产方已经开始的工作。

回归测试至少覆盖同轮发射、分轮发射、缺少第二事件与主动取消。本文只演示 Node 的普通 EventEmitter 和 events.once;浏览器事件或第三方流库可能有不同约定。不要把这组实验扩大为所有调度队列的统一优先级规则,先检查所用发布器与等待接口。

资料核对日期:2026年10月2日(北京时间)。示例在 Node.js 24.19.0 中独立运行。

参考资料

Node.js 官方文档:连续等待多个事件的注意事项

Node.js 官方文档:events.once

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。