Node.js 内置 events 模块足够大多数单进程场景,核心在于管理监听器生命周期与异常安全,避免随意全局挂载导致内存泄漏。
核心结论:使用单例模式封装 EventEmitter 实现事件总线,重点在于监听器的注册与销毁配对,以及异步监听器内的异常捕获。
- 适用场景:单进程内部模块间松耦合通信,如订单创建后通知邮件、库存服务
- 关键注意:emit 默认同步执行,异步监听器需处理 Promise rejection 避免未捕获异常,如需等待监听器完成需自行封装
- 最佳实践:业务逻辑结束后手动移除监听,避免长生命周期对象持有不再需要的回调
单例封装与配置
不要直接在每个文件 new EventEmitter,建议封装为单例。创建 src/utils/event-bus.js,支持通过环境变量调整监听器上限:
const { EventEmitter } = require('events');
class EventBus extends EventEmitter {
constructor() {
super();
// 避免默认 10 个限制触发警告,根据业务规模通过环境变量配置
const maxListeners = process.env.MAX_EVENT_LISTENERS || 20;
this.setMaxListeners(parseInt(maxListeners, 10));
}
}
module.exports = new EventBus();其他模块直接引入即可,确保全局只有一个事件总线实例,避免事件隔离。
异步监听器异常处理
EventEmitter 的 emit 是同步的,但监听器内部可能是异步操作。若异步操作抛出错误且未捕获,可能导致进程不稳定。
1. 基础异常捕获
监听器内部务必包裹 try/catch,防止单个监听器失败影响其他监听器:
const bus = require('../utils/event-bus');
const { ORDER_CREATED } = require('../events/event-types');
bus.on(ORDER_CREATED, async (payload) => {
try {
await sendEmail(payload.userId, 'Order Created');
} catch (err) {
console.error('Send email failed:', err);
// 此处异常不会阻断其他监听器,但需记录日志
}
});2. 实现异步 Emit(如需等待监听器完成)
若业务需要等待所有异步监听器执行完毕,需封装 emitAsync:
async function emitAsync(event, data) {
const listeners = bus.listeners(event);
const promises = listeners.map(async (listener) => {
try {
await listener(data);
} catch (err) {
console.error(`Listener error for ${event}:`, err);
}
});
await Promise.all(promises);
}内存泄漏预防与清理
事件泄漏指的是当事件监听器不再需要时未被正确移除,导致它们持续存在于内存中。常见场景包括单页应用页面切换时未移除旧监听器、动态组件销毁时未清理绑定。
1. 使用具名函数
避免使用匿名函数,以便移除:
function handleOrder(data) { /* ... */ }
// 添加
bus.on('order:created', handleOrder);
// 移除
bus.off('order:created', handleOrder);2. 避免循环重复绑定
避免在高频函数或循环中重复添加监听器,除非明确知道需要累积监听。
验证与监控
1. 检查警告日志
运行项目时观察控制台,若出现 (node) warning: possible EventEmitter memory leak detected 警告,说明同一事件监听器超过设定阈值,需检查是否重复绑定或未清理。
2. 监听器数量检查
调试时可使用 bus.listenerCount(eventName) 查看特定事件的监听器数量,确认在预期范围内。
3. 内存监控
在长期运行的进程中,通过 process.memoryUsage() 观察堆内存变化。若内存持续攀升且无业务增长,可能存在事件泄漏。