Skip to content

Node系列 · Node基础:EventEmitter

Node 的事件驱动模型几乎全部基于 EventEmitter:fs.ReadStreamdata 事件、net.Socketconnect 事件、processexit 事件、http.Serverrequest 事件——背后都是它。理解 EventEmitter 的发布订阅机制、最大监听数陷阱和错误事件特殊处理,就理解了 Node 一半的 API 设计模式。

一、EventEmitter 是什么

EventEmitter 是 Node 内置的发布订阅(pub/sub)实现:

  • 发布者调用 emit(eventName, ...args) 触发事件
  • 订阅者调用 on(eventName, handler) 注册回调
  • 事件名是任意字符串,回调按注册顺序同步触发

二、基本 API

2.1 创建与订阅

text
event-demo.js
javascript
const { EventEmitter } = require('node:events');

class MyEmitter extends EventEmitter {}
const bus = new MyEmitter();

// on = addListener 的别名
bus.on('greet', (name) => {
  console.log(`hello, ${name}`);
});

// once:只触发一次后自动移除
bus.once('firstTime', () => {
  console.log('this runs only once');
});

// emit 触发事件
bus.emit('greet', 'Alice');   // 'hello, Alice'
bus.emit('greet', 'Bob');     // 'hello, Bob'
bus.emit('firstTime');        // 'this runs only once'
bus.emit('firstTime');        // (无输出,监听器已自动移除)

2.2 移除监听器

javascript
const handler = (data) => console.log('received:', data);
bus.on('data', handler);

// 移除指定监听器
bus.off('data', handler);          // Node 10+
// 等价老写法:bus.removeListener('data', handler)

// 移除某事件的所有监听器
bus.removeAllListeners('data');

// 不传参:移除所有事件的所有监听器
bus.removeAllListeners();

TIP

offremoveListener 的别名(Node 10+),与浏览器 removeEventListener 命名风格对齐。

2.3 监听器数量

javascript
bus.on('data', () => {});
bus.on('data', () => {});
bus.on('data', () => {});

console.log(bus.listenerCount('data')); // 3
console.log(bus.eventNames());         // ['data']

三、最大监听数限制

EventEmitter 默认限制单个事件最多 10 个监听器。超过会触发警告,但不会崩溃

javascript
const bus = new EventEmitter();

for (let i = 0; i < 12; i++) {
  bus.on('data', () => {});
}

// 输出(stderr):
// (node:1234) MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
// 12 data listeners added to [EventEmitter]. Use emitter.setMaxListeners() to increase limit

3.1 为什么有这限制?

防止"监听器泄漏"——开发时忘记移除监听器,导致内存里堆积越来越多的回调。10 是一个合理上限,超过就该审视设计。

3.2 调整上限

javascript
// 单个实例调整
bus.setMaxListeners(20);

// 全局调整(不推荐,影响所有 EventEmitter)
// EventEmitter.defaultMaxListeners = 20;

// 用 INFINITE 符号表示不限制
const { INFINITE } = require('node:events');
bus.setMaxListeners(INFINITE);

3.3 识别泄漏

javascript
function checkListeners(emitter, eventName, expected) {
  const count = emitter.listenerCount(eventName);
  if (count > expected) {
    console.warn(`warning: ${eventName} has ${count} listeners (expected ${expected})`);
  }
}

四、错误事件(error

error 事件有特殊处理:如果发布 error没有监听器,Node 会抛出未捕获异常并导致进程崩溃

javascript
const bus = new EventEmitter();

bus.emit('error', new Error('boom'));

// 输出:
// Error: boom
//     at ...
// [进程崩溃,exit code 非 0]

DANGER

所有会发出 error 事件的 EventEmitter,必须监听 error。fs 流、http 请求、process 等都是。

javascript
bus.on('error', (err) => {
  console.error('捕获到错误:', err.message);
});

bus.emit('error', new Error('boom')); // 被捕获,进程不退出

如果业务不关心错误细节,可以注册一个空 handler 避免崩溃:

javascript
bus.on('error', () => {}); // 空 handler,仅为防止崩溃

WARNING

空 handler 会静默吞掉所有错误。生产环境至少要 console.error 记录日志。

五、同步还是异步触发?

emit 调用是同步的——所有监听器在 emit 调用栈内执行:

javascript
const bus = new EventEmitter();

bus.on('data', () => console.log('A'));
bus.on('data', () => console.log('B'));
bus.on('data', () => console.log('C'));

console.log('before emit');
bus.emit('data');
console.log('after emit');

// 输出:
// before emit
// A
// B
// C
// after emit

监听器执行顺序:

  1. 按注册顺序同步触发
  2. emit 调用栈内全部跑完才返回
  3. 监听器抛异常会中断后续监听器
javascript
bus.on('data', () => { throw new Error('first'); });
bus.on('data', () => console.log('B'); // 不会被调用
bus.emit('data'); // 异常往上抛

六、典型应用:Node 内置类的继承链

EventEmitter 是 Node 事件模型的根基,很多内置类都继承自它:

这就解释了为什么 process.on('exit', ...)http.Server.on('request', ...)fs.createReadStream().on('data', ...) 都长得一样——本质都是 EventEmitter 的 on / emit

七、自定义类继承 EventEmitter

业务类继承 EventEmitter 就能发布自定义事件。下面的 TaskQueue 在关键节点 emit,订阅方拿异步通知:

javascript
const { EventEmitter } = require('node:events');

class TaskQueue extends EventEmitter {
  constructor() {
    super();
    this.tasks = [];
  }

  add(task) {
    this.tasks.push(task);
    this.emit('added', task);          // 通知订阅者
  }

  process() {
    if (this.tasks.length === 0) {
      this.emit('empty');
      return;
    }
    const task = this.tasks.shift();
    this.emit('processing', task);
    // ... 实际处理逻辑
    this.emit('processed', task);
  }
}

const queue = new TaskQueue();
queue.on('added', (task) => console.log('+ added:', task));
queue.on('processed', (task) => console.log('✓ done:', task));
queue.on('empty', () => console.log('queue empty'));

queue.add({ id: 1, name: 'send email' });
queue.add({ id: 2, name: 'resize image' });
queue.process();
queue.process();
queue.process(); // empty

7.1 Promise 化 EventEmitter

EventEmitter 不返回 Promise,但常被 Promise 化(一次性的事件变 await):

javascript
function once(emitter, eventName) {
  return new Promise((resolve, reject) => {
    emitter.once(eventName, resolve);
    emitter.once('error', reject);
  });
}

// 用法:等待 stream 的 'open' 事件
const { once } = require('node:events');
const stream = fs.createReadStream('./big.txt');

await once(stream, 'open');
console.log('文件已打开');

await once(stream, 'close');
console.log('文件已关闭');

Node 16+ 内置 events.once(),不需要自己包装。

八、EventEmitter vs EventTarget vs Promise

API一对多一次性返回值标准
EventEmitter✅ 多监听器onceNode 私货
EventTarget(浏览器)✅ 多监听器{ once: true }DOM 标准
Promise❌ 单回调✅ 天然一次性thenableECMAScript

何时选哪个:

场景推荐
Node 内置 API(stream、process、http)EventEmitter(不得不)
一次性结果(HTTP 请求、文件读取)Promise(更现代)
浏览器代码或跨平台EventTarget(DOM 标准)
高频多订阅(发布订阅总线)EventEmitter / EventTarget

九、最佳实践

场景推荐
监听 error必须监听,否则进程崩溃
监听器泄漏setMaxListeners(20) 显式声明(说明确实需要)
一次性事件onceevents.once()
清理监听器在资源生命周期结束时 removeListenerremoveAllListeners
异步事件流配合 stream(EventEmitter + 流控制)
多实例隔离每个实例独立订阅,不全局共享 bus
调试监听器getEventListeners(emitter, eventName)(Node 15.2+)

9.1 调试技巧

javascript
const { getEventListeners } = require('node:events');

// 查某事件的监听器
const handlers = getEventListeners(bus, 'data');
console.log('data 事件的监听器数量:', handlers.length);

十、常见错误

错误原因解决
MaxListenersExceededWarning单事件监听器超过 10setMaxListeners 或检查重复注册
未捕获 error 事件崩溃没监听 errorbus.on('error', handler)
监听器永远不触发事件名拼写错检查 emit 和 on 的事件名一致
内存泄漏监听器没移除在合适的生命周期调 removeListener
emit 内异常中断后续监听器监听器抛异常监听器内部 try/catch

十一、小结

  • EventEmitter 是 Node 事件模型的根基;内置类(stream / http / net / process)几乎都继承它
  • API:on / once / emit / off / removeAllListeners / listenerCount
  • emit 同步触发所有监听器,按注册顺序
  • 默认最大监听数 10,超过警告但不崩溃;用 setMaxListeners 调整
  • error 事件无监听器时进程崩溃——必须监听
  • 一次性事件用 onceevents.once();一次性结果优先用 Promise
  • 高频多订阅场景用 EventEmitter;一次性结果用 Promise;跨平台用 EventTarget