模式概念
发布订阅模式定义了一种对象到对象的通信机制,其中一个对象(称为发布者)会向多个对象(称为订阅者)传递消息。在这种模式中,发布者和订阅者不必直接相互了解对方的存在,而是通过一个中介角色(称为消息中间件、消息代理或事件总线)来交换信息。订阅者从中间件中接收消息,而无需知道消息的发送者是谁。
模式结构
发布订阅模式通常包含以下几个主要组件:
发布者Publisher:发布者不关心谁会接收这些消息,只需将消息发送到消息中间件,并指定消息的主题,中间件负责后续的分发工作。订阅者Subscriber:订阅特定的主题,并不直接与发布者交互。当中间件接收到与订阅者所订阅主题匹配的消息时,会将其转发给相应的订阅者。中间件Channel(中介,代理):中央消息中介,接收来自发布者的消息,存储和转发这些消息给所有已订阅相应主题的订阅者主题:消息分类的标识符
代码实现
//发布者类
class Publisher {
constructor() {
this.subscribers = new Map(); // 存储主题及其对应的订阅者
}
//添加 指定类型的订阅者
subscribe(theme, subs) {
// 如果主题尚未注册,初始化一个空数组
if (!this.subscribers.has(theme)) {
this.subscribers.set(theme, []);
}
// 将订阅者添加到订阅者数组
this.subscribers.get(theme).push(subs);
}
//通知订阅者
publish(theme, data) {
// 获取该主题的所有订阅者
const subscribers = this.subscribers.get(theme);
if (subscribers) {
// 遍历并执行所有订阅者的回调函数
subscribers.forEach(subs => {
subs.callback(data);
});
}
}
//移除订阅者中指定业务
unsubscribe(theme, subsName) {
// 获取该主题的订阅者数组
const subscribers = this.subscribers.get(theme);
if (subscribers) {
// 找到对应的订阅者并从数组中移除
const index = subscribers.findIndex(subs => subs.name === subsName);
if (index > -1) {
subscribers.splice(index, 1);
}
}
}
}
//订阅者类
class Subscriber {
constructor(name, cb) {
this.name = name; //订阅者id
this.callback = cb; //订阅者函数
}
}
// 创建一个发布者
const pub = new Publisher();
// 定义一个订阅者
const subscriber1 = new Subscriber("sub1", data => {
console.log("sub1 received data:", data);
});
// 定义另一个订阅者
const subscriber2 = new Subscriber("sub2", data => {
console.log("sub2 received data:", data);
});
// 订阅主题 'message'
pub.subscribe("message", subscriber1);
// 订阅同一主题 'message'
pub.subscribe("message", subscriber2);
// 发布主题 'message',所有订阅者都会收到消息
pub.publish("message", "Hello, World!");
// sub1 received data: Hello, World!
// sub2 received data: Hello, World!
// 取消订阅者sub1
pub.unsubscribe("message", "sub1");
// 再次发布主题 'message',只有 sub2 会收到消息
pub.publish("message", "This is another message.");
// sub2 received data: This is another message.
代码解释:
Publisher类初始化时创建了一个Map来存储主题和订阅者的映射关系。subscribe方法允许订阅者注册他们对特定主题的监听函数。publish方法用于触发主题,执行所有注册到该主题的回调函数。unsubscribe方法允许订阅者取消对特定主题的监听。Subscriber类用于创建订阅者。
这个简单的发布订阅模式实现允许多个发布者和多个订阅者,并且可以通过 Publisher 类的方法来管理事件的订阅和发布。这种模式在实际应用中可以用于实现组件间的解耦通信。
模式的效果
发布订阅模式的优点:
- 解耦:发布者和订阅者之间没有直接的依赖关系,它们通过消息通道进行通信。
- 扩展性:可以轻松地添加更多的订阅者,而不需要修改发布者的代码。
- 灵活性:订阅者可以选择性地订阅感兴趣的消息类型。
发布订阅模式的缺点:
- 消息传递的顺序:不能保证消息传递的顺序。
- 性能问题:在有大量订阅者或消息的情况下,消息通道可能会成为性能瓶颈。
与观察者模式对比
结构
-
观察者模式:
- 观察目标(Subject):持有观察者列表,并提供注册和注销观察者的接口。
- 观察者(Observer):定义一个更新接口,用于接收主题状态变化的通知。
-
发布订阅模式:
- 发布者(Publisher):产生事件并发送消息的对象。
- 订阅者(Subscriber)/观察者(Observer):接收事件并响应的对象。
- 消息通道(Channel):存储和转发消息的中介。
- 主题:标识符。
灵活性和扩展性
-
观察者模式:
- 扩展性较低,因为当增加新的观察者时,可能需要修改观察目标的代码。
- 灵活性较低,观察者和观察目标是需要相互指定的,之间有直接的依赖关系。
-
发布订阅模式:
- 扩展性较高,可以轻松添加或删除订阅者,而不需要修改发布者的代码。
- 灵活性较高,发布者和订阅者之间没有直接的依赖关系。发布者和订阅者之间不需要相互知道,只需要知道类型,通过
第三方实现调度,属于经过解耦合的观察者模式
消息传递
-
观察者模式:
- 消息传递通常是同步的,主题状态改变时,所有观察者会立即接收到通知。
-
发布订阅模式:
- 消息传递可以是同步的或异步的,根据消息通道的实现而定。
应用场景
-
观察者模式:
- 适用于对象之间存在直接的依赖关系,且对象数量较少的场景。
-
发布订阅模式:
- 适用于对象之间需要解耦,或者对象数量较多、通信模式复杂的场景。
模式的实际应用
MQTT
MQTT协议使用的是发布/订阅设计模式。这种设计模式是MQTT协议的核心机制,它允许消息的发送者(发布者)与消息的接收者(订阅者)之间解耦,无需直接相互了解对方的存在或详细信息。以下是发布/订阅模式在MQTT协议中的具体表现:
- 发布者(Publisher) :设备或应用程序向MQTT代理(Broker)发布消息。发布者不关心谁会接收这些消息,只需指定消息的主题(Topic),即消息所属的类别或标签。
- 订阅者(Subscriber) :设备或应用程序向MQTT代理订阅特定的主题。订阅者表明其对某一类消息感兴趣,但并不直接与发布者交互。当代理接收到与订阅者所订阅主题匹配的消息时,会将其转发给相应的订阅者。
- 主题(Topic) :作为消息分类的标识符,类似于一种消息路由机制。发布者发布消息时附带一个主题,订阅者则根据主题来筛选感兴趣的消息。主题可以包含层级结构,使用斜杠(/)分隔不同的主题层级,以支持更精细的订阅过滤。
- 代理(Broker) :作为中央消息中介,负责接收来自发布者的消息,存储(可选,取决于服务质量级别)和转发这些消息给所有已订阅相应主题的订阅者。代理确保了发布者与订阅者之间的逻辑分离,维护着主题订阅关系,并根据主题匹配规则进行消息分发。
发布/订阅模式的优势在于其松耦合性,使得系统的扩展性和灵活性得以增强。发布者和订阅者可以独立地加入或离开系统,改变订阅主题,或者开始/停止发布消息,而不影响其他参与者。
这种模式特别适用于物联网(IoT)场景,因为设备数量众多、分布广泛且可能具有间歇性的网络连接,同时需要处理大量的异步事件和数据流。
MQTT协议正是基于发布/订阅设计模式来实现高效、轻量级、跨平台的设备间消息通信。
事件总线EventBus
事件总线基于发布订阅模式实现,它允许不同的组件或对象之间进行通信,而不需要直接引用对方。事件总线充当中介,使得组件可以发送(发布)和接收(订阅)事件或消息,而不必知道其他组件的存在。
时间总线的工作原理:
- 事件总线维护一个
中心事件存储,通常是使用一个对象或Map来保存事件名称和订阅者回调函数的映射关系。 - 订阅:组件向事件总线注册自己的回调函数,表示对特定事件的兴趣;事件总线将回调函数添加到对应事件的订阅者列表中。
- 取消订阅:组件可以在不再需要接收事件时,从事件总线注销自己的回调函数;事件总线从对应事件的订阅者列表中移除该回调函数。
- 发布:当事件发生时,触发事件的组件(可以是任何组件)向事件总线发布事件;事件总线根据事件名称,查找并执行所有注册到该事件的回调函数。
- 广播:事件总线将事件广播给所有订阅了该事件的回调函数;每个回调函数会接收到发布事件时传递的数据,并可以独立处理。
组件之间不需要直接引用对方,降低代码之间的耦合度;新的组件也可以轻松的订阅和发布事件,不会影响现有代码;还可以异步的进行事件处理,不会阻塞主线程;并且可以增加错误处理,防止一个错误的回调影响其他订阅者。
框架推荐:
Vue 2中使用的是 new Vue() 实例实现事件总线,而 Vue 3 推荐使用 mitt.js。
mitt这是一个轻量级的事件库,它不依赖于任何特定的前端框架,因此可以在Vue、React、Angular等框架中使用。
Vue响应式系统
Vue三大核心系统中的响应式系统就是基于发布订阅模式来实现的,响应式模块是Vue数据驱动特性的基石,它实现了数据变化与视图更新之间的自动同步。
依赖收集:当访问到响应式数据时,自动追踪当前活跃的反应式依赖(如组件实例、计算属性等),并将它们添加到依赖收集链中。依赖通知:当响应式数据发生变化时,触发依赖通知流程,遍历并更新所有依赖于该数据的反应式依赖,引发它们自身的更新逻辑(如重新计算、重新渲染等)。
部分源码:defineReactive
/**
* Define a reactive property on an Object.
*/
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean,
observeEvenIfShallow = false
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if (
(!getter || setter) &&
(val === NO_INITIAL_VALUE || arguments.length === 2)
) {
val = obj[key]
}
let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
if (__DEV__) {
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key
})
} else {
dep.depend() //依赖收集
}
if (childOb) {
childOb.dep.depend()
if (isArray(value)) {
dependArray(value)
}
}
}
return isRef(value) && !shallow ? value.value : value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (!hasChanged(value, newVal)) {
return
}
if (__DEV__ && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else if (getter) {
// #7981: for accessor properties without setter
return
} else if (!shallow && isRef(value) && !isRef(newVal)) {
value.value = newVal
return
} else {
val = newVal
}
childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock)
if (__DEV__) {
dep.notify({
type: TriggerOpTypes.SET,
target: obj,
key,
newValue: newVal,
oldValue: value
})
} else {
dep.notify() //通知订阅者,进行update操作
}
}
})
return dep
}
具体参见源码,位置在core 文件夹/observer 文件夹下
❤今天的分享就到这里,希望可以帮助到你!假如你对文章感兴趣,可以来我的公众号:小新学研社。