标准答案
- 内部可以用 Map 保存事件名到监听集合的关系。
- on(event, listener) 添加监听,并最好返回一个取消订阅函数。
- off(event, listener) 移除指定监听,避免页面销毁后继续响应事件。
- once(event, listener) 可以包装原监听,执行后自动 off。
- 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 时直接遍历原集合,回调里新增或删除监听会影响当前触发过程。
- 把事件总线当成全局状态管理,最后事件来源和执行顺序都不清楚。