发布订阅模式
在 JavaScript
中,发布订阅模式(也称为观察者模式)是一种常用的设计模式,可以让不同对象之间以一种松耦合的方式进行通信。这种模式在实际项目中具有广泛的应用,可以提高代码的可维护性和灵活性。本文将通过通俗易懂的语言,深入探讨 JavaScript
中的发布订阅模式,介绍其基本概念和用法,并通过具体的代码示例来演示如何在实际项目中应用这种设计模式。
概要是用
chatgpt
生成的 ~ 觉得如何呢 ~
1. 什么是发布订阅模式 ?
发布订阅模式是一种对象间一对多的依赖关系(利用消息队列),可以让对象之间轻松通信。
- 一对多意味着
n
个订阅者对应1
个消息队列
- 一对多意味着
羞涩难懂 ~ 简单来说就是一种便于对象间(发布者与订阅者)通信的手段或方式 ~
1.1 基本概念
让我们一起想象一下 ~
有一个发布者(
publisher
),他的职责是维护(添加、删除)一个订阅者列表,并在特定事件发生时通知所有已注册的订阅者。有多个订阅者(
subsrciber
),他们的职责是注册(或制定)特定事件发生时执行特定的操作(回调函数)。有一个触发器,叫做事件(或消息),它作为发布者和订阅者之间的联结,发布者负责触发事件,并通知所有的订阅者指定事件发生,从而让订阅者执行特定操作。
怎么多出一个消息队列 ~
可以这么说,消息队列就是个中介 ~
订阅者既然要订阅事件,那么需要把自己想订阅的事件注册到消息队列中
作为发布者,发布该事件到消息队列中,也就是该事件触发时,由消息队列统一调度(执行)订阅者注册到消息队列的代码。
1.2 基本用途
- 发布订阅模式广泛地应用于
JavaScript
客户端编程中,所有的浏览器事件(mouseover
,keypress
等)都是使用该模式的例子。
1.3 优点
松耦合
- 对象之间解耦,它们不需要直接相互引用或依赖,从而使得系统更加灵活、可维护和可扩展。
可复用性
- 发布订阅模式可以在多个地方使用,让不同的订阅者可以订阅同一个事件,从而实现功能的复用。
可扩展性
- 发布者和订阅者可以动态地增加或删除,不影响彼此的功能,从而方便地扩展系统。
解耦逻辑
- 通过将订阅者的逻辑封装在回调函数中,可以将不同的业务逻辑分离开来,使得代码更加清晰、可维护。
异步处理
- 发布订阅模式可以方便地处理异步事件,例如在异步请求完成后通知订阅者更新
UI
。
- 发布订阅模式可以方便地处理异步事件,例如在异步请求完成后通知订阅者更新
2. 发布订阅模式的特征
我们先定义声明消息队列
message
,有需要的对象(或订阅者)obj
去订阅那么,消息队列在
JavaScript
中可以用数组模拟[fn1,fn2,fn3]
需要注意的是:对象不是主动触发,而是被动触发! 这也决定了该设计模式的优势之处 !
举个栗子来理解吧 ~
小张去书店买书,去书店问:“没有”,回家。过一会再去问,没有,回家。过一会又再去问:没有,回家。
相当于在实际开发场景中,频繁的调用方法(轮询)。
- 例:使用
websocket
长连接,setInterval
... - 缺点:网络负担大,服务器开销大。
- 例:使用
小张去书店买书,问:没有,先订阅(on),就是给书店店员联系方式,一旦有了书,店员就是发送一条消息,然后手机响了触发消息队列叫小张去买书,书买着了,联系方式不需要了。
相当于在实际开发场景中,使用发布订阅模式。
addEventListener()
本质上就是一个发布订阅模式的应用,比如绑定点击事件,不是主动触发方法,而是状态发生变化(点击)被动触发回调函数。
3. 实现一个简单的发布订阅模式
整体实现思路
创建一个类
class --> Observer
在这个类里创建一个消息队列
message
on
方法 - 用于将回调函数fn
添加到消息队列(订阅者注册事件到这里)emit
方法 - 取到event
事件类型,根据event
值去执行对应消息列表中的函数(发布者发布事件到消息队列,消息队列处理代码)off
方法 - 可以根据event
事件类型取消订阅(取消订阅)
3.1 搭建基本结构
简化版
/*
分析构造函数
属性:消息队列
{
"click": [fn1, fn2, fn3],
"xxx": [handlerA, handlerB],
//...
}
能向消息队列里面添加内容 $on
删除消息队列中的内容 $off
触发消息队列中内容 $emit
*/
class Observer {
constructor() { }
$on() { }
$off() { }
$emit() { }
}
3.2 初始化消息队列
class Observer {
constructor() {
this.message = {
// "红宝书": [handlerA, handlerB]
}
}
$on() { }
$off() { }
$emit() { }
}
3.3 $on 方法编写
- 用于将回调函数
fn
添加到消息队列this.message
实际上是为
type
绑定相关事件。类似于:btn.addEventListener("click",fn)
//todo...
$on(type,fn) {
// 先判断消息队列中是否有 type 属性
// 有 --> 直接添加到消息队列 (使用 push...)
// 没有 --> 为 type 属性初始化一个空数组
if(!this.message[type]) {
this.message[type] = [];
}
this.messagep[type].push(fn);
}
//todo...
检验一下!
// 使用构造函数创建一个实例
const p1 = new Observer();
p1.$on("红宝书", handlerA);
p1.$on("红宝书", handlerB);
function handlerA() {
console.log("handlerA");
}
function handlerB() {
console.log("handlerB");
}
没问题!
![](https://raw.githubusercontent.com/tcSteamedEggs/my-blog-pic/main/检验1.png)
3.4 $off 方法编写
- 根据
event
事件类型取消订阅(取消订阅)
// todo...
$off(type, fn) {
// 首先判断是否订阅,没有订阅直接返回或报错异常
// 再判断是否 fn 为 空(undefined)
// 是 --> 删除 type 全部事件
// 不是就仅删除这个函数方法
if (!this.message[type]) {
return false;
// return new Error('没有订阅 type...');
}
if (!fn) {
// delete this.message[type]
this.message[type] = undefined;
return;
} else {
// 删除 --> 过滤掉这个方法或其他方法...
this.message[type] = this.message[type].filter(item => item !== fn);
}
}
// todo...
验证(略)
3.5 $emit 方法编写
- 获取到
event
事件类型,根据event
值去执行对应缓存列表中的函数(发布者发布事件到调度中心,调度中心处理代码)
实际上就是去执行回调函数!
$emit(type) {
if (!this.message[type]) {
return false;
}
this.message[type].forEach(item => {
//遍历并执行
item();
}
}
检验一下!
const p1 = new Observer();
p1.$on("红宝书", handlerA);
p1.$on("红宝书", handlerB);
p1.$on("红宝书", handlerC);
p1.$emit("红宝书");
![](https://raw.githubusercontent.com/tcSteamedEggs/my-blog-pic/main/检验2.png)
是不是很简单!实践一下吧!
4. 实际场景运用
4.1 杂志订阅
假设有一个发布者
paper
,它发行一个日报和月刊。无论是日报还是日刊,有一个名为jack
的订阅者都会收到通知。paper
对象有一个subscribers
属性,是一个数组,用于保存所有的订阅者。订阅的过程就是仅将订阅者放入数组。当一个事件发生时,
paper
遍历这个订阅者列表,然后通知它们。通知其实就是调用订阅者对象的一个方法。因此,在订阅的过程中,订阅者需要提供一个方法给paper
对象的subsrcibe
。因为这些成员方法对任何对象都是通用的,因此可以作为一个单独的、公共的对象如
publisher
提取出来,可以通过混元模式将它们复制到任何对象当中,比如paper
对象。
// 做法很多 类 构造函数 对象(我们使用)
// 发布者是一个对象 我们写一个具有通用功能的发布者
var publisher = {
// 用于保存所有的订阅者
subscribers: {
any: []
// 默认事件类型 any
// "红宝书": [handlerA, handlerB]
},
// 类似于 Observer 中的 $on
subscribe: function(type, fn) {
type = type || "any";
if(typeof this.subscribers[type] === 'undefined') {
this.subscribers[type] = []
}
this.subscribers[type].push(fn);
}
}
paper
对象也可以提供unsubscribe()
方法,它用于将订阅者从数组者移除。还有一个方法publish()
,用于调用订阅者的方法。- 定义辅助方法
visitSubscribers
{
// todo...
// 辅助方法 visitSubscribers 辅助 publish 和 unsubscribe 也可以不使用辅助方法 类似于 3.
visitSubscribers: function(action, arg, type) {
type = type || "any";
// action 行为 publish/unsubscribe
// arg 参数 publication/fn
// type “要绑定的事件”
var subscribers = this.subscribers[type];
// subscribers 订阅 type 的订阅者数组
var max = subscribers.length;
for(var i = 0; i < max; i++) {
if(action === 'publish') {
// 依次调用订阅者的方法
// arg 为 publication
subscribers[i](arg);
} else {
// action 为 unsubscribe
if(subscribers[i] === arg) {
// arg 为 fn
//找到这个要剔除的方法
subscribers.splice(i,1);
}
}
}
}
// todo...
}
publish
和unsubscribe
方法编写,借助辅助方法
{
// todo...
publish: function(publication,type) {
// 调用辅助方法
this.visitSubscribers("publish",publication,type);
},
unsubscribe: function(type,fn) {
this.visitSubscribers("unsubscribe",fn,type);
}
// todo...
}
- 使用混元模式(简单理解就是将任意多数量的对象中复制属性。然后将它们混在一起组成一个新对象),通过复制通过发布者的方法将如
paper
对象转变为发布者。
function makePublisher(obj) {
for(var i in publisher) {
//原型属性不需要 只需要方法
if(publisher.hasOwnProperty(i) && typeof publisher[i] === 'function') {
obj[i] = publisher[i];
}
}
// 注意要添加 subscribers 属性,没有会报错
obj.subscribers = { any: [] };
}
- 实现
paper
对象,发布日报和月刊,并变成发布者
var paper = {
daily: function() {
this.publish("日报来咯");
},
monthly: function() {
this.publish("月刊来咯","monthly");
}
}
makePublisher(paper);
- 声明订阅者
jack
,用于订阅paper
var jack = {
// 有两个方法
drinkCoffee: function(paper) {
console.log('喝着咖啡,' + paper);
},
sundayPreNap: function(monthly) {
console.log('睡觉前,' + monthly);
}
}
// jack 订阅 paper
paper.subscribe('monthly', jack.sundayPreNap);
/* 相当于
this.subscribers = {
"monthly": [ sundayPreNap ]
}
*/
paper.subscribe(undefined, jack.drinkCoffee);
/* 相当于
this.subscribers = {
"any": [ drinkCoffee ],
"monthly": [ sundayPreNap ],
}
*/
- 触发事件
paper.monthly();
// monthly 事件发生时触发
// 会调用 publish 方法 --> 相当于执行 sundayPreNap
// "睡觉前,月刊来咯"
paper.daily();
// any 事件发生时触发 (默认事件)
// 会调用 publish 方法 --> 相当于执行 drinkCoffee
// "喝着咖啡,日刊来咯"
// 这里的 publish 方法我们不是直接调用的,而是间接调用
检验一下
![](https://raw.githubusercontent.com/tcSteamedEggs/my-blog-pic/main/检验3.png)
- 此时涉及到的对象都是松耦合的,而且不在修改代码的前提下,我们可以给
paper
添加更多的订阅者,同时jack
可以在任何时候取消订阅!
![](https://raw.githubusercontent.com/tcSteamedEggs/my-blog-pic/main/检验4.png)
- 取消订阅
![](https://raw.githubusercontent.com/tcSteamedEggs/my-blog-pic/main/检验5.png)
- 现在我们把
jack
也变成一个发布者,在博客和微博等平台上,人人都可以是发布者!
makePublisher(jack);
jack.blog = function(msg) {
this.publish(msg);
}
- 现在
jack
可以在博客上发布文章了,让paper
订阅一下jack
的博客以便审核内容,提供一个方法readBlog()
。
paper.readBlog = function(blog) {
console.log("审核博客内容为:" + blog);
}
//订阅 默认事件 any
jack.subscribe(undefined, paper.readBlog);
//发布博客文章
jack.blog("发布订阅模式");
![](https://raw.githubusercontent.com/tcSteamedEggs/my-blog-pic/main/检验6.png)
太棒了!居然看完了!
小结
发布订阅模式是一个用于对象间通信的设计模式,通信方式更加灵活、可维护和可扩展,像在 Vue
、 React
等前端框架中,组件之间的通信,其中就有使用到发布订阅模式,具体可以看看 pubsub-js
,一个深受欢迎且较为成熟的库 ~
希望本文对你有所帮助 !