外观
怎么实现发布订阅?
发布订阅用事件名把「发生了什么」和「谁关心」分开。on 注册,off 取消,emit 调用当前监听,once 调用一次后移除。取消时必须用注册时的同一个函数引用。
原理
js
class Emitter {
constructor() {
this.events = new Map()
}
on(type, handler) {
const list = this.events.get(type) ?? []
list.push(handler)
this.events.set(type, list)
return () => this.off(type, handler)
}
off(type, handler) {
const list = this.events.get(type)
if (!list) return
this.events.set(
type,
list.filter((fn) => fn !== handler)
)
}
emit(type, ...args) {
const list = [...(this.events.get(type) ?? [])]
for (const handler of list) handler(...args)
}
once(type, handler) {
const wrap = (...args) => {
this.off(type, wrap)
handler(...args)
}
return this.on(type, wrap)
}
}emit 先复制数组再遍历。这样监听函数里面立刻 off 自己或别人,不会改到正在遍历的那一份。
once 包了一层。取消这次一次性监听时,要拿 on 返回的取消函数,或者 off 那个包装函数,而不能 off 当初传入的 handler,因为表里存的是包装。
追问
和观察者模式有什么差别?
观察者模式里,目标通常直接持有观察者。发布订阅中间还有一个事件通道,发布者不必知道订阅者是谁。前端的 event bus、DOM 事件,都是后面这一种。
页面里为什么要少用全局事件总线?
谁都能 emit,数据从哪里来会变得难追。组件之间优先用 props、回调或状态管理。事件总线适合真正的跨模块通知,并且要在卸载时 off。
易错点
off用另一个长得一样的箭头函数,引用不同,取消无效。once的包装函数如果在handler抛错之前就off,错误不会导致监听留下。上面的实现是先移除再调用;如果希望失败后还能再收一次,顺序要换过来,并在面试里说明。emit时监听抛错会打断后面的监听。需要隔离的话,每个handler各自try/catch。