发布订阅
发布订阅题主要考事件模型和边界处理:
- 如何存储事件名和回调列表
emit时如何传参off如何删除指定回调once如何只执行一次emit过程中如果有回调被删除,是否会影响当前遍历
核心思路
用一个 Map 保存事件中心:
Map<eventName, Set<handler>>on:如果事件不存在,先创建一个Set,再加入回调emit:找到事件对应的回调集合,依次执行,并把参数透传off:从集合中删除指定回调;如果集合空了,删除事件名once:包装原回调,执行一次后先解绑,再执行原回调emit时拷贝一份回调数组,避免遍历过程中增删集合导致行为混乱
完整实现
EventEmmiters
class EventEmitter {
constructor() {
this.events = new Map()
}
on(eventName, handler) {
if (typeof handler !== 'function') {
throw new TypeError('handler must be a function')
}
if (!this.events.has(eventName)) {
this.events.set(eventName, new Set())
}
this.events.get(eventName).add(handler)
return () => {
this.off(eventName, handler)
}
}
off(eventName, handler) {
const handlers = this.events.get(eventName)
if (!handlers) {
return
}
handlers.delete(handler)
if (handlers.size === 0) {
this.events.delete(eventName)
}
}
emit(eventName, ...args) {
const handlers = this.events.get(eventName)
if (!handlers) {
return false
}
[...handlers].forEach((handler) => {
handler(...args)
})
return true
}
/**
* 1. 注册一个包装函数 wrapper
* 2. wrapper 执行时先把自己解绑
* 3. 再调用用户传入的原始回调
*/
once(eventName, handler) {
const wrapper = (...args) => {
this.off(eventName, wrapper)
handler(...args)
}
return this.on(eventName, wrapper)
}
clear(eventName) {
if (eventName === undefined) {
this.events.clear()
return
}
this.events.delete(eventName)
}
}Usage
const eventBus = new EventEmitter()
function say(name) {
console.log('hello', name)
}
const unsubscribe = eventBus.on('login', say)
eventBus.emit('login', 'byte') // hello byte
unsubscribe()
eventBus.emit('login', 'byte') // 不执行
eventBus.once('ready', () => {
console.log('ready once')
})
eventBus.emit('ready') // ready once
eventBus.emit('ready') // 不执行重要
发布订阅与观察者模式的区别在于:
- 观察者模式中被观察者直接持有观察者,双方知道彼此
- 发布订阅模式中发布者和订阅者通过事件中心通信,双方解耦
