在前端开发中,随着应用规模的不断扩大,组件之间的通信问题日益突出。直接通过 props 或方法调用进行数据传递虽然简单,但在复杂场景下容易导致代码耦合高、扩展性差。**发布-订阅模式(Publish-Subscribe, Pub/Sub)**正是解决这一问题的有效手段,它可以将事件的发布者与订阅者解耦,使前端系统更灵活、可维护。
本文将带你从概念理解、手写实现、框架中的常见实现,到 React Demo 对比,全面掌握发布-订阅模式的核心思想和应用场景。
一、模式概念
发布-订阅模式的核心思想是:
将事件的产生者(发布者)和事件的处理者(订阅者)通过事件中心(Event Bus / 消息总线)解耦,中介管理事件和订阅关系。
核心角色
- 发布者(Publisher) :负责发布事件,但不关心谁会响应事件
- 订阅者(Subscriber) :负责订阅事件并处理,但不关心事件来源
- 事件中心(Event Bus) :负责维护事件订阅关系,并将事件从发布者传递给订阅者
特点:
- 低耦合:发布者与订阅者互不依赖
- 可扩展:多个订阅者可响应同一事件,多个发布者可触发同一事件
- 异步处理(可选):事件触发可异步执行,提高系统响应性
前端典型应用场景:
- 跨组件通信(尤其是非父子组件)
- 模块间解耦
- 插件或第三方库交互
二、框架中的实现示例
在实际开发中,很多框架提供了内置或常用的方式来实现发布-订阅机制。
2.1 Vue 2.x
Vue 2 中可以使用 Vue 实例作为事件总线:
javascript
// bus.js
import Vue from 'vue';
export const bus = new Vue();
// 组件A发布事件
bus.$emit('update', { msg: 'Hello Vue!' });
// 组件B订阅事件
bus.$on('update', data => {
console.log('接收到数据:', data);
});
特点:
- 简单直接,可用于跨组件通信
- 适用于中小型项目,复杂项目可使用 Vuex 等状态管理方案
2.2 Node.js
Node 内置了 EventEmitter
模块,可实现类似 Pub/Sub 功能:
javascript
const EventEmitter = require('events');
const emitter = new EventEmitter();
// 订阅事件
emitter.on('message', data => console.log('Received:', data));
// 发布事件
emitter.emit('message', 'Hello Node!');
特点:
- 适合后端或全栈场景
- 异步触发事件,方便模块解耦
2.3 React
React 没有内置事件总线,但可以通过:
- 自定义 EventBus(如手写实现)
- 状态管理库(Redux / MobX / Zustand)
- Context + useReducer上下文 + useReducer
来实现类似 Pub/Sub 功能
以上框架实现与后续 React Demo 的 EventBus 手写实现原理相同:都是通过事件中心解耦发布者和订阅者。
三、手写 EventBus 实现
理解概念后,我们可以手写一个 EventBus,用于管理事件订阅和发布:
javascript
// EventBus.js
class EventBus {
constructor() {
this.events = {}; // 存储事件与订阅者回调
}
// 订阅事件
on(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
}
// 发布事件
emit(event, data) {
const callbacks = this.events[event];
if (callbacks) {
callbacks.forEach(cb => cb(data));
}
}
// 移除订阅
off(event, callback) {
const callbacks = this.events[event];
if (!callbacks) return;
this.events[event] = callbacks.filter(cb => cb !== callback);
}
}
export default new EventBus();
思考点:
events
对象是事件名到回调数组的映射on
注册回调,emit
触发事件,off
移除订阅- 发布者无需知道订阅者数量,订阅者无需知道事件来源
javascript
import EventBus from './EventBus.js';
EventBus.on('greet', data => {
console.log('greet', data)
})
EventBus.emit('greet', { msg: "hello world" })
setTimeout(() => {
EventBus.emit('greet', { msg: "第二条消息" })
}, 5000)

四、React Demo 对比
为了更直观地理解发布-订阅模式的优势,我们创建一个小 Demo:用户在输入框输入内容,另一个组件显示输入结果。
4.1 不使用发布-订阅模式(直接调用)
javascript
// Display.js
import React from 'react';
export default function Display({ data }) {
return <div>显示内容: {data}</div>;
}
// Input.js
import React, { useState } from 'react';
export default function Input({ onChange }) {
const [value, setValue] = useState('');
return (
<div>
输入内容:<input
value={value}
onChange={e => {
setValue(e.target.value);
onChange(e.target.value); // 直接调用父组件方法
}}
/>
</div>
);
}
// App.js
import React, { useCallback, useState } from 'react';
import Input from './Input';
import Display from './Display';
export default function App() {
const [data, setData] = useState('');
const handleChange = useCallback((data: string) => {
setData(data);
}, [])
return (
<div>
<Input onChange={handleChange} />
<Display data={data} />
</div>
);
}

特点分析:
- Input 必须知道 App 的状态方法
setData
- 扩展性差:增加其他响应组件需要修改 App 或 Input
4.2 使用发布-订阅模式
javascript
// Input.js
import React, { useState } from 'react';
import bus from './EventBus';
export default function Input() {
const [value, setValue] = useState('');
return (
<div>
<input
value={value}
onChange={e => {
setValue(e.target.value);
bus.emit('data-update', e.target.value); // 发布事件
}}
/>
</div>
);
}
javascript
// Display.js
import React, { useState, useEffect } from 'react';
import bus from './EventBus';
export default function Display() {
const [data, setData] = useState('');
useEffect(() => {
bus.on('data-update', setData); // 订阅事件
return () => bus.off('data-update', setData); // 清理订阅
}, []);
return <div>显示内容: {data}</div>;
}
javascript
// App.js
import React from 'react';
import Input from './Input';
import Display from './Display';
export default function App() {
return (
<div>
<Input />
<Display />
</div>
);
}
优势分析:
- 低耦合:Input 不关心谁订阅事件
- 高扩展性:新增组件只需订阅事件,无需修改 Input
- 复用性:EventBus 可在项目任意模块复用,多模块可同时响应事件
五、对比总结
特性 | 直接调用 | 发布-订阅模式 |
---|---|---|
耦合度 | 高 | 低 |
可扩展性 | 差 | 好 |
复用性 | 低 | 高 |
适合场景 | 简单父子组件通信 | 跨组件/跨模块通信 |
六、实践思考
通过 Demo 对比,我们可以得出发布-订阅模式在前端开发中的价值:
-
解耦性高:发布者与订阅者无需互相了解
-
可扩展性强:新增功能或组件无需修改原有代码
-
复用性好:事件中心可在整个前端项目复用
-
注意事项:
- 避免事件过多导致事件链复杂
- 清理订阅,防止内存泄漏
💡 进一步扩展:
- 在大型 React 项目中,可结合 Context 或状态管理库(Redux/MobX)实现类似 Pub/Sub
- 可扩展异步事件、事件优先级、事件命名空间等高级功能
结语 :
发布-订阅模式是前端设计模式中非常实用的行为型模式,尤其适用于跨组件通信和模块解耦。通过本文,你已经掌握了从概念理解、框架示例、手写实现,到 React 实战的完整流程,并清晰看到它相较于直接调用的优势。