Node.js 的 EventEmitter 有哪些需要注意的地方?
EventEmitter 的 emit 会同步、按注册顺序调用监听器;没有 error 监听器时 emit('error') 会直接抛出,可能让进程崩溃;同一事件超过 10 个监听器会打印 MaxListenersExceededWarning,常是泄漏的信号。events.once() 把一次事件变成 Promise,events.on() 把事件流变成异步迭代器,都会在 error 时拒绝或抛出。
简答
Node.js 里的 HTTP 服务、流、子进程,背后都是
EventEmitter:用on注册监听器,用emit触发事件。要先知道一点,emit是同步的,它会按注册顺序把监听器一个个调用完才返回,并不是丢到下一轮去执行。最容易出事的是
error事件。如果一个 emitter 没有error监听器,emit('error')会直接把错误抛出来,没人捕获的话进程就崩了。所以官方建议总是给error事件加监听器。还有监听器上限。同一个事件默认超过 10 个监听器,Node.js 会打印
MaxListenersExceededWarning,提示可能有内存泄漏。这不是硬限制,监听器照样能加,但它通常说明每个请求都在加监听器却没有移除。另外
events模块提供了两个工具函数:once()返回一个 Promise,等到事件触发时兑现;on()返回一个异步迭代器,可以用for await一条条处理事件。两者在等待时遇到error事件都会拒绝或抛出,on()的循环要靠break或AbortSignal结束。
emit 是同步调用监听器的
下面的代码用 Node.js 22.23.3 运行。emit 之前和之后各打印一次,看监听器什么时候执行。
import { EventEmitter } from 'node:events';
const orders = new EventEmitter();
orders.on('created', (id) => console.log('记录日志', id));
orders.once('created', (id) => console.log('只发一次通知', id));
console.log('触发前');
orders.emit('created', 1); // 依次打印:记录日志 1、只发一次通知 1
console.log('触发后');
console.log(orders.emit('created', 2), orders.listenerCount('created')); // 先打印“记录日志 2”,再打印 true 1
输出顺序是“触发前、记录日志 1、只发一次通知 1、触发后”。两个监听器都在 emit 返回之前执行完了,这就是官方文档说的同步、按注册顺序调用。所以监听器里如果有很重的同步计算,会直接阻塞调用 emit 的那段代码;需要异步时,要在监听器里自己用 setImmediate() 或 process.nextTick() 推迟。
once 注册的监听器触发一次就被移除,所以第二次 emit 只剩一个监听器。emit 的返回值表示这个事件有没有监听器,有就是 true。
on、off、once 和 emit 怎样手写,以及 emit 过程中增删监听器会怎样,见《怎样手写一个支持 on、off、once 和 emit 的事件中心?》。
为什么没有 error 监听器会让进程崩溃
error 是一个特殊的事件名。官方文档规定,emitter 没有任何 error 监听器时,触发 error 事件会把错误抛出,打印堆栈后 Node.js 进程退出。
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
try {
emitter.emit('error', new Error('连接断开'));
} catch (err) {
console.log('被抛出了', err.message); // 被抛出了 连接断开
}
emitter.on('error', (err) => console.log('已处理', err.message));
emitter.emit('error', new Error('连接断开')); // 已处理 连接断开
为什么这样设计?因为错误是不能被默默吞掉的。普通事件没人听,emit 返回 false 就算了;错误没人处理,就按异常抛出去,逼开发者正视它。所以用流、socket 这类对象时,一定要监听 error,否则一次网络断开就可能让整个服务退出。
如果只想观察错误、比如上报监控,又不想改变“没人处理就抛出”的行为,可以监听 events.errorMonitor 这个符号。它的监听器先于 error 监听器执行,但不算“处理了错误”,没有真正的 error 监听器时错误照样抛出。
监听器上限是在提醒什么
默认情况下,同一个事件的监听器超过 10 个,Node.js 会往 stderr 打印一条警告:
MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 data listeners added to [EventEmitter]. MaxListeners is 10.
这不是硬限制,第 11 个监听器照样会加上去。它的作用是提醒:常见的错误写法是每来一个请求,就给一个全局的 emitter 加一个监听器,用完不移除,监听器越积越多,它们闭包里引用的数据也释放不掉。
所以看到这个警告,第一反应应该是找出在哪里重复添加了监听器,而不是调大上限。确实需要更多监听器时,用 emitter.setMaxListeners(n) 只改这一个实例;events.defaultMaxListeners 会影响所有实例,包括修改之前创建的,官方文档提醒要谨慎使用。启动时加 --trace-warnings 能看到是哪行代码加的监听器。排查泄漏的完整方法见《Node.js 服务内存泄漏怎样排查?》。
events.once() 和 events.on() 怎样配合 async/await
emitter.once() 是注册回调,而 events 模块的 once() 函数返回一个 Promise,适合“等某个事件发生一次”:
import { EventEmitter, once } from 'node:events';
const server = new EventEmitter();
const ready = once(server, 'ready');
server.emit('ready', 8000);
const [port] = await ready;
console.log(port); // 8000
Promise 兑现的值是一个数组,里面是 emit 时传的所有参数,所以要解构取值。等待期间如果触发了 error,Promise 会被拒绝,这样 try/catch 就能捕获。
on() 函数返回异步迭代器,适合一条条处理持续到来的事件:
import { EventEmitter, on } from 'node:events';
const ticks = new EventEmitter();
const controller = new AbortController();
process.nextTick(() => {
ticks.emit('tick', 'a');
ticks.emit('tick', 'b');
controller.abort();
});
try {
for await (const [value] of on(ticks, 'tick', { signal: controller.signal })) {
console.log('收到', value); // 收到 a、收到 b
}
} catch (err) {
console.log(err.name); // AbortError
}
这个循环不会自己结束,因为事件随时可能再来。结束它有三种办法:循环里 break,传入 signal 后调用 abort()(循环会抛出 AbortError),或者用 close 选项指定某些事件触发时结束。退出循环时它会移除自己加的监听器。
官方文档还提醒,for await 的循环体是一次处理一个事件的,即使里面有 await,下一个事件也要等这一个处理完。需要并发处理时不要用它。
常见追问
EventEmitter 和浏览器的 EventTarget 有什么区别?
EventEmitter的监听器直接收到emit传的参数,同一个函数可以重复注册多次,error事件有“没人监听就抛出”的特殊规则。EventTarget的监听器收到的是一个Event对象,同一个监听器对同一类型只能注册一次,error也没有特殊行为。Node.js 也实现了EventTarget,但没有事件冒泡这类层级传播;events.once()对EventTarget同样适用。
监听器是 async 函数,里面抛错会怎样?
emit不会等待它,官方文档指出,这时抛出的错误会变成未处理的 Promise 拒绝。可以在创建 emitter 时传{ captureRejections: true },这样被拒绝的 Promise 会转成error事件;或者在 async 监听器里自己try/catch。