在软件架构中,发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。(来源: 发布/订阅 维基百科)
场景
发布-订阅
对于前端开发来讲是对比较常见的。以下的几种场景其实都是使用了发布-订阅
模式。
DOM
操作中的addEventListener
。Vue
中的事件总线的概念。Node.js
中的EventEmitter
以及内置库。
什么是发布-订阅模式
发布-订阅
是对象中的一种一对多的依赖关系,当一个对象触发一个事件的时候,所有订阅该事件的对象将得到通知。
举个例子,假如你在某平台订阅了某个专题,该系统会自动在该专题更新的时候,主动推送信息给到你,而不用你手动地去查阅。这个例子就类似发布-订阅模式。
上图有几个概念需要理清
发布者:通过事件中心派发事件
订阅者:通过事件中心进行事件的订阅
事件中心:负责存放事件和订阅者的关系.
如何实现一个发布-订阅模式
基础版本
思路:
- 我们的事件中心我们使用
Map
的结构来进行存储。Map
中,以事件名作为键
,而值
则是存放对应事件的订阅者. on
方法是事件订阅的方法,可以根据type
来找到对应的订阅者的集合,我们将传入的函数callback
添加到集合去。emit
方法是事件发布的方法,根据type
来找到对应的订阅者的集合,并依次调用订阅者。off
方法是取消订阅的方法,根据type
来找到对应的订阅者的集合,实质上是根据引用来匹配对应的订阅者,将其移出集合中。 代码如下:
class EventEmmiter {
subscribes: Map<string, Array<Function>>;
constructor() {
this.subscribes = new Map();
}
/**
* 事件订阅
* @param type 订阅的事件名称
* @param callback 触发的回调函数
*/
on(type: string, callback: Function) {
const sub = this.subscribes.get(type) || [];
sub.push(callback);
this.subscribes.set(type, sub);
}
/**
* 发布事件
* @param type 发布的事件名称
* @param args 发布事件的额外参数
*/
emit(type: string, ...args: Array<any>) {
const sub = this.subscribes.get(type) || [];
const context = this;
sub.forEach(fn => {
fn.call(context, ...args);
})
}
/**
* 取消订阅
* @param type 取消订阅的事件名称
* @param callback 取消订阅的具体事件
*/
off(type: string, callback: Function) {
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('eventName', () => {
console.log('第一次订阅');
});
eventEmmiter.emit('eventName');
const secondEmmiter = (a: number, b: number) => {
console.log(`第二次订阅, 结果为${a + b}`);
}
eventEmmiter.on('eventName', secondEmmiter);
eventEmmiter.emit('eventName', 1, 3);
eventEmmiter.off('eventName', secondEmmiter);
eventEmmiter.emit('eventName', 1, 3);
输出结果
第一次订阅
第一次订阅
第二次订阅, 结果为4
第一次订阅
上述结果符合预期,这时,我们一个比较简易版的发布-订阅
完成啦~
支持once订阅
我们使用过发布-订阅
都应该知道发布-订阅
直接一次订阅,即这个订阅者只会被通知一次。下面,我们来增加这个操作。
思路:我们的订阅者需要具有once
的属性,来进行判断,并且,在每一次emit
后,我们需要把once
事件清除。
基于以上的思路,我们需要改变我们订阅者的数据结构。
// 此前的订阅者 (实质上就是函数)
subscribes: Map<string, Array<Function>>;
// 现在的订阅者
interface SubscribeEvent {
fn: Function;
once: boolean;
}
subscribes: Map<string, Array<SubscribeEvent>>;
同时,我们发现once
和on
的逻辑只在于参数的不同,这一部分进行抽离。所以我们有了以下的代码
interface SubscribeEvent {
fn: Function;
once: boolean;
}
class EventEmmiter {
subscribes: Map<string, Array<SubscribeEvent>>;
constructor() {
this.subscribes = new Map();
}
addEvent(type: string, callback: Function, once: boolean = false) {
const sub = this.subscribes.get(type) || [];
sub.push({ fn: callback, once });
this.subscribes.set(type, sub);
}
on(type: string, callback: Function) {
this.addEvent(type, callback);
}
emit(type: string, ...args: Array<any>) {
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);
}
off(type: string, callback: Function) {
const sub = this.subscribes.get(type);
if(sub) {
const newSub = sub.filter(({ fn }) => fn !== callback);
this.subscribes.set(type, newSub);
}
}
once(type: string, callback: Function) {
this.addEvent(type, callback, true);
}
}
const eventEmmiter = new EventEmmiter();
eventEmmiter.on('eventName', () => {
console.log('第一次订阅');
});
eventEmmiter.once('eventName', () => {
console.log('once test')
});
eventEmmiter.emit('eventName');
eventEmmiter.emit('eventName');
输出的结果:
第一次订阅
once test
第一次订阅
上方,我们的once
订阅也支持了。
发布缓存
基于以上的代码,我们考虑一种情景,如果还没有订阅者订阅事件,但我们的事件中心发布了事件,那么这时候这次发布相当于无效的。这种情况,也许是可以接受的,但是,在某些特定的场景中,我们需要将发布的事件缓存起来,等到有订阅者订阅的时候,进行调用。
思路:使用_cacheQueue
来进行发布的事件的缓存,由于,我们事件发布的时候是有可能带参数的,所以,我们这个需要用一个集合/数组来存放这些参数。
type CacheArgs = Array<any>;
_cacheQueue: Map<string, Array<CacheArgs>>;
最终代码如下
interface SubscribeEvent {
fn: Function;
once: boolean;
}
type CacheArgs = Array<any>;
class EventEmmiter {
subscribes: Map<string, Array<SubscribeEvent>>;
_cacheQueue: Map<string, Array<CacheArgs>>;
constructor() {
this.subscribes = new Map();
this._cacheQueue = new Map();
}
addEvent(type: string, callback: Function, once: boolean = false) {
const cache = this._cacheQueue.get(type) || [];
if(cache.length !== 0) {
cache.forEach(args => {
callback(...args);
})
this._cacheQueue.delete(type);
}
const sub = this.subscribes.get(type) || [];
sub.push({ fn: callback, once });
this.subscribes.set(type, sub);
}
on(type: string, callback: Function) {
this.addEvent(type, callback);
}
emit(type: string, ...args: Array<any>) {
const sub = this.subscribes.get(type) || [];
if(sub.length === 0) {
const cache = this._cacheQueue.get(type) || [];
cache.push(args)
this._cacheQueue.set(type, cache);
} else {
const context = this;
sub.forEach(({ fn }) => {
fn.call(context, ...args);
});
const newSub = sub.filter(item => !item.once);
this.subscribes.set(type, newSub);
}
}
off(type: string, callback: Function) {
const sub = this.subscribes.get(type);
if(sub) {
const newSub = sub.filter(({ fn }) => fn !== callback);
this.subscribes.set(type, newSub);
}
}
once(type: string, callback: Function) {
this.addEvent(type, callback, true);
}
}
const eventEmmiter = new EventEmmiter();
eventEmmiter.emit('test_cache', 1, 2);
eventEmmiter.emit('test_cache', 1, 3);
eventEmmiter.on('test_cache', (a: number, b: number) => {
console.log("事件发布后才订阅的, 计算的值为",a + b);
});
eventEmmiter.on('test_cache', (a: number, b: number) => {
console.log("已没有发布事件的缓存了, 不会触发",a + b);
});
结果:
事件发布后才订阅的, 计算的值为 3
事件发布后才订阅的, 计算的值为 4
上述符合预期,这里,一个简易的发布-订阅
模式就完成了。
发布-订阅和观察者模式的区别
个人觉得这个区别,可能在于自己如何理解发布-订阅
和观察者模式
。
发布订阅模型属于广义上的观察者模式,对象中的一种一对多的依赖关系,当一个对象触发一个事件的时候,所有订阅该事件的对象将得到通知。
发布订阅模式是常用的一种观察者的模式的实现,并且从解耦和重用角度来看,更优于典型的观察者模式。
- 在观察者模式中,观察者需要直接订阅目标事件;在目标发出内容改变的事件后,直接接收事件并作出响应。
- 在发布订阅模式中,发布者和订阅者多了一个发布通道;一方面从发布者接收事件,另一方面向订阅者发布事件;订阅者需要从事件通道订阅事件。
总结
上文讲述了发布-订阅
的场景和简单的实现。有兴趣的小伙伴想更深入一点的话,可以去看看Node.js
的EventEmitter
,学习并将其应用在自己的业务场景中。
优点
- 模块间进行解耦,不强关联与特定的其他模块,只需订阅相关事件即可。
- 异步编程中,代码可以松耦合。
缺点
- 松耦合弱化对象间的关系,debug时程序可能难以追踪。
最后,感谢你阅读这篇文章,写的并非很深入,可能也存在一些小错误或笔误,也请见谅,可以在下方评论区评论,不胜感激。另外,如果本文对你有一点点帮助或启发,希望可以点个赞哈,支持是创作的动力~