行为型:发布订阅模式

news/2025/1/12 23:31:18/

定义  

  发布订阅模式是基于一个事件(主题)通道,希望接收通知的对象Subscriber(订阅者)通过自定义事件订阅主题,被激活事件的对象 Publisher (发布者)通过发布主题事件的方式通知订阅者 Subscriber (订阅者)对象。

  简单说就是发布者与订阅者通过事件来通信,这里的发布者是之前观察者模式中的被观察者,订阅者是观察者模式中的观察者,他们角色定位是等价的,只不过是不同的叫法。

发布订阅与观察者模式

  平时我们在微博中关注某个大v,这个大v 并不关心我这个订阅者具备什么特征,我只是通过微博这个平台关注了他,他也只是把他要分享的话题通过微博发出来,我和他之间并不存在直接的联系,然后我自动就能看到这个大v发布的消息,这就是发布订阅模式。

  发布订阅者模式与观察者模式类似,但是两者并不完全相同,发布订阅者模式与观察者相比多了一个中间层事件调度中心,用来对发布者发布的信息进行处理,再通知到各个特定的订阅者,大致过程如下图所示

发布者只是发布某事件,无需知道哪些订阅者,订阅者只需要订阅自己感兴趣的事件,无需关注发布者。

发布者完全不用感知订阅者,不用关心它怎么实现回调方法,事件的注册和触发都发生在独立于双方的第三方平台(调度中心)上,发布-订阅模式下,实现了完全地解耦。

通过之前对观察者模式的实现,我们的Subject类中是持有observer对象的,因此并没有实现两个类的完全解耦。通过添加中间层的调度中心类,我么可以将订阅者和发布者完全解耦,两者不再有直接的关联,而是通过调度中心关联起来。下面我们实现一个发布订阅者模式。

传统写法模拟发布订阅模式

按照上面思路,我们需要写下如下三个类,然后事件中心对象是发布者、订阅者之间的桥梁,我们很快写下如下代码:

  1. 发布者 ---- 观察者模式中的【被观察者】
  2. 订阅者 ---- 观察者模式中的【订阅者】
  3. 事件中心 ---- 类似公共的一个平台
 
/*
发布者:发布、注册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'); // 不会执行

小结

  1. 发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布订阅模式
  2. 发布者(被观察者)直接操作订阅者的操作,叫做观察者模式
  3. 发布订阅模式,发布者完全不用感知订阅者,不用关心它怎么实现回调方法,事件的注册和触发都发生在独立于双方的第三方平台(事件调度中心)上,发布-订阅模式下,实现了完全地解耦。
  4. 发布订阅核心通过事件来通信,在调度中心中派发给具体的订阅者。

 


http://www.ppmy.cn/news/990186.html

相关文章

Qt C++实现Excel表格的公式计算

用Qt的QTableViewQStandardItemModelQStyledItemDelegate实现类似Excel表格的界面,在parser 模块中提供解析表格单元格输入的公式。单元格编辑结束后按回车进行计算和更新显示。 效果如下: 支持的公式计算可以深度嵌套,目前parser模块中仅提…

lucene、solr、es的区别以及应用场景

目录 1. Lucene:2. Solr:3. Elasticsearch: Lucene、Solr 和 Elasticsearch(ES) 都是基于 Lucene 引擎的搜索引擎,它们之间有相似之处,但也有一些不同之处。 Lucene 是一个低级别的搜索引擎库,它提供了一种用于创建和维护全文索引的 API&…

电脑重启后VScode快捷方式失效,找不到Code.exe

问题描述 下班回家关了部分程序就直接关机了,回家后重启电脑发现vscode的快捷方式就失效了,提示Code.exe已被移动或删除。 解决方法 查看你的vscode安装目录,Microsoft VS Code目录下大概率会存在一个名为_的文件夹,然后会发现…

数据结构:树的存储结构

学习树之前,我们已经了解了二叉树的顺序存储和链式存储,哪么我们如何来存储普通型的树结构的数据?如下图1: 如图1所示,这是一颗普通的树,我们要如何来存储呢?通常,存储这种树结构的数…

WPF实战学习笔记29-登录数据绑定,编写登录服务

添加登录绑定字段、命令、方法 修改对象:Mytodo.ViewModels.ViewModels using Mytodo.Service; using Prism.Commands; using Prism.Events; using Prism.Mvvm; using Prism.Services.Dialogs; using System; using System.CodeDom.Compiler; using System.Collec…

重新理解RocketMQ Commit Log存储协议

最近突然感觉:很多软件、硬件在设计上是有root reason的,不是by desgin如此,而是解决了那时、那个场景的那个需求。一旦了解后,就会感觉在和设计者对话,了解他们的思路,学习他们的方法,思维同屏…

C# XML 的读写以及和JSON对比

通过我们进行跨平台传输,我们需要把某一个平台特有的数据类型转化为一种通用的数据类型序列化和反序列化 通用形式有两种: 《1》JSON:是一种以键值形式组成 《2》XML:可扩展标记语言 XML文件格式要求: 《1》头部需要有…

Android 设备兼容性使用详解

和你一起终身学习,这里是程序员Android 经典好文推荐,通过阅读本文,您将收获以下知识点: 一、设备兼容性分类二、硬件设备兼容三、软件 APP 兼容四、兼容不同语言五、兼容不同分辨率六、兼容不同屏幕方向布局七、兼容不同硬件 Feature八、兼容…