标准答案

  1. 内部可以用 Map 保存事件名到监听集合的关系。
  2. on(event, listener) 添加监听,并最好返回一个取消订阅函数。
  3. off(event, listener) 移除指定监听,避免页面销毁后继续响应事件。
  4. once(event, listener) 可以包装原监听,执行后自动 off。
  5. emit(event, ...args) 触发事件时复制监听列表,降低遍历过程中增删监听带来的副作用。

题目解析

发布订阅的价值是解耦事件生产者和消费者。生产者只发事件,不关心谁在听;消费者只订阅自己关心的事件。

手写时不能只写 on 和 emit。off、once 和遍历稳定性更能体现工程意识,否则组件反复挂载后很容易留下重复监听或内存泄漏。

EventEmitter 适合轻量事件通知,但不应该替代清晰的数据流。事件过多时,调用关系会变隐式,排查顺序和数据来源会很困难。

代码示例

下面的实现覆盖订阅、取消、一次性订阅和事件触发。

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)

    return () => this.off(event, listener)
  }

  off(event, listener) {
    const listeners = this.events.get(event)
    if (!listeners) return

    listeners.delete(listener)
    if (listeners.size === 0) {
      this.events.delete(event)
    }
  }

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

    return this.on(event, wrapper)
  }

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

常见误区

  • 只实现 on 和 emit,忽略 off,导致监听无法清理。
  • emit 时直接遍历原集合,回调里新增或删除监听会影响当前触发过程。
  • 把事件总线当成全局状态管理,最后事件来源和执行顺序都不清楚。

作者信息