实现发布订阅模式

手写实现一个完整的发布订阅模式(EventEmitter),支持事件的订阅、发布、取消订阅等功能

问题

发布订阅模式是一种常见的设计模式,用于实现对象间的松耦合通信。需要实现一个事件管理器,支持以下功能:

  1. 订阅事件(on):注册事件监听器
  2. 发布事件(emit):触发事件并传递参数
  3. 取消订阅(off):移除指定的事件监听器
  4. 单次订阅(once):事件触发一次后自动取消订阅
  5. 清空事件(clear):清空指定事件的所有监听器

解答

class EventEmitter {
  constructor() {
    // 存储事件及其对应的监听器列表
    this.events = {};
  }

  /**
   * 订阅事件
   * @param {string} eventName - 事件名称
   * @param {Function} callback - 回调函数
   */
  on(eventName, callback) {
    if (typeof callback !== 'function') {
      throw new TypeError('callback must be a function');
    }

    // 如果事件不存在,初始化为空数组
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }

    // 将回调函数添加到事件监听器列表
    this.events[eventName].push(callback);

    return this; // 支持链式调用
  }

  /**
   * 发布事件
   * @param {string} eventName - 事件名称
   * @param  {...any} args - 传递给回调函数的参数
   */
  emit(eventName, ...args) {
    // 如果事件不存在或没有监听器,直接返回
    if (!this.events[eventName] || this.events[eventName].length === 0) {
      return false;
    }

    // 执行所有监听器
    this.events[eventName].forEach(callback => {
      callback(...args);
    });

    return true;
  }

  /**
   * 取消订阅
   * @param {string} eventName - 事件名称
   * @param {Function} callback - 要移除的回调函数
   */
  off(eventName, callback) {
    // 如果事件不存在,直接返回
    if (!this.events[eventName]) {
      return this;
    }

    // 如果没有指定回调函数,移除该事件的所有监听器
    if (!callback) {
      delete this.events[eventName];
      return this;
    }

    // 过滤掉指定的回调函数
    this.events[eventName] = this.events[eventName].filter(
      cb => cb !== callback && cb.origin !== callback
    );

    return this;
  }

  /**
   * 单次订阅(触发一次后自动取消)
   * @param {string} eventName - 事件名称
   * @param {Function} callback - 回调函数
   */
  once(eventName, callback) {
    if (typeof callback !== 'function') {
      throw new TypeError('callback must be a function');
    }

    // 创建包装函数
    const wrapper = (...args) => {
      callback(...args);
      // 执行后立即取消订阅
      this.off(eventName, wrapper);
    };

    // 保存原始回调的引用,用于 off 方法
    wrapper.origin = callback;

    this.on(eventName, wrapper);

    return this;
  }

  /**
   * 清空指定事件的所有监听器
   * @param {string} eventName - 事件名称(可选,不传则清空所有事件)
   */
  clear(eventName) {
    if (eventName) {
      delete this.events[eventName];
    } else {
      this.events = {};
    }

    return this;
  }

  /**
   * 获取指定事件的监听器数量
   * @param {string} eventName - 事件名称
   */
  listenerCount(eventName) {
    return this.events[eventName] ? this.events[eventName].length : 0;
  }
}

使用示例

// 创建事件管理器实例
const emitter = new EventEmitter();

// 示例1:基本的订阅和发布
function onLogin(username) {
  console.log(`用户 ${username} 登录成功`);
}

emitter.on('login', onLogin);
emitter.emit('login', 'Alice'); // 输出: 用户 Alice 登录成功

// 示例2:多个监听器
emitter.on('login', (username) => {
  console.log(`发送欢迎邮件给 ${username}`);
});

emitter.emit('login', 'Bob');
// 输出:
// 用户 Bob 登录成功
// 发送欢迎邮件给 Bob

// 示例3:取消订阅
emitter.off('login', onLogin);
emitter.emit('login', 'Charlie'); // 只输出: 发送欢迎邮件给 Charlie

// 示例4:单次订阅
emitter.once('payment', (amount) => {
  console.log(`支付 ${amount} 元成功`);
});

emitter.emit('payment', 100); // 输出: 支付 100 元成功
emitter.emit('payment', 200); // 无输出(已自动取消订阅)

// 示例5:链式调用
emitter
  .on('message', (msg) => console.log('收到消息:', msg))
  .on('message', (msg) => console.log('消息长度:', msg.length))
  .emit('message', 'Hello World');
// 输出:
// 收到消息: Hello World
// 消息长度: 11

// 示例6:清空事件
emitter.clear('message'); // 清空 message 事件的所有监听器
emitter.clear(); // 清空所有事件

// 示例7:获取监听器数量
emitter.on('test', () => {});
emitter.on('test', () => {});
console.log(emitter.listenerCount('test')); // 输出: 2

关键点

  • 数据结构设计:使用对象存储事件名和监听器数组的映射关系,便于快速查找和管理
  • 链式调用:所有方法返回 this,支持链式调用,提升代码可读性
  • once 实现技巧:通过包装函数在执行后自动调用 off 方法,同时保存原始回调引用以支持手动取消
  • 参数传递:使用剩余参数(...args)和展开运算符,支持任意数量的参数传递
  • 边界处理:处理事件不存在、监听器为空等边界情况,增强代码健壮性
  • 类型检查:对回调函数进行类型校验,避免运行时错误
  • 灵活的 off 方法:支持移除指定回调或清空整个事件的所有监听器
  • 实用工具方法:提供 listenerCount 等辅助方法,方便调试和监控