实现发布订阅模式

手写 Event Bus / EventEmitter

问题

实现一个发布订阅模式(Event Bus / EventEmitter),支持事件的订阅、取消订阅、触发和一次性订阅。

解答

class EventEmitter {
  constructor() {
    // 存储事件和对应的回调函数列表
    this.events = {};
  }

  // 订阅事件
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    return this; // 支持链式调用
  }

  // 取消订阅
  off(event, callback) {
    if (!this.events[event]) return this;

    if (!callback) {
      // 没传 callback,移除该事件所有监听器
      delete this.events[event];
    } else {
      // 移除指定的回调
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
    return this;
  }

  // 触发事件
  emit(event, ...args) {
    if (!this.events[event]) return this;

    this.events[event].forEach(callback => {
      callback.apply(this, args);
    });
    return this;
  }

  // 只订阅一次
  once(event, callback) {
    // 包装函数,执行后自动移除
    const wrapper = (...args) => {
      callback.apply(this, args);
      this.off(event, wrapper);
    };
    this.on(event, wrapper);
    return this;
  }
}

使用示例

const emitter = new EventEmitter();

// 订阅事件
function handleMessage(msg) {
  console.log('收到消息:', msg);
}

emitter.on('message', handleMessage);
emitter.on('message', (msg) => console.log('另一个监听器:', msg));

// 触发事件
emitter.emit('message', 'Hello World');
// 输出:
// 收到消息: Hello World
// 另一个监听器: Hello World

// 取消订阅
emitter.off('message', handleMessage);
emitter.emit('message', '第二条消息');
// 输出:
// 另一个监听器: 第二条消息

// 一次性订阅
emitter.once('login', (user) => console.log(user, '登录了'));
emitter.emit('login', '张三'); // 输出: 张三 登录了
emitter.emit('login', '李四'); // 无输出,监听器已移除

关键点

  • 用对象存储事件名到回调数组的映射
  • off 需要处理两种情况:移除全部监听器和移除指定监听器
  • once 通过包装函数实现,执行后调用 off 移除自身
  • 返回 this 支持链式调用
  • emit 使用 apply 传递参数,保证 this 指向正确