JavaScript设计模式:发布订阅模式
模式概念
发布订阅模式定义了一种对象到对象的通信机制,其中一个对象(称为发布者)会向多个对象(称为订阅者)传递消息。在这种模式中,发布者和订阅者不必直接相互了解对方的存在,而是通过一个中介角色(称为消息中间件、消息代理或事件总线)来交换信息。订阅者从中间件中接收消息,而无需知道消息的发送者是谁。
模式结构
发布订阅模式通常包含以下几个主要组件:
- 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);
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
类用于创建订阅者对象。
该实现允许多个发布者和订阅者,且可以管理事件的订阅和取消订阅。
模式的效果
优点
- 解耦:发布者与订阅者之间没有直接的依赖关系,它们通过消息通道进行通信。
- 扩展性:可以轻松地添加新的订阅者,无需修改发布者代码。
- 灵活性:订阅者可以选择订阅感兴趣的消息。
缺点
- 消息传递顺序:不能保证消息传递的顺序。
- 性能问题:当订阅者或消息过多时,消息通道可能成为性能瓶颈。
与观察者模式对比
- 观察者模式:观察目标与观察者之间存在直接依赖,通常用于较小规模的事件处理。
- 发布订阅模式:通过中间件解耦发布者和订阅者,适用于对象间需要解耦或通信复杂的场景。
区别:
- 结构:
- 观察者模式:观察者与被观察者直接联系。
- 发布订阅模式:通过消息中间件实现发布者和订阅者的通信。
- 扩展性:
- 发布订阅模式扩展性更强,可以添加新的订阅者,发布者无需变动。
- 消息传递:
- 发布订阅模式可异步或同步,观察者模式多为同步。
模式的实际应用
1. MQTT
MQTT
协议广泛应用于物联网(IoT)场景,基于发布订阅模式实现设备间的消息通信。
- 发布者:设备发布消息到 MQTT 代理。
- 订阅者:设备订阅某个主题,接收匹配消息。
- 主题:消息分类标识符,发布者与订阅者基于主题进行通信。
2. 事件总线(EventBus)
事件总线也是基于发布订阅模式的实现,允许不同组件之间进行解耦通信。
- 订阅:组件向事件总线注册回调函数,表示对某事件的兴趣。
- 发布:组件触发事件,事件总线负责通知所有订阅者。
在 Vue
中,Vue 2 使用 new Vue()
实现事件总线,Vue 3 推荐使用 mitt.js
。
import mitt from 'mitt';
const eventBus = mitt();
// 发布事件
eventBus.emit('message', 'Hello, EventBus');
// 订阅事件
eventBus.on('message', (msg) => {
console.log(msg); // 输出: Hello, EventBus
});
3. Vue 响应式系统
Vue 的响应式系统是基于发布订阅模式实现的。数据变化时,触发相关依赖更新。
// 简化版的依赖收集和通知更新
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect);
}
}
notify() {
this.subscribers.forEach(sub => sub());
}
}
总结
发布订阅模式是一种非常灵活的设计模式,解耦了发布者与订阅者的关系,使得系统更易扩展。它在物联网通信、前端框架中的事件系统等场景中得到了广泛应用。无论是单独使用,还是结合其他设计模式,发布订阅模式都是复杂系统中不可或缺的组件。