EventEmitter 事件驱动
本教程共 76 篇 · 第 16 篇 · 更新于 2026-07-25 · 约 5 分钟阅读
16. EventEmitter 事件驱动
本节目标:EventEmitter 的用法、事件驱动模式和内存泄漏防范。
Node.js 的异步世界有两套话语体系:一套是 Promise / async-await,处理“一次性的异步结果”;另一套是 EventEmitter,处理“持续发生的事件流”。
如果你写过浏览器的 element.addEventListener('click', ...),EventEmitter 就是它在服务端的亲兄弟。node:events 模块里的 EventEmitter 类,构成了 Node.js 事件驱动架构的基石。
基础用法:订阅与发布
import EventEmitter from 'node:events';
const emitter = new EventEmitter();
// 订阅(监听)事件
emitter.on('message', (data) => {
console.log('收到:', data);
});
// 发布(触发)事件
emitter.emit('message', 'hello world');
on 注册监听器,emit 触发事件。事件名是字符串,可以带任意数量的参数传给监听器。
同一个事件可以挂多个监听器,按注册顺序依次执行:
emitter.on('order', () => console.log('A'));
emitter.on('order', () => console.log('B'));
emitter.emit('order');
// A
// B
once:只监听一次
有些事件你只想处理一次,比如数据库连接成功:
emitter.once('connect', () => {
console.log('数据库已连接,这句只输出一次');
});
emitter.emit('connect');
emitter.emit('connect'); // 第二次不会触发
once 在触发后自动移除自己,省得你手动 off。
移除监听器
off(或旧的 removeListener)可以卸掉指定监听器。要卸掉,你得保留监听器的引用:
function onData(data) {
console.log(data);
}
emitter.on('data', onData);
// ... 某个时刻
emitter.off('data', onData);
如果想清空某个事件的所有监听器:
emitter.removeAllListeners('data');
或者清空全部:
emitter.removeAllListeners();
Tip内存泄漏的常见原因,就是事件监听器只增不减。尤其是长时间运行的服务里,给请求对象绑了
on('error', ...)却从不释放,堆里的闭包越积越多。养成off的习惯,或者在设计时就考虑事件生命周期。
error 事件:不能忽视的特殊事件
EventEmitter 规定,error 事件如果没有监听器,触发时会抛出异常。如果进程也没捕获这个异常,Node.js 进程会直接退出。
const emitter = new EventEmitter();
emitter.emit('error', new Error('boom'));
// 进程崩溃:Unhandled 'error' event
所以,任何可能 emit error 的 EventEmitter 实例,都应该挂上监听器:
emitter.on('error', (err) => {
console.error('捕获到错误:', err.message);
});
这也解释了为什么流(Stream)和 HTTP 服务器一定要处理 error 事件——不然一个网络抖动就能让你的服务重启。
继承 EventEmitter
实际开发中,很少直接 new EventEmitter(),而是让自己的类继承它:
import EventEmitter from 'node:events';
class MyServer extends EventEmitter {
constructor(port) {
super();
this.port = port;
}
start() {
console.log(`监听端口 ${this.port}`);
this.emit('started', this.port);
}
}
const server = new MyServer(3000);
server.on('started', (port) => {
console.log(`服务已在 ${port} 启动`);
});
server.start();
Node.js 核心模块里,http.Server、net.Server、fs.ReadStream 等等,都是 EventEmitter 的子类。所以你才能写 server.on('request', ...)、stream.on('data', ...)。
监听器数量上限
默认情况下,同一个事件超过 10 个监听器,EventEmitter 会在控制台打印警告:
MaxListenersExceededWarning: Possible EventEmitter memory leak detected
这不是硬限制,只是提醒你可能在泄漏。如果你确实需要更多监听器,可以改:
emitter.setMaxListeners(20);
或者设为 Infinity(不推荐,除非你清楚在做什么)。
一个完整示例:简单的事件总线
有时候模块之间需要松耦合通信,可以搭一个轻量的事件总线:
import EventEmitter from 'node:events';
class EventBus extends EventEmitter {
constructor() {
super();
this.setMaxListeners(100);
}
}
const bus = new EventBus();
// 模块 A:监听用户登录
bus.on('user:login', (user) => {
console.log(`[日志] ${user.name} 登录了`);
});
// 模块 B:监听用户登录,发通知
bus.on('user:login', (user) => {
console.log(`[通知] 欢迎 ${user.name}`);
});
// 某处登录逻辑
function handleLogin(user) {
bus.emit('user:login', user);
}
handleLogin({ name: 'Alice' });
// [日志] Alice 登录了
// [通知] 欢迎 Alice
这比直接函数调用解耦得多,但也要小心:事件总线让数据流变得隐式,调试时不好追踪。中大型项目里,不妨结合 TypeScript 给事件名和载荷加上类型约束。
EventEmitter 与 Promise 的边界
EventEmitter 适合“零个或多个”的事件序列;Promise 适合“零个或一个”的异步结果。两者可以结合:
import { once } from 'node:events';
// 把一次性事件转成 Promise
const [data] = await once(emitter, 'data');
console.log(data);
node:events 的 once 函数(注意是小写,和 emitter.once 不同)返回一个 Promise,在事件触发时 resolve。这在 async/await 代码里很顺手。
Note
events.once自 Node.js v11.13.0 起可用,v24 LTS 里当然稳定。如果事件名是error,它会 reject Promise,天然支持try/catch。