4.2 事件发布/订阅模型

36 阅读2分钟

这一小节是第四章的开篇,也是Node异步编程最基础、最常用的模式——事件发布/订阅(Event Publish/Subscribe),核心类就是EventEmitter。几乎所有Node核心模块(如fs、net、http、stream)都继承自它,它是Node“事件驱动”哲学的直接体现。

朴灵作者用很大篇幅讲解EventEmitter,因为在2013年,它是解决回调地狱的最主流方式之一(解耦、灵活、可组合)。

为什么需要事件发布/订阅?

  • 回调函数是一对一的(一个操作一个回调),耦合度高。
  • 实际场景往往“一对多”或“多对多”:
    • 一个文件读取完成,可能需要触发多个处理(日志、缓存、响应客户端)。
    • 一个服务器错误,需要通知多个监听者(监控、日志、重启)。
  • 发布/订阅模式完美解耦:发布者只管emit事件,订阅者只管on监听,互不干扰。

EventEmitter 的核心 API

Node的events模块提供了EventEmitter类:

const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}  // 自定义事件发射器
// 或直接用
const myEmitter = new EventEmitter();

主要方法:

  1. on(eventName, listener) / addListener:订阅事件

    myEmitter.on('event', () => console.log('事件发生!'));
    
  2. once(eventName, listener):只监听一次

    myEmitter.once('once', () => console.log('只触发一次'));
    
  3. emit(eventName[, ...args]):发布(触发)事件

    myEmitter.emit('event');  // 触发
    
  4. off(eventName, listener) / removeListener:移除监听

    const cb = () => console.log('要移除');
    myEmitter.on('event', cb);
    myEmitter.off('event', cb);
    
  5. removeAllListeners([eventName]):移除所有或指定事件监听

  6. listeners(eventName):获取某个事件的监听器数组

  7. eventNames():获取所有事件名

经典示例:用EventEmitter组织异步操作

假设我们要读取文件 → 解析 → 保存 → 通知:

const EventEmitter = require('events');
const fs = require('fs');

class FileProcessor extends EventEmitter {}

const processor = new FileProcessor();

// 订阅多个处理
processor.on('read', (data) => console.log('读取完成:', data.length));
processor.on('parse', () => console.log('解析开始'));
processor.on('save', () => console.log('保存成功'));

// 异步流程
fs.readFile('data.txt', (err, data) => {
  if (err) return console.error(err);
  
  processor.emit('read', data);  // 触发读取完成
  
  // 模拟解析
  setTimeout(() => {
    processor.emit('parse');
    
    // 模拟保存
    setTimeout(() => {
      processor.emit('save');
    }, 100);
  }, 200);
});

输出:

读取完成: xxx
解析开始
保存成功

优势:每个阶段独立,容易添加新监听者(比如加个日志监听),不改原有代码。

错误处理(重要!)

EventEmitter有一个约定:如果emit('error')且没有监听者,进程会崩溃!

正确写法:

myEmitter.on('error', (err) => console.error('出错了:', err));

myEmitter.emit('error', new Error('boom!'));

注意事项(书里强调的坑)

  • 监听器数量默认最多10个,超了会警告(setMaxListeners调整)。
  • emit是同步执行所有监听器(顺序执行)。
  • 事件名是字符串(或Symbol,避免冲突)。