创见博客
发布订阅的实现
七崽爱吃小饼干2026/01/30阅读 2专栏 JavaScript

发布订阅模式,这个模式的核心是解耦事件的发布者和订阅者,让它们之间不直接通信,而是通过一个事件中心来管理。

实现思路

发布订阅模式主要包含三个核心角色和对应的功能:

  1. 事件中心(EventEmitter):存储事件与对应回调函数的映射关系,提供订阅、取消订阅、发布的方法。
  2. 订阅者:通过 on 方法订阅事件,并注册回调函数。
  3. 发布者:通过 emit 方法发布事件,触发该事件下所有订阅的回调函数。

同时还需要考虑:

  • 一个事件可以绑定多个回调函数
  • 支持取消指定事件的指定回调
  • 支持只执行一次的订阅(once 方法)

完整实现代码

javascript
class EventEmitter {
  constructor() {
    // 存储事件和回调的映射:{ eventName: [fn1, fn2, ...] }
    this.events = Object.create(null);
  }

  /**
   * 订阅事件
   * @param {string} eventName - 事件名称
   * @param {Function} callback - 回调函数
   * @returns {EventEmitter} 返回实例,支持链式调用
   */
  on(eventName, callback) {
    // 校验参数合法性
    if (typeof eventName !== 'string' || typeof callback !== 'function') {
      throw new Error('事件名必须是字符串,回调必须是函数');
    }

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

    // 避免重复订阅同一个回调
    if (!this.events[eventName].includes(callback)) {
      this.events[eventName].push(callback);
    }

    return this;
  }

  /**
   * 发布事件(触发回调)
   * @param {string} eventName - 事件名称
   * @param  {...any} args - 传递给回调的参数
   * @returns {EventEmitter} 返回实例,支持链式调用
   */
  emit(eventName, ...args) {
    if (typeof eventName !== 'string') {
      throw new Error('事件名必须是字符串');
    }

    // 如果事件没有订阅者,直接返回
    if (!this.events[eventName]) return this;

    // 遍历执行所有回调,并传递参数
    this.events[eventName].forEach(callback => {
      // 确保回调执行时的 this 指向 EventEmitter 实例(可选,根据需求调整)
      callback.call(this, ...args);
    });

    return this;
  }

  /**
   * 取消订阅
   * @param {string} eventName - 事件名称
   * @param {Function} [callback] - 可选,指定要取消的回调;不传则取消该事件所有回调
   * @returns {EventEmitter} 返回实例,支持链式调用
   */
  off(eventName, callback) {
    if (typeof eventName !== 'string') {
      throw new Error('事件名必须是字符串');
    }

    // 事件不存在,直接返回
    if (!this.events[eventName]) return this;

    if (typeof callback === 'function') {
      // 只取消指定的回调
      this.events[eventName] = this.events[eventName].filter(fn => fn !== callback);
    } else {
      // 取消该事件所有回调
      this.events[eventName] = [];
    }

    return this;
  }

  /**
   * 订阅一次事件(执行后自动取消)
   * @param {string} eventName - 事件名称
   * @param {Function} callback - 回调函数
   * @returns {EventEmitter} 返回实例,支持链式调用
   */
  once(eventName, callback) {
    if (typeof eventName !== 'string' || typeof callback !== 'function') {
      throw new Error('事件名必须是字符串,回调必须是函数');
    }

    // 包装一个一次性回调
    const onceCallback = (...args) => {
      callback.call(this, ...args); // 执行原回调
      this.off(eventName, onceCallback); // 执行后取消订阅
    };

    this.on(eventName, onceCallback);
    return this;
  }
}

代码使用示例

javascript
// 1. 创建事件中心实例
const emitter = new EventEmitter();

// 2. 定义回调函数
function handleMsg(msg) {
  console.log('收到消息:', msg);
}

// 3. 订阅事件
emitter.on('message', handleMsg);

// 4. 发布事件(触发回调)
emitter.emit('message', 'Hello 发布订阅!'); // 输出:收到消息:Hello 发布订阅!

// 5. 订阅一次性事件
emitter.once('onceEvent', (num) => {
  console.log('一次性事件触发:', num);
});
emitter.emit('onceEvent', 123); // 输出:一次性事件触发:123
emitter.emit('onceEvent', 456); // 无输出(已自动取消订阅)

// 6. 取消指定回调的订阅
emitter.off('message', handleMsg);
emitter.emit('message', '取消后不会输出'); // 无输出

// 7. 取消事件所有回调
emitter.on('test', () => console.log('test1'));
emitter.on('test', () => console.log('test2'));
emitter.off('test'); // 取消test事件所有回调
emitter.emit('test'); // 无输出

核心代码解释

  1. constructor:初始化 events 对象,用于存储事件和回调的映射,使用 Object.create(null) 避免继承原型链上的属性。
  2. on 方法:校验参数后,将回调函数添加到对应事件的回调数组中,同时避免重复订阅。
  3. emit 方法:触发指定事件的所有回调,并将参数传递给回调函数,保证回调执行时的 this 指向(可选)。
  4. off 方法:支持取消指定回调或整个事件的所有回调,通过 filter 过滤掉要取消的回调。
  5. once 方法:包装原回调,执行后自动调用 off 取消订阅,实现“只执行一次”的效果。

总结

  1. 发布订阅模式的核心是事件中心,通过 events 对象维护事件与回调的映射,解耦发布者和订阅者。
  2. 核心方法包括:on(订阅)、emit(发布)、off(取消订阅),扩展方法 once(一次性订阅)是常用增强。
  3. 实现时需注意参数校验、避免重复订阅、回调参数传递、this 指向等细节,保证代码的健壮性。
评论
0/100