发布订阅模式(Publish-Subscribe Pattern)是一种对象间解耦的通信模式 ,核心思想是:发送者(发布者)不直接与接收者(订阅者)通信,而是通过一个 "中间媒介"(事件总线 / 主题中心)传递消息,双方无需知道对方的存在,从而降低耦合度。
javascript
发布者
│
│ 发布 "userLogin"事件(带数据)
▼
事件总线(Event Bus)
├──────────────────────┐
▼ ▼
订阅者A 订阅者B
"userLogin" "userLogin"
│ │
└─→ 触发回调(收到数据) └─→ 触发回调(收到数据)
一、核心角色
-
发布者(Publisher):负责产生消息并 "发布" 到中间媒介(无需知道谁会接收)。例:前端的按钮点击事件、后端的订单状态变更。
-
订阅者(Subscriber):向中间媒介 "订阅" 感兴趣的消息类型,当有对应消息发布时,会收到通知并处理。例:监听按钮点击的回调函数、接收订单状态变更的短信服务。
-
事件总线 / 主题中心(Event Bus/Topic):作为中间枢纽,管理 "消息类型" 与 "订阅者" 的映射关系,接收发布者的消息后,转发给所有订阅该类型的订阅者。
二、与观察者模式的区别
很多人会混淆两者,核心差异在于是否有中间媒介:
- 观察者模式:观察者(Subscriber)直接依赖被观察者(Publisher),被观察者维护观察者列表,状态变化时直接通知。(如:Vue 的响应式系统中,组件直接观察数据变化)
- 发布订阅模式 :通过独立的 "事件总线" 解耦,发布者和订阅者完全隔离。(如:前端的
EventBus、后端的消息队列 Kafka)
三、前端常见应用场景
-
组件通信 :非父子组件间通信(如 Vue 的
EventBus、React 的PubSub库)。javascript// 简单的EventBus实现 class EventBus { constructor() { this.events = {}; // 存储 { 事件类型: [回调函数列表] } } // 订阅 on(type, callback) { if (!this.events[type]) this.events[type] = []; this.events[type].push(callback); } // 发布 emit(type, data) { if (this.events[type]) { this.events[type].forEach(callback => callback(data)); } } // 取消订阅 off(type, callback) { if (this.events[type]) { this.events[type] = this.events[type].filter(cb => cb !== callback); } } } // 使用 const bus = new EventBus(); // 订阅 bus.on('userLogin', (user) => console.log('欢迎', user.name)); // 发布 bus.emit('userLogin', { name: '张三' }); // 输出:欢迎 张三
事件监听 :DOM 事件本质是发布订阅模式(addEventListener是订阅,用户操作是发布)。
javascript
// 订阅点击事件
document.getElementById('btn').addEventListener('click', handleClick);
// 用户点击按钮时,浏览器作为事件总线发布事件,触发handleClick
-
状态管理 :Redux/Vuex 中,组件订阅状态变化(
subscribe),action 发布状态更新。 -
跨页面通信 :
window.postMessage配合发布订阅,实现 iframe 或多标签页通信。
四、优势与注意事项
优势:
- 解耦:发布者和订阅者互不依赖,便于扩展(新增订阅者无需修改发布者)。
- 灵活性:支持一对多通信(一个事件可被多个订阅者处理)。
- 可维护性:集中管理事件,避免代码中散落的直接调用。
注意事项:
- 内存泄漏 :订阅后需及时取消(如组件销毁时调用
off),否则残留的回调会导致内存泄漏。 - 事件命名规范 :避免事件类型冲突(可使用命名空间,如
user:login、order:pay)。 - 调试难度:消息流通过中间层,可能增加追踪事件来源的复杂度。