标准答案

  1. 内部可以用 Map 保存事件名和监听函数集合。
  2. on 用来添加监听,emit 用来按事件名触发所有监听。
  3. off 要能移除指定监听,避免页面或模块销毁后继续响应事件。
  4. once 可以通过包装函数实现,执行一次后自动取消订阅。
  5. 触发事件时最好复制一份监听列表,避免回调执行过程中增删监听导致遍历异常。

题目解析

发布订阅适合把事件生产者和消费者解耦。生产者只负责发出事件,不需要知道谁在监听;消费者只关心自己订阅的事件。

手写 EventEmitter 时,取消订阅和一次性订阅比 on/emit 更容易暴露工程细节。只会添加不会移除,很容易在页面切换、组件卸载或重复初始化时造成内存泄漏。

它也不是全局状态管理的替代品。如果事件太多、顺序复杂、数据来源不清晰,系统会变成隐式调用链,调试成本会很高。

代码示例

一个简化实现可以覆盖订阅、触发、取消和一次性监听。

JavaScript
class EventEmitter {
  constructor() {
    this.events = new Map()
  }

  on(event, listener) {
    const listeners = this.events.get(event) || new Set()
    listeners.add(listener)
    this.events.set(event, listeners)
  }

  off(event, listener) {
    this.events.get(event)?.delete(listener)
  }

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

  emit(event, ...args) {
    const listeners = [...(this.events.get(event) || [])]
    listeners.forEach(listener => listener(...args))
  }
}

常见误区

  • 只实现 on 和 emit,忽略 off,导致监听无法清理。
  • emit 时直接遍历原集合,回调里增删监听会影响当前遍历。
  • 把 EventEmitter 当作万能跨模块通信方案,最后事件来源和调用顺序都难以追踪。

作者信息