发布-订阅(Pub/Sub)模式
1)从问题/需求引入:为什么需要它?
你可能遇到过这些场景:
- 页面里多个组件都想“知道”某个事件发生了:用户登录成功、主题切换、语言切换、购物车数量变化……
- 你不希望 A 组件直接
importB 组件然后手动调用 B 的方法(耦合越来越重) - 你只想“发出一个消息”,至于谁来处理、处理多少个、以后会不会新增处理者,你不想管
这时,一个自然的需求就是:让消息发送者和接收者解耦。发布-订阅模式就是为这种“广播式通知”和“模块解耦”而生的。
2)关键概念:发布-订阅模式到底是什么?
发布-订阅(Publish-Subscribe)模式的核心角色:
- 发布者(Publisher):只负责发布消息(事件),不关心谁来处理
- 订阅者(Subscriber):订阅某类消息(事件),当消息发生时执行回调
- 事件中心 / 消息总线(Event Bus / Broker):负责维护事件与回调列表的映射,并在发布时通知订阅者
关键点(简洁版):
- 解耦:发布者不直接调用订阅者
- 一对多:同一事件可被多个订阅者响应
- 可动态增减:订阅者可随时订阅/取消订阅
常见易混点:
观察者模式通常是“被观察者”直接维护观察者列表(更紧密一些)。
发布-订阅多了一个“事件中心/中介”,发布者和订阅者彼此不知道对方(更解耦)。
3)用最小代码例子说明(手写一个 Pub/Sub)
下面实现一个简单事件中心,支持:
on(event, handler)订阅emit(event, data)发布off(event, handler)取消订阅
class EventBus {
constructor() {
this.events = new Map(); // eventName => Set<handler>
}
on(eventName, handler) {
if (!this.events.has(eventName)) {
this.events.set(eventName, new Set());
}
this.events.get(eventName).add(handler);
// 可选:返回取消订阅函数,便于清理
return () => this.off(eventName, handler);
}
off(eventName, handler) {
const set = this.events.get(eventName);
if (!set) return;
set.delete(handler);
if (set.size === 0) this.events.delete(eventName);
}
emit(eventName, payload) {
const set = this.events.get(eventName);
if (!set) return;
// 拷贝一份,避免回调中 off/on 导致遍历异常
[...set].forEach((handler) => {
try {
handler(payload);
} catch (err) {
// 真实项目里建议统一上报/日志
console.error(`[EventBus] handler error on "${eventName}"`, err);
}
});
}
}
// ===== 使用示例 =====
const bus = new EventBus();
// 订阅者 A:更新 UI
const unsubscribeA = bus.on("theme:change", (theme) => {
console.log("A 收到主题变更:", theme);
});
// 订阅者 B:持久化设置
bus.on("theme:change", (theme) => {
console.log("B 保存主题到 localStorage:", theme);
localStorage.setItem("theme", theme);
});
// 发布者:某个地方触发主题切换
bus.emit("theme:change", "dark");
// 取消订阅(避免内存泄漏)
unsubscribeA();
bus.emit("theme:change", "light");
你会发现:发布方只管 emit,订阅方只管 on,彼此不需要引用对方模块。
4)常见应用场景(前端特别常见)
4.1 跨组件/跨模块通信(尤其非父子关系)
例如:Header 的登录状态变化,需要通知 Sidebar、UserCard、CartIcon 更新。
4.2 全局事件:主题、语言、权限、网络状态
i18n:changeauth:login/auth:logoutnetwork:offline/network:online
4.3 异步流程编排与解耦
比如:上传完成后,不同模块要做不同事情(刷新列表、提示 toast、埋点上报)。
4.4 埋点/日志/监控
业务代码只发布 track:event,具体由埋点模块订阅处理,实现“零侵入”。
4.5 微前端/插件化架构
子应用或插件通过消息总线沟通,避免硬依赖彼此实现细节。
5)注意事项(踩坑提醒)
-
记得取消订阅(避免内存泄漏)
在单页应用里,组件卸载但订阅没清理,会导致回调一直存在、重复执行、内存增长。
建议on返回unsubscribe,在卸载时调用。 -
避免“全局事件泛滥”
事件名太随意、到处emit会变成“隐式依赖”,排查问题困难。
建议:- 事件命名空间化:
module:action(如auth:login) - 统一维护事件常量/文档
- 事件命名空间化:
-
谨慎处理同步/异步时序
emit默认同步执行订阅回调,若回调里有耗时逻辑会阻塞。
可考虑:- 回调内部自行异步(如
queueMicrotask/setTimeout) - 或在 bus 中提供
emitAsync
- 回调内部自行异步(如
-
错误隔离
一个订阅者抛错不应影响其他订阅者执行(上面示例用try/catch做了隔离)。 -
数据结构选择
用Set存回调可避免重复订阅同一个函数;遍历时建议拷贝,避免边遍历边修改。
小结
发布-订阅模式解决的核心痛点是:模块之间需要通信,但不想互相强耦合。
通过事件中心把“谁发消息”和“谁处理消息”隔开,你能更轻松地扩展功能、替换模块、复用逻辑。