人人都会AI编程

9.4 跨组件通信:全局状态库、Event Bus 方案对比

更新时间:2026-07-10

当组件树的层级较深,或者通信的双方在组件树上没有直接的父子关系(比如不同页面间的数据同步),前面介绍的状态提升、Context、兄弟组件通信等手段就显得力不从心。此时,就需要借助更全局的机制来实现跨组件通信。最主流的两种方案是全局状态库Event Bus(事件总线)

9.4.1 全局状态库:集中管理的响应式状态

全局状态库(如 Redux、Zustand、Jotai)的核心思想是将状态从组件树中“抽离”出来,放到一个独立的 Store 中。任何组件都可以读取 Store 中的数据,也可以通过派发动作(dispatch)或直接调用方法来更新数据。当 Store 中的数据变化时,所有依赖该数据的组件会自动重新渲染。

工作模式:

组件 A ──(读取 state)──┐
                      Store ←── 更新操作
组件 B ──(更新 state)──┘

典型示例(以 Zustand 为例):

// store.js - 全局 Store
import { create } from 'zustand';

export const useCartStore = create((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  removeItem: (id) => set((state) => ({ items: state.items.filter(i => i.id !== id) })),
}));
// 组件A:购物车图标(读取状态)
function CartIcon() {
  const count = useCartStore((state) => state.items.length);
  return <span>🛒 {count}</span>;
}

// 组件B:商品列表中的“加入购物车”按钮(写入状态)
function AddToCartButton({ product }) {
  const addItem = useCartStore((state) => state.addItem);
  return <button onClick={() => addItem(product)}>加入购物车</button>;
}

CartIcon 和 AddToCartButton 彼此完全不知道对方的存在,通过全局 Store 实现了间接通信。

优势:

  • 数据流清晰可追踪:状态集中管理,修改只能通过 Store 提供的接口,且通常支持 DevTools(如 Redux DevTools)记录每一次状态变更。
  • 自然融入 React 响应式体系:大多数现代状态库(Zustand、Redux Toolkit)都基于 React 的 useSyncExternalStore 或 Context,能精确触发组件更新,避免不必要的渲染。
  • 类型安全(配合 TypeScript):Store 的类型可以贯穿整个应用,减少运行时错误。
  • 适合复杂状态逻辑:当多个组件需要共享频繁更新、有复杂派生逻辑的数据时,状态库提供了专门工具(如中间件、Selector、Immer 集成)。

劣势:

  • 学习成本:部分库(如传统 Redux)模板代码较多,需要理解 Action、Reducer 等概念。
  • 过度使用:简单场景下引入全局状态库可能“杀鸡用牛刀”,反而增加不必要的复杂度。

9.4.2 Event Bus(事件总线):轻量级的发布/订阅

Event Bus 是一种发布/订阅模式(Pub/Sub)的实现。它在全局维护一个事件中心,组件可以发布(emit)自定义事件,其他组件可以订阅(on)这些事件并执行回调。Event Bus 不持有状态,只负责事件的传递。

在 React 中最常见的做法是使用第三方微型库(如 mitt)或自建一个简单的 EventBus 对象。

典型示例(基于 mitt):

// eventBus.js
import mitt from 'mitt';
export const bus = mitt();
// 组件A:发布事件
function LoginButton() {
  const handleLogin = () => {
    // ...登录逻辑
    bus.emit('user-logged-in', { userId: 123 });
  };
  return <button onClick={handleLogin}>登录</button>;
}

// 组件B:订阅事件
function UserProfile() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const handler = (data) => {
      setUser(data);
    };
    bus.on('user-logged-in', handler);
    return () => bus.off('user-logged-in', handler);
  }, []);

  return user ? <div>欢迎,用户 {user.userId}</div> : null;
}

优势:

  • 极度轻量:不需要额外的库或复杂配置,几行代码就能实现。
  • 灵活解耦:发布者和订阅者完全不知道对方的存在,适合临时、一次性的事件通知,如“全局提示条触发”、“播放器控制命令”等。
  • 对遗留代码友好:可以在不重构现有组件的前提下,快速打通任意两个组件之间的通信。

劣势:

  • 数据流模糊,难以调试:事件没有统一的管理中心,谁在什么时候发了什么事件,容易在复杂交互中失控,排查问题困难。
  • 破坏 React 单向数据流:本质上是一种“任意方向的通信”,容易导致组件行为不可预测,状态变化来源难以追踪。
  • 容易导致内存泄漏:需要手动在组件卸载时取消订阅,如果遗漏会造成回调持续触发甚至操作已卸载的组件状态。
  • 无法保证状态一致性:Event Bus 不持有状态,多个订阅者可能收到不同的事件顺序,难以实现状态的最终一致性。

9.4.3 方案对比与选型建议

| 维度 | 全局状态库(Zustand / Redux 等) | Event Bus |
| ---------- | --------------------------------- | ------------------------------- |
| 数据流 | 清晰、单向,可跟踪 | 模糊、多源,难以追踪 |
| 调试体验 | 完善(DevTools、Action Log) | 差,需要自行记录 |
| 与 React 融合 | 天然集成,自动响应式更新 | 需要手动订阅/取消,易出 bug |
| 适用场景 | 共享业务状态、复杂状态管理 | 一次性事件、命令式通知、解耦临时需求 |
| 学习成本 | 中(Zustand 低,Redux 中高) | 极低 |
| 性能 | 基于 Selector 精确更新 | 手动控制,易误触发多余渲染 |
| 类型安全 | 通常良好 | 需额外封装,大多较差 |

选型建议:

  • 优先选择全局状态库。对于绝大多数跨组件共享数据的需求,都应该使用 Zustand、Redux Toolkit 等全局状态管理方案。它们提供可预测的数据流和良好的调试能力,是 React 官方推荐的思路(Context + useReducer 也是一种轻量级的“全局状态”)。
  • 仅在特定场景下使用 Event Bus。当你的确只需要通知一个动作发生,而不需要关心“数据是什么”,并且发布者和订阅者完全不需要知道对方的存在,例如:
  • 全局 Toast 提示:bus.emit('show-toast', '操作成功')
  • 跨微前端子应用的简单信令
  • 与 React 外部 (如 WebSocket 回调、第三方地图库) 通信时作为中间层

在这些场景下,Event Bus 可以作为 React 数据流的补充,而不是替代。

  • 避免用 Event Bus 管理复杂状态。如果事件开始携带越来越多的数据,或者你需要根据事件去修改一系列相关状态,请立刻重构为全局状态库,否则项目会逐渐变成“事件地狱”。

最终,跨组件通信方案的选择没有绝对的对错,但维护一个可预测的状态流是 React 带给我们的核心价值,任何破坏这种可预测性的工具都应谨慎引入。