设计模式 —— 发布订阅模式

article/2025/10/27 3:18:21

设计模式 —— 发布订阅模式

《工欲善其事,必先利其器》

banner

我在之前有写过一篇关于 《观察者模式》 的文章,大家有兴趣的可以去看看,个人认为那个例子还是挺生动的。(狗头)

不过今天我们要学习的是,发布订阅模式。那么话不多说,我们开始!

一、什么是发布订阅模式?

发布订阅模式,听起来好像很陌生?但其实我们在工作之中经常有它的射影,例如:

  • Vue 中的 EventBus, $on 以及 $emit 和 $off;
  • Nodejs 中的 EventEmitter,其中 on 和 emit;
  • MQTT 中的 Topic,也是应用了此设计模式。

可见,虽然设计模式在日常的业务开发中可能用到的地方并不多,但是一门优秀的框架,其根本上是离不开设计模式数据结构算法的,这两者对于程序员的编程思想有着举足轻重的意义,我们依旧还是有学习它的必要。

那么发布订阅模式观察者模式又有什么区别呢?如下图,我们可以直观的观察到,这两种模式之间的区别:

观察者模式和发布订阅模式的区别

发布订阅模式,发布者和订阅者是间接的关系。他们之间其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知,前提必须是,订阅者和发布者任何一方触发了同一个主题。本质上,发布者是否发布,订阅者是无感的(完全解耦)。只有发布者发布的内容是订阅者订阅的主题,订阅者才会收到通知。

观察者模式 ,观察者和被观察者之间是直接的关系,被观察者的变化都会影响或通知到另外一方。从上图看,我们可以认为:

  • 观察者模式包含着发布订阅模式
  • 两者都拥有通知客户的能力,发布订阅模式由调度中心分配,没有直接关联,相当于观察者模式的升级版
  • 观察者模式主体在发生事件时与客体是松解耦的,不需要感知到客体具体的行为,进行统一的update,但主体还是需要感知到客体的存在,初始化时要预先attach到观察者。发布订阅模式主体不需要感知到客体的任何行为或存在,主体和客体通过事件关联,解耦性更强

二、为什么会有这两种设计模式?

额。。好问题。。。

无语

个人认为,有时候设计模式就是为了解耦

例如,Vue 中数据的双向绑定。假如我某个Object发生了变化,其中,我有很多个子组件都双向绑定了这个对象的不同字段。那么其实,我的 Object 中只需要 update 其中一个字段,则对应的子组件中的字段就会发生改变,这个过程中观察其他字段的子组件,是无感的。

反之,如果全部都集中管理这些字段,假如这时候我新增了一个字段或删减了一个字段,那么我就需要重写这部分的代码。而且每触发一次字段,所有的子组件都要被通知一遍,这对性能的损耗无疑是巨大的。这就是高度耦合的不好的地方。

解耦 的思想,对于程序员来讲也是非常重要的。因此,就有了这两种设计模式

三、如何实现发布订阅模式?

备注,以下代码都在 Node 环境下编写的,有需要的小伙伴自行查阅文末的 git。

举例: 实现一个 EventBus。就是所谓的 事件总线模式,其实就和发布订阅模式非常类似,比如我们关注了一个作者,作者发布文章之后我们就能收到信息,这就是一种订阅发布的关系。

export default class EventBus {constructor() {this.eventId = 0;this.eventLine = {};}$on(eventName, handler) {this.eventId++;if (!this.eventLine[eventName]) this.eventLine[eventName] = {};this.eventLine[eventName][this.eventId] = handler;console.log(eventName + "新增了一位粉丝!!");return this.eventId;}$emit(eventName, ...args) {const eventHandlers = this.eventLine[eventName];for(const id in eventHandlers) {eventHandlers[id](...args);}}$off(eventName, key) {delete this.eventLine[eventName][key];if (!Object.keys(this.eventLine[eventName]).length) delete this.eventLine[eventName];console.log("一位粉丝取消了关注");}
}

然后,实例化事件总线,并模拟一下场景:

import EventBus from "EventBus.js";const eventBus = new EventBus;
// 关注
const key1 = eventBus.$on("vk哥", (articleName) => {// 张三关注了你,有发布新文章请通知它console.log("vk哥发布新文章了!!—— 《" + articleName + "》,通知了张三。");
})
const key2 = eventBus.$on("vk哥", (articleName) => {// 李四关注了你,有发布新文章请通知它console.log("vk哥发布新文章了!!—— 《" + articleName + "》,通知了李四。");
})
// 发布
eventBus.$emit("vk哥", "设计模式——发布订阅模式");
// 取消关注
eventBus.$off("vk哥", key2);
// 取消关注后再次发布
eventBus.$emit("vk哥", "Vue2.0源码剖析");

然后,让我们看看效果:

效果

可见,发布者和订阅者通过同一个主题,也就是 “vk哥”,来绑定关系的。换句话说,发布者是否发布与订阅者是否订阅,没有直接的关联,达到了完全解耦的目的。

四、Vue 源码中的发布订阅模式

温馨提示:含有英文的注释都是源码的原注释,中文的注释才是我自己理解的注释。

Vue 里面,发布订阅模式就体现在它自带的事件总线的方法:

  • Vue.$on
  • Vue.$once
  • Vue.$off
  • Vue.$emit

所以接下来,我们就这四个核心方法进行分析。

eventsMixin 里面:

  • Vue.prototype.$on
Vue.prototype.$on = function (event: string | Array<string>,fn: Function
): Component {const vm: Component = this// 判断传入的主题是否为数组,如果是,则遍历数组为每一个主题都添加 fnif (isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {vm.$on(event[i], fn)}} else {// 如果不是,就直接为当前主题添加 fn;(vm._events[event] || (vm._events[event] = [])).push(fn)// optimize hook:event cost by using a boolean flag marked at registration// instead of a hash lookup// 这里是判断是否为子组件注入额外的声明周期钩子, 可以选择不看if (hookRE.test(event)) {vm._hasHookEvent = true}}return vm
}
  • Vue.prototype.$once
Vue.prototype.$once = function (event: string, fn: Function): Component {const vm: Component = this// 把订阅和取消订阅封装成一个新函数对象function on() {// 取消订阅当前函数对象 onvm.$off(event, on)// 回调执行 fnfn.apply(vm, arguments)}// 为新函数对象的 fn 赋值on.fn = fn// 用新函数对象 on 订阅主题vm.$on(event, on)return vm
}
  • Vue.prototype.$off
Vue.prototype.$off = function (event?: string | Array<string>,fn?: Function
): Component {const vm: Component = this// all// 如果没有传入参数,则将所有的主题全部清空为一个空对象// Object.create(null) 是创建一个空对象if (!arguments.length) {vm._events = Object.create(null)return vm}// array of events// 判断是否主题为数组,如果是则遍历取消订阅if (isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {vm.$off(event[i], fn)}return vm}// specific event// vm._events[event!] 是强解析,判断必有 event 参数的情况,这是 typescript 语法const cbs = vm._events[event!]if (!cbs) {// 有 event 参数,但是已经没有 callback 了return vm}// 如果没有 fn 参数,则设置主题为 null,并不删除主题if (!fn) {vm._events[event!] = nullreturn vm}// specific handler// 如果有 callback 的情况,就遍历,一个个从数组删除取消订阅// 用 while 则是防止意外的错误let cblet i = cbs.lengthwhile (i--) {cb = cbs[i]if (cb === fn || cb.fn === fn) {cbs.splice(i, 1)break}}return vm
}
  • Vue.prototype.$emit
Vue.prototype.$emit = function (event: string): Component {const vm: Component = thisif (__DEV__) {const lowerCaseEvent = event.toLowerCase()if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {tip(`Event "${lowerCaseEvent}" is emitted in component ` +`${formatComponentName(vm)} but the handler is registered for "${event}". ` +`Note that HTML attributes are case-insensitive and you cannot use ` +`v-on to listen to camelCase events when using in-DOM templates. ` +`You should probably use "${hyphenate(event)}" instead of "${event}".`)}}// 获取 callbackslet cbs = vm._events[event]if (cbs) {// 如果 callback 的长度大于1,就把 vm._events[event] 整理成数组,方便循环cbs = cbs.length > 1 ? toArray(cbs) : cbs// 这里是将参数的第一项 eventName 去除const args = toArray(arguments, 1)// 定义错误提示const info = `event handler for "${event}"`// 遍历把每个 callback 都执行一次for (let i = 0, l = cbs.length; i < l; i++) {// 这个是错误捕获函数,一旦报错会把 info 抛出,但并不会让整个js进程奔溃invokeWithErrorHandling(cbs[i], vm, args, vm, info)}}return vm
}

以上仅仅是部分代码,我只是取出了 Event 的核心,也就是本文所讲的 发布订阅模式 而已。相信结合我的注释看应该能理解,如果有兴趣的小伙伴也可以自己看一下源码的伟大!!!(狗头)

五、Vue 事件总线的弊端

针对全局的组件通信,个人建议最好就不用 EventBus 。为什么?

因为一旦跨组件通信,最大的问题就是事件来源不明确,如果不是自己写的代码,其他人并不知道这东西在哪触发的,啥时候触发,会触发多少次?归根结底就是一个管理困难的问题,没有一个直观的调用顺序,维护起来非常之困难。如果在这个模块上出现问题,那么排查起来将会是灾难级别的。

但是,仁者见仁智者见智吧,我的观点就是 存在即合理。尽可能的做到:不弃用、不滥用。

最后,感谢你的阅读,希望我的文章能够帮到你,愿你的未来一片光明。


http://chatgpt.dhexx.cn/article/oQR7Bcy8.shtml

相关文章

小侃设计模式(十八)-发布订阅模式

1.概述 发布订阅模式又叫观察者模式&#xff08;Observer Pattern&#xff09;&#xff0c;它是指对象之间一对多的依赖关系&#xff0c;每当那个特定对象改变状态时&#xff0c;所有依赖于它的对象都会得到通知并被自动更新&#xff0c;它是行为型模式的一种。观察者模式内部…

发布-订阅模式

发布-订阅模式 学习知识要善于思考&#xff0c;思考&#xff0c;再思考。 —— 爱因斯在众多设计模式中&#xff0c;可能最常见、最有名的就是发布 - 订阅模式了&#xff0c;本篇我们一起来学习这个模式。 发布 - 订阅模式 &#xff08;Publish-Subscribe Pattern, pub-sub&a…

什么是发布订阅模式?

发布-订阅模式&#xff08;Publish-Subscribe pattern&#xff09;是一种软件架构模式&#xff0c;用于实现组件之间的解耦和消息传递。在这种模式中&#xff0c;组件&#xff08;发布者&#xff09;将消息发送到一个中心&#xff08;消息代理或主题&#xff09;&#xff0c;然…

发布订阅模式

零、目录 应用场景实现原理代码实现全局模式下的订阅发布模式&#xff08;泛化的订阅发布模式&#xff09;总结 一、应用场景 ​ 发布订阅模式&#xff0c;广泛的存在于在我们的生活之中。 ​ 举个一个简单的例子来说&#xff0c;当我们在浏览视频或者博客论坛之类的网…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:VLCPlayer

本文简述如何在Smobiler中使用VLCPlayer插件&#xff0c;该插件支持播放rtsp流。 Step 1. 新建一个SmobilerForm窗体&#xff0c;再拖入VLCPlay&#xff0c;布局如下 在设计器中给VLCPlayer.Url赋值或者在窗体的Load事件中赋值 演示使用的rtsp流地址 rtsp://wowzaec2demo.strea…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:TTS

本文简述如何在Smobiler中使用TTS文字转语音。 Step 1. 新建一个SmobilerForm窗体&#xff0c;并在窗体中加入TTS和Button&#xff0c;布局如下 Button的点击事件代码&#xff1a; private void button1_Press(object sender, EventArgs e){ //第一个参数为文本&#xff1b;第…

Smobiler 仿得到APP个人主页

原型如下&#xff1a; 完整代码参考 https://github.com/comsmobiler/BlogsCode/blob/master/Source/BlogsCode_SmobilerForm/MyForm/dedao.cs 思路 可以将原型按照上图分成2个部分&#xff0c;部分A可以使用label、image、button、imagebutton、fontIcon控件来实现&#xff…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:PDFView

本文简述如何在Smobiler中使用PDFView。 Step 1. 新建一个SmobilerForm窗体&#xff0c;再拖入PDfView&#xff0c;布局如下 PDFView.ResourcrPath默认Document&#xff0c;指项目下\Resources\Document&#xff0c;若是pdf文件放在该文件夹下&#xff0c;则在设计器中直接赋值…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:OCR组件

本文简述如何在Smobiler中使用OCR组件进行文字识别。 Step 1. 新建一个SmobilerForm窗体&#xff0c;并在窗体中加入OCR和Button&#xff0c;布局如下 Button的点击事件代码&#xff1a; private void button1_Press(object sender, EventArgs e){ocr1.Recognize((obj,args)>…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:ArcFace人脸识别

本文简述如何在Smobiler中使用ArcFace&#xff08;虹软人脸识别&#xff09;。 Step 1. 新建一个SmobilerForm窗体&#xff0c;再拖入Button,Label,TextBox和AcrFace,布局如下 在设计器中给MediaView.Url赋值或者在窗体的Load事件中赋值 Button的事件代码如下 string message …

移动OA办公——Smobiler第一个开源应用解决方案,快来get吧

产品简介 SmoONE是一款移动OA类的开源解决方案&#xff0c;通过Smobiler平台开发&#xff0c;包含了注册、登陆、用户信息等基本功能。集成了OA中使用场景较多的报销、请假、部门管理、成本中心等核心功能。 免费获取方案 开源代码&#xff1a;https://github.com/comsmobile…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:UsbSerial串口通讯组件

本文简述如何在Smobiler中使用UsbSerial。 Step 1. 新建一个SmobilerForm窗体&#xff0c;再拖入UsbSerial和Button&#xff0c;布局如下 按钮事件代码&#xff1a; //连接private void button1_Press_2(object sender, EventArgs e){usbSerial1.Connect(Smobiler.Plugins.USBS…

Smobiler实现手机弹窗

前言 在实际项目中有很多场景需要用到弹窗&#xff0c;如图1 那么这些弹窗在Smobiler中如何实现呢&#xff1f; 正文 Smobiler实现弹窗有两种方式&#xff1a;1.MessageBox.Show 2.ShowDialog和ShowContextDialog。前者适合简易弹窗&#xff0c;后者适合自定义弹窗。 Messa…

Smobiler实现美观登录界面——C# 或.NET Smobiler实例开发手机app(二)

目录 一、 本文目标 二、 准备工作 1、 数据库 2、 材料 三、 界面布局 1、设置控件的属性值 &#xff08;1&#xff09; 输入框 &#xff08;2&#xff09; 图片属性 &#xff08;3&#xff09; HandElectricity的标题的label属性 &#xff08;4&#xff09;登录按钮…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:MediaView

本文简述如何在Smobiler中使用MediaView。 Step 1. 新建一个SmobilerForm窗体&#xff0c;再拖入MediaView&#xff0c;MediaView.Size设置&#xff08;300,225&#xff09;,布局如下 在设计器中给MediaView.Url赋值或者在窗体的Load事件中赋值 播放本地视频可以通过GetResourc…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:AliPay组件

本文简述如何在Smobiler中调用支付宝支付。 Step 1. 界面 新建一个窗体&#xff0c;并在窗体中拖入Button&#xff0c;Label&#xff0c;AliPay等控件&#xff0c;布局如下&#xff1a; Step 2. 代码 在窗体中声明变量 //订单编号private string tradeNo;//支付宝应用编号&am…

.NET(C#、VB)APP开发——Smobiler平台控件介绍:BarcodeReader组件

本文简述如何在Smobiler中使用BarcodeReader组件进行条码识别。Barcodereader通过机器学习能识别不规则条码&#xff0c;效率更好。 Step 1. 新建一个SmobilerForm窗体&#xff0c;并在窗体中加入Barcodereader和Button&#xff0c;布局如下 Button的点击事件代码&#xff1a; …

.NET(C#、VB)APP开发——Smobiler平台控件介绍:LiveStream和LiveStreamPlayer

本文简述如何在Smobiler中使用LiveStream和LiveStreamPlayer。 LiveStream 直播推送插件 Step 1. 新建一个SmobilerForm窗体&#xff0c;并在窗体中加入LiveStream和Button&#xff0c;布局如下 选中LisvStream&#xff0c;在设计器中设置Url&#xff08;需要事先准备一个视频…

【转载】smobiler说明

类似开发WinForm的方式&#xff0c;使用C#开发Android和IOS的移动应用&#xff1f;听起来感觉不可思议&#xff0c;那么Smobiler平台到底是如何实现的呢&#xff0c;这里给大家介绍一下。 客户端 Smobiler分为两种客户端&#xff0c;一种是开发版&#xff0c;一种是打包版 开发…

.NET(C#)能开发出什么样的APP?盘点那些通过Smobiler开发的移动应用

.NET程序员一定最熟悉所见即所得式开发,熟悉的Visual Studio开发界面,熟悉的C#代码。 Smobiler也是因为具备这样的特性,使开发人员,可以在VisualStudio上,像开发WinForm一样拖拉控件,让许多人在开发APP时,再次回到所见即所得的开发方式中去。 Smobiler的快速开发,让Ama…