[设计模式]从零开始实现发布订阅模式
在前端学习中相信大家多多少少都会接触设计模式,虽然发布-订阅模式严格来说并不属于设计模式的一种,而属于观察者模式的一种常用实现,更优于典型的观察者模式,那如何实现呢,跟着我一步一步来实现吧。
什么是发布-订阅模式?
发布-订阅模式
是对象中一种一对多的依赖关系,当一个对象触发触发某一事件时,所有订阅该事件的对象都能得到通知。
举个例子,当我们在小说阅读平台追更了某一本小说时,当作者发布最新章节时,你就会收到更新的通知,换言之就是我们“读者”订阅了小说更新通知的这一事件,而作者更新章节也就是发布了事件,触发了通知。
![[设计模式]从零开始实现发布订阅模式](https://static.blogweb.cn/article/ec9118caebd2474c897ab61a68311dcb.webp)
用上面的例子我们就可以理清楚发布订阅模式中各个概念:
发布者
:通过调度中心发布事件(也就是作者通过小说平台更新最新章节)订阅者
:通过调度中心订阅事件(也就是读者通过小说平台接受通知并阅读最新章节)调度中心
:负责存放订阅者与事件之间关系(也就是小说平台这个桥梁)
如何实现发布订阅-模式?
首先我们要明确一个发布-订阅模式,里面应该包含四个方法
on(type,callback)
订阅事件,其中接收两个参数,type为订阅事件的事件名,callback为事件触发之后执行的回调函数;emit(type,...args)
发布事件,其中接收两个参数,type为发布的事件名,...args为传给订阅回调函数的参数;off(type,callback)
取消订阅事件,其中接收两个参数,type为取消订阅的事件名,callback 取消订阅的具体函数;once(type,callback,once)
只订阅一次事件,第一次事件发布执行回调之后直接取消订阅事件。
最基础版本
我们使用一个class类来实现
class EventEmmiter {
constructor() {
this.subscribes = new Map(); // 存放订阅的事件
}
// 事件订阅,type 订阅事件的名称, callback 触发的回调函数
on(type,callback) {
}
// 事件发布,type 发布事件的名称, args 发布事件的额外参数
emit(type,...args) {
}
// 取消订阅, type 取消订阅的事件名称, callback 取消订阅的具体函数
off(type,callback) {
}
}
实现on方法
on(type,callback) {
const sub = this.subscribes.get(type) || [];
sub.push(callback); // 将订阅事件放入事件数组中
this.subscribes.set(type,sub);
}
实现emit方法
emit(type,...args) {
const sub = this.subscribes.get(type) || [];
const context = this; // 用于将发布函数的作用域绑定到类中
sub.forEach(fn => {
fn.call(context,...args);
})
}
在实现发布方法时我们要明白一件事,订阅的回调函数都是发布订阅的实体类EventEmmiter
来调用的,所以我们要处理一下this的指向问题,每一个订阅事件的回调函数可能都不一样,所以我们在on事件中,是以一个对象的形式存储,订阅的事件为key值,而value是一个数组,数组中是该订阅事件需要执行的所有的回调函数,所以emit发布事件时需要我们遍历整个value数组,将this的指向使用call方法显示绑定一下。
实现off方法
off(type,callback) {
const sub = this.subscribes.get(type);
if(sub) {
const newSub = sub.filter(fn => fn !== callback);
this.subscribes.set(type,newSub);
}
}
此处还可以多一些细节,比如sub不存在,即没有与要取消的事件所对应的对象可以throw Error抛出一个错误,想实现比较完美的小伙伴可以自行加上。
最基础版本完整代码
class EventEmmiter {
constructor() {
this.subscribes = new Map(); // 存放订阅的事件
}
// 事件订阅,type 订阅事件的名称, callback 触发的回调函数
on(type,callback) {
const sub = this.subscribes.get(type) || [];
sub.push(callback); // 将订阅事件放入事件数组中
this.subscribes.set(type,sub);
}
// 事件发布,type 发布事件的名称, args 发布事件的额外参数
emit(type,...args) {
const sub = this.subscribes.get(type) || [];
const context = this; // 用于将发布函数的作用域绑定到类中
sub.forEach(fn => {
fn.call(context,...args);
})
}
// 取消订阅, type 取消订阅的事件名称, callback 取消订阅的具体函数
off(type,callback) {
const sub = this.subscribes.get(type);
if(sub) {
const newSub = sub.filter(fn => fn !== callback);
this.subscribes.set(type,newSub);
}
}
}
测试案例
const eventEmmiter = new EventEmmiter();
eventEmmiter.on('study',() => {
console.log('第一次订阅');
})
eventEmmiter.emit('study');
const SecondEvent = (month,day) => {
console.log('第二次订阅,时间为' + month + '月' + day + '日');
}
eventEmmiter.on('study',SecondEvent);
eventEmmiter.emit('study',3,8);
eventEmmiter.off('study',SecondEvent);
eventEmmiter.emit('study',3,8);
谷歌浏览器运行截图:
![[设计模式]从零开始实现发布订阅模式](https://static.blogweb.cn/article/0ed31d099393479f8518d1e23fd5ef32.webp)
实现once方法
once订阅一次与on订阅方法其实大差不差,所以为了代码的耦合性低一些,我们将订阅事件分出来,写一个addEvent方法,这样,代码的可读性也更起强了。但是在emit,off方法中也需要做一些微微的调整,因为在订阅事件时,如果是只订阅一次,那我就添加一个once字段,在emit方法中只要有once字段,我第一次发布执行完之后就自动取消订阅了。
addEvent(type,callback,once) {
const sub = this.subscribes.get(type) || [];
sub.push({fn:callback,once});
this.subscribes.set(type,sub);
}
on方法与once方法就相差一个参数而已
// 只订阅一次
once(type,callback) {
this.addEvent(type,callback,true);
}
on(type,callback) {
// const sub = this.subscribes.get(type) || [];
// sub.push(callback); // 将订阅事件放入事件数组中
// this.subscribes.set(type,sub);
this.addEvent(type,callback);
}
只是将on和once共同的逻辑分出一个addEvent方法来实现,在emit和off事件中因为回调函数的存储方式改成了对象,所以遍历数组时需要拿对象中的fn回调函数来判断。
完整版本代码
class EventEmmiter {
constructor() {
this.subscribes = new Map(); // 存放订阅的事件
}
addEvent(type,callback,once) {
const sub = this.subscribes.get(type) || [];
sub.push({fn:callback,once});
this.subscribes.set(type,sub);
}
// 事件订阅,type 订阅事件的名称, callback 触发的回调函数
on(type,callback) {
// const sub = this.subscribes.get(type) || [];
// sub.push(callback); // 将订阅事件放入事件数组中
// this.subscribes.set(type,sub);
this.addEvent(type,callback);
}
// 事件发布,type 发布事件的名称, args 发布事件的额外参数
emit(type,...args) {
const sub = this.subscribes.get(type) || [];
const context = this; // 用于将发布函数的作用域绑定到类中
sub.forEach(({fn}) => {
fn.call(context,...args);
})
const newSub = sub.filter(item => !item.once);
this.subscribes.set(type,newSub);
}
// 取消订阅, type 取消订阅的事件名称, callback 取消订阅的具体函数
off(type,callback) {
const sub = this.subscribes.get(type);
if(sub) {
const newSub = sub.filter(({fn}) => fn !== callback);
this.subscribes.set(type,newSub);
}
}
// 只订阅一次
once(type,callback) {
this.addEvent(type,callback,true);
}
}
测试用例
const eventEmmiter = new EventEmmiter();
eventEmmiter.once('notice',() => {
console.log('只订阅一次');
})
const publish = (a,b) => {
const sum = a + b
console.log(a,b+'两数之和为' + sum);
}
eventEmmiter.on('notice',publish);
eventEmmiter.emit('notice',2,3);
eventEmmiter.emit('notice',4,5);
![[设计模式]从零开始实现发布订阅模式](https://static.blogweb.cn/article/c42f0abbb0c6477f8404928bb4fd1e85.webp)
总结
以上就是笔者在学习过程中对于发布-订阅模式的认识与理解,在Vue的事件总线中也有着发布订阅模式,所以理解并运用好实用的设计模式对于我们的学习开发有着不小的帮助。
转载自:https://juejin.cn/post/7208188844615761978