定义
发布订阅模式是基于一个事件(主题)通道,希望接收通知的对象Subscriber
(订阅者)通过自定义事件订阅主题,被激活事件的对象 Publisher
(发布者)通过发布主题事件的方式通知订阅者 Subscriber
(订阅者)对象。
简单说就是发布者与订阅者通过事件来通信,这里的发布者是之前观察者模式中的被观察者,订阅者是观察者模式中的观察者,他们角色定位是等价的,只不过是不同的叫法。
发布订阅与观察者模式
平时我们在微博中关注某个大v,这个大v 并不关心我这个订阅者具备什么特征,我只是通过微博这个平台关注了他,他也只是把他要分享的话题通过微博发出来,我和他之间并不存在直接的联系,然后我自动就能看到这个大v发布的消息,这就是发布订阅模式。
发布订阅者模式与观察者模式类似,但是两者并不完全相同,发布订阅者模式与观察者相比多了一个中间层事件调度中心,用来对发布者发布的信息进行处理,再通知到各个特定的订阅者,大致过程如下图所示
发布者只是发布某事件,无需知道哪些订阅者,订阅者只需要订阅自己感兴趣的事件,无需关注发布者。
发布者完全不用感知订阅者,不用关心它怎么实现回调方法,事件的注册和触发都发生在独立于双方的第三方平台(调度中心)上,发布-订阅模式下,实现了完全地解耦。
通过之前对观察者模式的实现,我们的Subject
类中是持有observer
对象的,因此并没有实现两个类的完全解耦。通过添加中间层的调度中心类,我么可以将订阅者和发布者完全解耦,两者不再有直接的关联,而是通过调度中心关联起来。下面我们实现一个发布订阅者模式。
传统写法模拟发布订阅模式
按照上面思路,我们需要写下如下三个类,然后事件中心对象是发布者、订阅者之间的桥梁,我们很快写下如下代码:
- 发布者 ---- 观察者模式中的【被观察者】
- 订阅者 ---- 观察者模式中的【订阅者】
- 事件中心 ---- 类似公共的一个平台
/* | |
发布者:发布、注册xxx事件 or 主题 | |
订阅者:自己的行为,取消订阅,订阅 | |
事件中心:注册发布者的某事件、取消注册发布者的某事件、注册订阅者、取消订阅者、发布事件(通知订阅者) | |
*/ | |
// 发布者 | |
class Pulisher { | |
constructor (name, evtCenter) { | |
this.name = name; | |
this.evtCenter = evtCenter; | |
} | |
// 向事件调度中心-注册某事件 | |
register (evtName) { | |
this.evtCenter.registerEvt(evtName) | |
} | |
unregister (evtName) { | |
this.evtCenter.unRegisterEvt(evtName) | |
} | |
// 向事件调度中心-发布某事件 | |
publish (evtName, ...params) { | |
this.evtCenter.publish(evtName, ...params) | |
} | |
} | |
// 订阅者 | |
class Subscriber { | |
constructor (name,evtCenter) { | |
this.name = name; | |
this.evtCenter = evtCenter; | |
} | |
//订阅 | |
subscribe(evtName) { | |
this.evtCenter.addSubscribe(evtName, this); | |
} | |
//取消订阅 | |
unSubscribe(evtName) { | |
this.evtCenter.unAddSubscribe(evtName, this); | |
} | |
//接收 | |
update(params) { | |
console.log(`我接收到了,${params}`); | |
} | |
} | |
// 事件调度中心 | |
class EvtCenter { | |
constructor (name) { | |
this.name = name; | |
this.evtHandle = {} | |
} | |
// 注册发布者要发布的事件 | |
registerEvt (evtName) { | |
if (!this.evtHandle[evtName]) { | |
this.evtHandle[evtName] = [] | |
} | |
} | |
// 取消注册发布者要发布的事件 | |
unRegisterEvt (evtName) { | |
delete this.evtHandle[evtName]; | |
} | |
// 增加订阅者-注册观察者 | |
addSubscribe(evtName, sub) { | |
if (this.evtHandle[evtName]) { | |
this.evtHandle[evtName].push(sub); | |
} | |
} | |
// 取消订阅者-移除注册观察者 | |
unAddSubscribe(evtName, sub) { | |
this.evtHandle[evtName].forEach((item, index) => { | |
if (item === sub) { | |
this.evtHandle[evtName].splice(index, 1); | |
} | |
}); | |
} | |
// 事件调度中心-发布某事件 | |
publish (evtName, ...params) { | |
this.evtHandle[evtName] && this.evtHandle[evtName].forEach((item) => { | |
item.update(...params); | |
}); | |
} | |
} | |
// 测试 | |
const evtCenter1 = new EvtCenter('报社调度中心1') | |
const pulisher1 = new Pulisher('报社1', evtCenter1) | |
const sub1 = new Subscriber('我是sub1, 我对日报感兴趣', evtCenter1) | |
const sub2 = new Subscriber('我是sub2, 我对日报感兴趣', evtCenter1) | |
const sub3 = new Subscriber('我是sub3, 我对中报感兴趣', evtCenter1) | |
const sub4 = new Subscriber('我是sub4, 我对晚报感兴趣', evtCenter1) | |
// 发布者-注册三个事件到事件中心 | |
pulisher1.register('广州日报') | |
pulisher1.register('广州中报') | |
pulisher1.register('广州晚报') | |
// 订阅者可以自己订阅,当然也可以直接操作事件中心 | |
sub1.subscribe('广州日报') | |
sub2.subscribe('广州日报') | |
sub3.subscribe('广州中报') | |
sub4.subscribe('广州晚报') | |
// 现在开始发布事件 | |
pulisher1.publish('广州日报', '广州日报') | |
pulisher1.publish('广州中报', '广州中报') | |
pulisher1.publish('广州晚报', '广州晚报') | |
pulisher1.unregister('广州日报') | |
// 再一次发布事件 | |
console.log('再一次发布事件,这次我取消了日报') // 没有输出广州日报 | |
pulisher1.publish('广州日报', '广州日报') | |
pulisher1.publish('广州中报', '广州中报') | |
pulisher1.publish('广州晚报', '广州晚报') |
简单写法--面向事件调度中心编程
在js中函数是第一等公民,天生适合回调函数,所以可以直接面向事件调度中心编码即可。我们要做的事情其实就是触发什么事件,执行什么动作。
// 事件调度中心 | |
class PubSub { | |
constructor () { | |
this.evtHandles = {} | |
} | |
// 订阅 | |
subscribe (evtName, callback) { | |
if (!this.evtHandles[evtName]) { | |
this.evtHandles[evtName] = [callback]; | |
} | |
this.evtHandles[evtName].push(callback); | |
} | |
// 发布 | |
publish(evtName, ...arg) { | |
if (this.evtHandles[evtName]) { | |
for(let fn of this.evtHandles[evtName]) { | |
fn.call(this, ...arg); | |
} | |
} | |
} | |
unSubscribe (evtName, fn) { // 取消订阅 | |
let fnList = this.evtHandles[evtName]; | |
if (!fnList) return false; | |
if (!fn) { | |
// 不传入指定取消的订阅方法,则清空所有key下的订阅 | |
this.evtHandles[evtName] = [] | |
} else { | |
fnList.forEach((item, index) => { | |
if (item === fn) { | |
fnList.splice(index, 1); | |
} | |
}) | |
} | |
} | |
} | |
// 先订阅在发布 | |
const pub1 = new PubSub() | |
// 订阅三个事件 | |
pub1.subscribe('onWork', time => { | |
console.log(`上班了:${time}`); | |
}) | |
pub1.subscribe('onWork', time => { | |
console.log(`上班了:${time},开始打开待办事项`); | |
}) | |
pub1.subscribe('onOffWork', time => { | |
console.log(`下班了:${time}`); | |
}) | |
pub1.subscribe('onLaunch', time => { | |
console.log(`吃饭了:${time}`); | |
}) | |
// 发布对应的事件 | |
pub1.publish('onWork', '09:00:00'); | |
pub1.publish('onLaunch', '12:00:00'); | |
pub1.publish('onOffWork', '18:00:00'); | |
// 取消onWork 事件 | |
pub1.unSubscribe('onWork'); | |
// 取消订阅 | |
pub1.unSubscribe('onWork'); | |
console.log(`取消 onWork`); | |
pub1.publish('onWork', '09:00:00'); // 不会执行 |
小结
- 发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做
发布订阅模式
。 - 发布者
(被观察者)
直接操作订阅者的操作,叫做观察者模式
- 发布订阅模式,发布者完全不用感知订阅者,不用关心它怎么实现回调方法,事件的注册和触发都发生在独立于双方的第三方平台(事件调度中心)上,发布-订阅模式下,实现了完全地解耦。
- 发布订阅核心通过事件来通信,在调度中心中派发给具体的订阅者。