JavaScript 发布订阅模式实现

3 阅读4分钟

发布订阅模式是 JavaScript 中最核心的设计模式之一。理解并手写一个完整的发布订阅系统,是每个前端开发者进阶的必经之路。

前言:为什么需要发布订阅模式?

我们先想象这样一个场景:当多个模块都需要响应同一个状态变化,这时候怎么处理呢?

传统硬编码依赖

class ModuleA {
  update(data) {
    ModuleB.render(data);
    ModuleC.refresh(data);
    ModuleD.notify(data);
    // ...
  }
}

这种情况下,每增加一个依赖模块,都要修改一次这里的代码。而且,如果有 100 个依赖模块,我们就要写 100 行调用语句,这显然是不合理的!

那么,正确的处理方式应该是什么呢?那就是发布订阅模式,解耦发布者和订阅者:

  • 发布者只负责发布事件,不关心谁在监听
  • 订阅者各自注册自己关心的事件

发布订阅模式

class EventBus {
    // 在这里实现事件的发布和订阅
}

const eventBus = new EventBus();
// 订阅者各自注册自己关心的事件
eventBus.on('dataUpdate', (data) => ModuleB.render(data));
eventBus.on('dataUpdate', (data) => ModuleC.refresh(data));
eventBus.on('dataUpdate', (data) => ModuleD.notify(data));

// 发布者只负责发布事件,不关心谁在监听
eventBus.emit('dataUpdate', newData);

理解发布订阅模式

核心概念

  • 发布者:只负责发布事件,不关心谁订阅
  • 订阅者:只关心自己需要的事件
  • 事件通道:存储事件和回调函数的对应关系

工作流

  1. 订阅者通过 on 方法注册事件监听器
  2. 发布者通过 emit 方法触发事件
  3. 事件通道找到所有对应的回调函数并执行
  4. 订阅者可以通过 off 方法取消订阅

优点

  • 解耦:发布者和订阅者互不知道对方的存在
  • 扩展性:新增订阅者无需修改发布者代码
  • 异步:支持事件的延迟触发和异步处理
  • 灵活:支持一次性监听、条件监听等高级特性

基础事件总线实现

简单实现

class SimpleEventBus {
  constructor() {
    this.events = {}; // 存储事件名到回调函数数组的映射
  }

  /**
   * 订阅事件
   * @param {string} eventName - 事件名
   * @param {Function} callback - 回调函数
   */
  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
    console.log(`事件 [${eventName}] 订阅成功,当前订阅者: ${this.events[eventName].length}`);
  }

  /**
   * 发布事件
   * @param {string} eventName - 事件名
   * @param {...any} args - 传递给回调的参数
   */
  emit(eventName, ...args) {
    const callbacks = this.events[eventName];
    if (!callbacks || callbacks.length === 0) {
      console.log(`事件 [${eventName}] 没有订阅者`);
      return false;
    }

    console.log(`事件 [${eventName}] 发布,通知 ${callbacks.length} 个订阅者`);
    callbacks.forEach(callback => {
      try {
        callback(...args);
      } catch (error) {
        console.error(`事件 [${eventName}] 回调执行错误:`, error);
      }
    });
    return true;
  }

  /**
   * 获取事件的订阅者数量
   */
  listenerCount(eventName) {
    return this.events[eventName]?.length || 0;
  }

  /**
   * 获取所有事件名称
   */
  eventNames() {
    return Object.keys(this.events);
  }
}

支持 once 一次性监听

class OnceEventBus extends SimpleEventBus {
  /**
   * 一次性订阅,执行一次后自动移除
   */
  once(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }

    // 包装原回调,在调用后自动移除
    const onceWrapper = (...args) => {
      this.off(eventName, onceWrapper);
      callback(...args);
    };

    // 保存原始回调的引用,以便取消订阅时能找到
    onceWrapper.originalCallback = callback;

    this.events[eventName].push(onceWrapper);
    console.log(`事件 [${eventName}] 一次性订阅成功`);
  }

  /**
   * 取消订阅
   */
  off(eventName, callback) {
    const callbacks = this.events[eventName];
    if (!callbacks) return false;

    // 如果没有指定回调,移除该事件的所有订阅
    if (!callback) {
      delete this.events[eventName];
      console.log(`事件 [${eventName}] 所有订阅已移除`);
      return true;
    }

    // 移除指定的回调
    const index = callbacks.findIndex(cb =>
      cb === callback || cb.originalCallback === callback
    );

    if (index !== -1) {
      callbacks.splice(index, 1);
      console.log(`事件 [${eventName}] 指定订阅已移除`);

      // 如果该事件没有订阅者了,清理空数组
      if (callbacks.length === 0) {
        delete this.events[eventName];
      }
      return true;
    }

    return false;
  }

  /**
   * 移除所有事件的所有订阅
   */
  removeAllListeners() {
    this.events = {};
    console.log('所有事件订阅已清空');
  }
}

Vue3 风格的事件总线

上述版本的代码其实已经能覆盖80%的业务场景了,但是在 Vue3 中,它缺不再推荐使用$on / $emit,而是改用 mitt ,这是为什么呢?

为什么Vue3不再推荐 on/on / emit,而是用mitt?

维度传统EventBusVue3响应式
触发方式显式emit隐式set
数据结构事件名+回调状态+副作用
取消订阅手动off自动收集依赖

从上表的对比中,我们可以看出传统 EventBus 和 Vue3 的事件总线有个显著的区别:Vue3 的事件总线是基于 状态+副作用 ,可以将其称为“有状态的事件”。

基本框架

class ReactiveEventBus {
  constructor() {
    this.events = new Map();
    this.history = new Map();
    this.lastEvent = new Map();
  }

  /**
   * 创建响应式事件
   */
  createReactiveEvent(eventName) {}

  /**
   * 创建带状态的响应式事件
   */
  createStatefulEvent(eventName, initialState = null) {}

  /**
   * 创建计算事件
   */
  computed(deps, compute) {}

  /**
   * 创建事件管道
   */
  pipe(...transforms) {}
}

创建响应式事件createReactiveEvent

createReactiveEvent(eventName) {
  let listeners = this.events.get(eventName);
  if (!listeners) {
    listeners = new Set();
    this.events.set(eventName, listeners);

    // 创建历史记录
    this.history.set(eventName, []);

    // 创建最近事件记录
    this.lastEvent.set(eventName, null);
  }

  return {
    // 监听事件
    on: (callback) => {
      listeners.add(callback);
      return () => listeners.delete(callback);
    },

    // 一次性监听
    once: (callback) => {
      const wrapper = (...args) => {
        callback(...args);
        listeners.delete(wrapper);
      };
      listeners.add(wrapper);
      return () => listeners.delete(wrapper);
    },

    // 触发事件
    emit: (...args) => {
      const timestamp = Date.now();

      // 记录到历史
      const history = this.history.get(eventName);
      history.push({ timestamp, args });

      // 限制历史长度
      if (history.length > 50) {
        history.shift();
      }

      // 更新最近事件
      this.lastEvent.set(eventName, { timestamp, args });

      // 触发监听器
      listeners.forEach(callback => {
        try {
          callback(...args);
        } catch (error) {
          console.error(`事件 [${eventName}] 错误:`, error);
        }
      });
    },

    // 获取历史
    history: () => this.history.get(eventName),

    // 获取最近事件
    last: () => this.lastEvent.get(eventName),

    // 清除历史
    clearHistory: () => this.history.set(eventName, []),

    // 移除所有监听器
    off: () => {
      listeners.clear();
      this.events.delete(eventName);
    }
  };
}

创建带状态的响应式事件createStatefulEvent

createStatefulEvent(eventName, initialState = null) {
  const event = this.createReactiveEvent(eventName);
  let state = initialState;

  // 增强事件对象
  return {
    ...event,

    // 获取状态
    get state() {
      return state;
    },

    // 更新状态并触发事件
    setState(newState) {
      const oldState = state;
      state = typeof newState === 'function'
        ? newState(state)
        : newState;

      event.emit(state, oldState);
      return state;
    },

    // 订阅状态变化
    subscribe(callback) {
      return event.on(callback);
    },

    // 获取当前状态
    snapshot: () => state
  };
}

创建计算事件computed

computed(deps, compute) {
  const resultEvent = this.createReactiveEvent(Symbol('computed'));

  let lastResult = null;

  const update = () => {
    const newResult = compute();
    if (newResult !== lastResult) {
      lastResult = newResult;
      resultEvent.emit(newResult);
    }
  };

  // 订阅依赖
  deps.forEach(dep => {
    if (typeof dep.on === 'function') {
      dep.on(update);
    }
  });

  // 立即计算一次
  update();

  return {
    value: () => lastResult,
    on: resultEvent.on,
    off: resultEvent.off
  };
}

创建事件管道pipe

pipe(...transforms) {
  return (eventName) => {
    const source = this.createReactiveEvent(eventName);
    const target = this.createReactiveEvent(`${eventName}:pipe`);

    source.on((...args) => {
      let result = args;
      for (const transform of transforms) {
        result = transform(result);
      }
      target.emit(result);
    });

    return {
      source,
      target
    };
  };
}

完整实现

class ReactiveEventBus {
  constructor() {
    this.events = new Map();
    this.history = new Map();
    this.lastEvent = new Map();
  }

  /**
   * 创建响应式事件
   */
  createReactiveEvent(eventName) {
    let listeners = this.events.get(eventName);
    if (!listeners) {
      listeners = new Set();
      this.events.set(eventName, listeners);

      // 创建历史记录
      this.history.set(eventName, []);

      // 创建最近事件记录
      this.lastEvent.set(eventName, null);
    }

    return {
      // 监听事件
      on: (callback) => {
        listeners.add(callback);
        return () => listeners.delete(callback);
      },

      // 一次性监听
      once: (callback) => {
        const wrapper = (...args) => {
          callback(...args);
          listeners.delete(wrapper);
        };
        listeners.add(wrapper);
        return () => listeners.delete(wrapper);
      },

      // 触发事件
      emit: (...args) => {
        const timestamp = Date.now();

        // 记录到历史
        const history = this.history.get(eventName);
        history.push({ timestamp, args });

        // 限制历史长度
        if (history.length > 50) {
          history.shift();
        }

        // 更新最近事件
        this.lastEvent.set(eventName, { timestamp, args });

        // 触发监听器
        listeners.forEach(callback => {
          try {
            callback(...args);
          } catch (error) {
            console.error(`事件 [${eventName}] 错误:`, error);
          }
        });
      },

      // 获取历史
      history: () => this.history.get(eventName),

      // 获取最近事件
      last: () => this.lastEvent.get(eventName),

      // 清除历史
      clearHistory: () => this.history.set(eventName, []),

      // 移除所有监听器
      off: () => {
        listeners.clear();
        this.events.delete(eventName);
      }
    };
  }

  /**
   * 创建带状态的响应式事件
   */
  createStatefulEvent(eventName, initialState = null) {
    const event = this.createReactiveEvent(eventName);
    let state = initialState;

    // 增强事件对象
    return {
      ...event,

      // 获取状态
      get state() {
        return state;
      },

      // 更新状态并触发事件
      setState(newState) {
        const oldState = state;
        state = typeof newState === 'function'
          ? newState(state)
          : newState;

        event.emit(state, oldState);
        return state;
      },

      // 订阅状态变化
      subscribe(callback) {
        return event.on(callback);
      },

      // 获取当前状态
      snapshot: () => state
    };
  }

  /**
   * 创建计算事件
   */
  computed(deps, compute) {
    const resultEvent = this.createReactiveEvent(Symbol('computed'));

    let lastResult = null;

    const update = () => {
      const newResult = compute();
      if (newResult !== lastResult) {
        lastResult = newResult;
        resultEvent.emit(newResult);
      }
    };

    // 订阅依赖
    deps.forEach(dep => {
      if (typeof dep.on === 'function') {
        dep.on(update);
      }
    });

    // 立即计算一次
    update();

    return {
      value: () => lastResult,
      on: resultEvent.on,
      off: resultEvent.off
    };
  }

  /**
   * 创建事件管道
   */
  pipe(...transforms) {
    return (eventName) => {
      const source = this.createReactiveEvent(eventName);
      const target = this.createReactiveEvent(`${eventName}:pipe`);

      source.on((...args) => {
        let result = args;
        for (const transform of transforms) {
          result = transform(result);
        }
        target.emit(result);
      });

      return {
        source,
        target
      };
    };
  }
}

结语

发布订阅模式是 JavaScript 异步编程的基石,掌握它的实现原理和应用场景,能够帮助我们写出更优雅、更可维护的代码。无论是构建大型应用还是开发 npm 包,这个模式都会是我们的重要工具。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!