8、✅ 手写 EventEmitter(发布订阅模式)

117 阅读1分钟

🎯 一、为什么要掌握发布订阅?

发布订阅是前端的“根设计模式”:

  • Vue2 的事件总线 $on / $emit 就是它
  • React 中的 Hook 管理器、Redux 中间件也用到
  • 微前端、组件通信、埋点系统、日志系统、消息队列……全靠它!

🧠 二、设计模式简述

✅ 发布订阅(Pub-Sub):

  • 发布者不直接通知订阅者,而是将消息交给调度中心
  • 订阅者注册回调函数,等待被通知

与之对比的观察者模式中,观察者被直接挂到被观察对象上,耦合更强。


✍️ 三、最简版 EventEmitter 实现(核心功能)

class EventEmitter {
  constructor() {
    this.events = {};
  }

  // 订阅
  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }

  // 取消订阅
  off(eventName, callback) {
    if (!this.events[eventName]) return;
    this.events[eventName] = this.events[eventName].filter(fn => fn !== callback);
  }

  // 发布
  emit(eventName, ...args) {
    if (!this.events[eventName]) return;
    this.events[eventName].forEach(fn => fn(...args));
  }
}

✅ 四、使用示例

const bus = new EventEmitter();

function handler(data) {
  console.log('接收到事件:', data);
}

bus.on('login', handler);
bus.emit('login', { user: 'Mark' }); // 接收到事件:{ user: 'Mark' }

bus.off('login', handler);
bus.emit('login', { user: 'Mark' }); // 无输出

🔁 五、支持一次订阅 once(高级功能)

once(eventName, callback) {
  const wrapper = (...args) => {
    callback(...args);
    this.off(eventName, wrapper);
  };
  this.on(eventName, wrapper);
}

🎯 六、完整版本(带 once + 防止重复)

class EventEmitter {
  constructor() {
    this.events = {};
  }

  on(event, callback) {
    (this.events[event] ||= []).push(callback);
  }

  off(event, callback) {
    if (!this.events[event]) return;
    this.events[event] = this.events[event].filter(fn => fn !== callback);
  }

  once(event, callback) {
    const wrapper = (...args) => {
      callback(...args);
      this.off(event, wrapper);
    };
    this.on(event, wrapper);
  }

  emit(event, ...args) {
    if (!this.events[event]) return;
    [...this.events[event]].forEach(fn => fn(...args)); // 防止事件中删除自己
  }
}

🧩 七、场景举例(项目应用)

场景用法
微前端通信bus.emit('app:loaded')
表单联动A 表单 on('fieldXChange'),B 表单监听
日志/埋点emit 事件上报
前端异常上报on('error') → sentry.report(...)

❗ 八、面试常见陷阱

问题正确处理
emit 时删订阅项会出错?使用浅拷贝 [].forEach()
同事件多次 on,怎么防重?使用 Set 或手动去重
如何实现全局唯一事件总线?封装 export const bus = new EventEmitter()