简介
发布订阅模式是一种常用的用于解耦的模式。
它和观察者模式的区别在于:
- 观察者模式:被观察者需要维护一个观察者的集合;
- 发布订阅模式:通信双方互相不知道对方的存在,通过第三方事件总线进行通信。
发布订阅模式在前端领域很常见,例如:
- 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);
}
}
1.本站内容仅供参考,不作为任何法律依据。用户在使用本站内容时,应自行判断其真实性、准确性和完整性,并承担相应风险。
2.本站部分内容来源于互联网,仅用于交流学习研究知识,若侵犯了您的合法权益,请及时邮件或站内私信与本站联系,我们将尽快予以处理。
3.本文采用知识共享 署名4.0国际许可协议 [BY-NC-SA] 进行授权
4.根据《计算机软件保护条例》第十七条规定“为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存储软件等方式使用软件的,可以不经软件著作权人许可,不向其支付报酬。”您需知晓本站所有内容资源均来源于网络,仅供用户交流学习与研究使用,版权归属原版权方所有,版权争议与本站无关,用户本人下载后不能用作商业或非法用途,需在24个小时之内从您的电脑中彻底删除上述内容,否则后果均由用户承担责任;如果您访问和下载此文件,表示您同意只将此文件用于参考、学习而非其他用途,否则一切后果请您自行承担,如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。
5.本站是非经营性个人站点,所有软件信息均来自网络,所有资源仅供学习参考研究目的,并不贩卖软件,不存在任何商业目的及用途
暂无评论内容