标准答案
- 内部可以用 Map 保存事件名和监听函数集合。
- on 用来添加监听,emit 用来按事件名触发所有监听。
- off 要能移除指定监听,避免页面或模块销毁后继续响应事件。
- once 可以通过包装函数实现,执行一次后自动取消订阅。
- 触发事件时最好复制一份监听列表,避免回调执行过程中增删监听导致遍历异常。
题目解析
发布订阅适合把事件生产者和消费者解耦。生产者只负责发出事件,不需要知道谁在监听;消费者只关心自己订阅的事件。
手写 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 当作万能跨模块通信方案,最后事件来源和调用顺序都难以追踪。