发布订阅模式的TS实现
简介
发布订阅模式是一种常用的用于解耦的模式。
它和观察者模式的区别在于:
- 观察者模式:被观察者需要维护一个观察者的集合;
- 发布订阅模式:通信双方互相不知道对方的存在,通过第三方事件总线进行通信。
发布订阅模式在前端领域很常见,例如:
- Vue 框架中组件的
$on
和$emit
方法; - Node.js 中 EventEmitter 中的
on
和emit
方法。
图示:
-
订阅者通过
on
方法注册事件: -
发布者通过
emit
触发回调列表:发布者和订阅者双方不知道各自的存在,它们仅通过
Event Bus
进行通信。
实现
事件总线最基本的两个方法是 on
和 emit
。
常见的设计还有两个方法是 off
和 once
,off
用于注销事件,once
是 on
的特例,表示仅订阅一次。
函数签名
type CallbackFn = (...args: any[]) => void;
on(eventName: string, callback: CallbackFn): void;
emit(enentName: string, ...args: any): void;
off(eventName: string, callback: CallbackFn): void;
once(eventName: string, callback: CallbackFn): void;
实现思路
在事件总线中需要建立起事件名到回调集合的映射:
- 当
on
时,将回调添加到指定事件名的回调集合中; - 当
emit
时,遍历指定时间名的回调集合,依次执行其中的回调函数;
回调集合可以使用Set
实现,也可以使用数组实现。
由于on
的实现需要做去重,建议使用Set,比较方便。
once的实现:
once 可以基于 on 和 off 实现,先使用 on 注册,执行回调之后就执行 off 注销,从而实现仅触发一次。
代码
使用 TypeScript 实现。
首先声明一个回调函数的类型,简化后续代码:
type CallbackFn = (...args: any[]) => void;
然后是声明 EventBus 类,成员属性中使用对象建立起 “事件名与回调集合” 的映射关系:
class EventBus{
events: Record<string, Set<CallbackFn>> = {};
constructor(){}
on(eventName: string, callback: CallbackFn){ /* ... */ }
emit(eventName: string, ...args: any[]){/* ... */}
off(eventName: string, callback: CallbackFn){/* ... */}
once(eventName: string, callback: CallbackFn){/* ... */}
}
on
on(eventName: string, callback: CallbackFn){
if(!this.events[eventName]){
this.events[eventName] = new Set();
}
this.events[eventName].add(callback);
}
- 如果事件名不存在,则要初始化创建一个 Set 用于记录。
- 如果事件名存在,则直接将回调添加到 Set 中。
这段代码可以通过 短路运算符 简化:
on(eventName: string, callback: CallbackFn){
(this.events[eventName] ??= new Set()).add(callback);
}
emit
遍历回调集合就🆗了。(记得带上函数参数)
emit(eventName: string, ...args: any[]){
this.events[eventName]?.forEach(cb => cb(...args));
}
off
注销事件也很简单,使用 Set 的 delete 方法就可以了。
off(eventName: string, callback: CallbackFn){
this.events[eventName]?.delete(callback);
}
once
对 once 传入的回调函数做一层包装,在执行之后调用 off 注销事件。
使用箭头函数是为了 this 指向正确。使用 function 和 bind 也🆗,箭头函数比较简洁。
once(eventName: string, callback: CallbackFn){
const handler = (...args: any[]) => {
callback(...args);
this.off(eventName, handler);
}
this.on(eventName, handler);
}
代码汇总:
type CallbackFn = (...args: any[]) => void;
class EventBus{
events: Record<string, Set<CallbackFn>> = {};
constructor(){}
on(eventName: string, callback: CallbackFn){
(this.events[eventName] ??= new Set()).add(callback);
}
emit(eventName: string, ...args: any[]){
this.events[eventName]?.forEach(cb => cb(...args));
}
off(eventName: string, callback: CallbackFn){
this.events[eventName]?.delete(callback);
}
once(eventName: string, callback: CallbackFn){
const handler = (...args: any[]) => {
callback(...args);
this.off(eventName, handler);
}
this.on(eventName, handler);
}
}