Node.js 事件模块与 EventEmitter

Node.js 的核心设计哲学是事件驱动和非阻塞 I/O。许多核心模块(如 httpfsstream)都基于事件机制。而这一切的基石就是 events 模块和 EventEmitter 类。本章将深入讲解如何在 Node.js 中使用事件,以及如何创建自己的事件发射器。

1. 什么是事件驱动编程?

事件驱动编程是一种编程范式,程序的执行流程由事件(如用户操作、传感器输出、消息等)决定。在 Node.js 中,许多对象都会触发事件,例如 TCP 服务器在有新连接时触发 connection 事件,可读流在有数据时触发 data 事件。这些对象都是 EventEmitter 的实例,通过绑定事件监听器来响应事件。

2. events 模块与 EventEmitter 类

events 模块只提供了一个 EventEmitter 类,它是 Node.js 事件机制的核心。首先需要引入模块:

const EventEmitter = require('events');

然后创建 EventEmitter 的实例:

const myEmitter = new EventEmitter();

3. 基本方法:on 和 emit

on 方法用于注册事件监听器,emit 方法用于触发事件。当事件被触发时,所有注册的监听器会按注册顺序同步执行。

const EventEmitter = require('events');
const myEmitter = new EventEmitter();

// 注册事件监听器
myEmitter.on('greet', (name) => {
  console.log(`Hello, ${name}!`);
});

// 触发事件
myEmitter.emit('greet', 'Node.js'); // 输出: Hello, Node.js!

emit 可以传递任意数量的参数给监听器函数。

4. 只监听一次:once

使用 once 方法注册的监听器最多只会被触发一次,触发后立即移除。

myEmitter.once('onceEvent', () => {
  console.log('这个只会执行一次');
});

myEmitter.emit('onceEvent'); // 输出
myEmitter.emit('onceEvent'); // 无输出

5. 移除监听器:removeListener 和 off

removeListener 方法用于移除指定的监听器。Node.js 10+ 也提供了别名 off

const callback = () => console.log('执行');
myEmitter.on('test', callback);

// 移除监听器
myEmitter.removeListener('test', callback);
// 或 myEmitter.off('test', callback);

myEmitter.emit('test'); // 无输出

5.1 移除所有监听器:removeAllListeners

myEmitter.removeAllListeners('test'); // 移除指定事件的所有监听器
myEmitter.removeAllListeners(); // 移除所有事件的所有监听器

6. 获取监听器信息

listenerCount 返回指定事件的监听器数量。listeners 返回指定事件的监听器函数数组。

console.log(myEmitter.listenerCount('greet')); // 整数
console.log(myEmitter.listeners('greet')); // [Function]

7. 事件参数与 this

在监听器中,this 指向 EventEmitter 实例本身。如果使用箭头函数,则 this 由词法作用域决定,不会指向实例。

myEmitter.on('test', function() {
  console.log(this === myEmitter); // true
});

myEmitter.on('test', () => {
  console.log(this === myEmitter); // false,this 是外部上下文
});

8. 错误处理:'error' 事件

当 EventEmitter 实例中发生错误时,通常应该触发 error 事件。如果没有为 error 事件注册监听器,Node.js 会将错误抛出,导致进程崩溃。

myEmitter.on('error', (err) => {
  console.error('发生错误:', err.message);
});

myEmitter.emit('error', new Error('出错了')); // 被捕获,不会崩溃

9. 异步与同步

EventEmitter 默认会同步调用所有监听器。如果希望异步执行,可以使用 setImmediateprocess.nextTick

myEmitter.on('async', () => {
  setImmediate(() => {
    console.log('异步执行');
  });
});

myEmitter.emit('async');
console.log('同步执行后');
// 输出顺序:同步执行后 -> 异步执行

10. 继承 EventEmitter

在实际开发中,我们常常创建自定义类,让它继承 EventEmitter,从而拥有事件能力。这可以通过 ES6 的 extends 实现。

const EventEmitter = require('events');

class MyEmitter extends EventEmitter {
  constructor(name) {
    super();
    this.name = name;
  }

  start() {
    console.log(`${this.name} 启动`);
    this.emit('start', this.name);
  }

  process(data) {
    this.emit('data', data);
  }
}

const myObj = new MyEmitter('测试对象');
myObj.on('start', (name) => {
  console.log(`事件:${name} 已启动`);
});
myObj.on('data', (data) => {
  console.log('收到数据:', data);
});

myObj.start();
myObj.process('hello');

11. 事件模块的其他内容

11.1 默认最大监听器数量

每个 EventEmitter 实例默认最多为同一事件注册 10 个监听器,超过会发出警告。可以通过 setMaxListeners 修改这个限制。

myEmitter.setMaxListeners(20);
console.log(myEmitter.getMaxListeners()); // 20

11.2 预定义事件:newListener 和 removeListener

EventEmitter 实例本身会触发两个内置事件:

  • newListener:当添加新监听器时触发。
  • removeListener:当移除监听器时触发。
myEmitter.on('newListener', (event, listener) => {
  console.log(`添加了 ${event} 事件的监听器`);
});

myEmitter.on('removeListener', (event, listener) => {
  console.log(`移除了 ${event} 事件的监听器`);
});

const fn = () => {};
myEmitter.on('foo', fn);
myEmitter.off('foo', fn);

12. 实用示例:构建一个简单的任务队列

使用 EventEmitter 实现一个发布-订阅模式的任务队列:

const EventEmitter = require('events');

class TaskQueue extends EventEmitter {
  constructor(concurrency) {
    super();
    this.concurrency = concurrency;
    this.queue = [];
    this.running = 0;
  }

  pushTask(task) {
    this.queue.push(task);
    process.nextTick(() => this.next());
  }

  next() {
    if (this.running >= this.concurrency || this.queue.length === 0) {
      return;
    }
    const task = this.queue.shift();
    this.running++;
    task().then((result) => {
      this.running--;
      this.emit('task-complete', result);
      this.next();
    }).catch(err => {
      this.running--;
      this.emit('error', err);
      this.next();
    });
  }
}

const queue = new TaskQueue(2);
queue.on('task-complete', (result) => {
  console.log('任务完成:', result);
});
queue.on('error', console.error);

// 模拟异步任务
queue.pushTask(() => new Promise(resolve => {
  setTimeout(() => resolve('任务1'), 1000);
}));
queue.pushTask(() => new Promise(resolve => {
  setTimeout(() => resolve('任务2'), 500);
}));
queue.pushTask(() => new Promise(resolve => {
  setTimeout(() => resolve('任务3'), 800);
}));

13. 注意事项

  • 始终为 error 事件注册监听器,避免进程崩溃。
  • 如果监听器很多,考虑设置合适的最大监听器数量,避免内存泄漏警告。
  • 使用 once 代替 on 如果只需要响应一次事件。
  • 移除不再需要的监听器,特别是全局对象上的事件,以避免内存泄漏。
最佳实践: 当创建自定义类时,继承 EventEmitter 是一种很好的方式,让类的实例拥有事件能力,同时保持代码的清晰和可扩展性。

小结

事件模块是 Node.js 异步编程的基石。通过 EventEmitter,我们可以轻松实现发布-订阅模式,构建松耦合、可扩展的系统。本章介绍了 EventEmitter 的核心 API,并通过实例展示了其应用。在后续章节中,我们将学习流(Stream)模块,它也是基于事件实现的。