React Native 状态管理大比拼:Event Bus 还是 Context?小白一看就懂!
在 React Native 开发中,当我们想让两个相隔十万八千里的组件"隔空对话"时,经常会面临一个灵魂拷问:到底是该用 Event Bus(事件总线),还是用 React Context(上下文)?
很多新手觉得这俩东西差不多,都能实现"全局通信",随便用哪个都行。但其实它们就像是**"对讲机"和"公告板"**的区别,用错了场景,后期的代码维护会让你痛不欲生。
今天,我们就用最通俗易懂的语言,来把这俩兄弟扒个底朝天!
1. 核心比喻:对讲机 VS 公告板
要理解它们的区别,只要记住这个比喻:
Event Bus (事件总线)= 对讲机 📻
- 怎么工作:你按下按钮喊一句"洞幺洞幺,收到请回答",这句话瞬间发出去。只有当时开着对讲机、调到同一频道的人才能听到。
- 特点 :传的是"动作"。喊完就没了,没有记忆。如果你喊的时候,对方正好把对讲机关了,那他永远都不知道你刚才说了什么。
React Context = 公告板 📋
- 怎么工作:你在村口的公告板上贴了一张纸:"今天中午吃红烧肉"。
- 特点 :传的是"状态/数据"。它是有记忆的。不管村民是上午来看,还是下午来看,只要公告没换,大家看到的都是"红烧肉"。
2. 深度对比:它们各有什么优缺点?
| 对比维度 | Event Bus (对讲机) | React Context (公告板) |
|---|---|---|
| 性能 | 极高 🚀 <br>发通知只让监听了的人干活,其他人该干嘛干嘛。 |
较差 🐢(容易踩坑) <br>公告板一换内容,所有盯着公告板的人(组件)都会重新刷新(渲染)。 |
| 使用范围 | 无限制 🌍 <br>可以在 React 组件里用,也能在普通的 .js 工具文件里用(比如网络请求报错时)。 |
局限于 React 组件树 🌳 <br>必须包在 <Provider> 标签里,只能在组件内部读取。 |
| 记性(状态持久性) | 鱼的记忆 🐟 <br>事件发出去那一瞬间,错过了就错过了。 |
大象的记忆 🐘 <br>数据一直都在,新打开的页面也能随时拿到最新数据。 |
| 清理麻烦度 | 必须手动清理 🧹 <br>页面销毁时如果不关掉监听,后台会一直运行,导致内存泄漏甚至重复报错。 |
全自动 🤖 <br>React 自己会管,页面关了就自动解绑,不用你操心。 |
3. 实战场景:到底该怎么选?
看了上面的对比,你可能还是有点懵。我们直接上具体的业务场景!
场景一:用户登录状态、App主题色
选择:React Context ✅
为什么? 因为这些是**"全局需要共享的静态/低频变化数据"**。不管用户跳到哪个页面,页面一打开就需要知道"我现在是谁?"、"我是黑夜模式还是白天模式?"。公告板模式(Context)最适合这种需要持久记忆的场景。
场景二:全局硬件扫码、Token过期被强制踢下线
选择:Event Bus ✅
为什么? 因为扫码是一个**"瞬间的动作"**。 比如:用户拿扫码枪扫了一个二维码。我们只需要在全局监听这个动作,然后通知当前页面去更新 UI。如果我们把"最新扫码结果"存到 Context 里,那每次扫码都会导致一堆无关的组件跟着重新渲染,非常浪费性能。而且,与硬件通信,底层天然就是基于事件的。
场景三:点击商品列表的"加入购物车",通知底部 TabBar 更新角标数量
选择:React Context (或者 Redux/Zustand) ✅
为什么? 很多新手这里会用 Event Bus,发一个"购物车数量+1"的事件。但这有个致命缺陷:如果你还没打开过 TabBar,或者从别的页面跳回来,角标数量就对不上了。因为购物车数据是"状态",必须持久保存,所以应该存在 Context 或者专门的状态管理库里。
4. 代码演示(极简版)
Event Bus 实战:全局订单扫码与串口刷卡(以 React Native 为例)
在智能收银机、工业平板等场景中,我们经常需要处理硬件设备的输入。用 RN 自带的 DeviceEventEmitter 来做事件总线是最优解。
案例 1:全局订单扫码状态同步
假设用户用扫码枪扫了一个订单条码,我们需要把订单状态从"制作中"变更为"请取餐"。
javascript
import { DeviceEventEmitter } from 'react-native';
// 1. 定义事件名(好习惯:集中管理)
export const GLOBAL_SCANNER_EVENTS = {
ORDER_STATUS_CHANGED: 'GLOBAL_ORDER_STATUS_CHANGED',
};
// 2. 发送方:全局的扫码监听服务(后台默默运行)
// 扫码枪扫码后触发
const handleOrderScanned = (barcode) => {
// 模拟接口调用成功,状态变更为 collecting (请取餐)
DeviceEventEmitter.emit(GLOBAL_SCANNER_EVENTS.ORDER_STATUS_CHANGED, {
orderId: barcode,
newStatus: 'collecting'
});
};
// 3. 接收方:订单列表页(OrderRecordList)
useEffect(() => {
// 戴上对讲机,监听订单状态变更
const subscription = DeviceEventEmitter.addListener(
GLOBAL_SCANNER_EVENTS.ORDER_STATUS_CHANGED,
({ orderId, newStatus }) => {
console.log(`订单 ${orderId} 状态变更为 ${newStatus}`);
if (newStatus === 'collecting') {
// 更新 UI:把订单从"制作中"移到"请取餐"列表
moveToCollectingList(orderId);
}
});
// ⚠️ 极其重要:页面销毁时一定要摘下对讲机,防止内存泄漏!
return () => {
subscription.remove();
};
}, []);
案例 2:串口读卡器刷卡监听
如果你的设备连着一个 LF125k 的低频刷卡器,全局监听刷卡动作也是同样的套路:
javascript
// 全局发送刷卡事件
const onCardRead = (cardNumber) => {
DeviceEventEmitter.emit('GLOBAL_CARD_SCANNED', cardNumber);
};
// 在需要响应刷卡的页面监听
useEffect(() => {
const sub = DeviceEventEmitter.addListener('GLOBAL_CARD_SCANNED', (cardNo) => {
// 自动填入输入框或者直接发起查询请求
queryEmployeeInfo(cardNo);
});
return () => sub.remove();
}, []);
Context 实战:用户登录状态与权限管理
javascript
import React, { createContext, useContext, useState } from 'react';
// 1. 建个公告板
export const AuthContext = createContext();
// 2. 贴公告(放在最顶层组件,比如 App.js)
export const App = () => {
const [isSignedIn, setIsSignedIn] = useState(false);
const [userInfo, setUserInfo] = useState(null);
return (
<AuthContext.Provider value={{ isSignedIn, setIsSignedIn, userInfo }}>
<NavigationContainer>
{/* 如果登录了,才挂载全局扫码监听服务 */}
{isSignedIn && <GlobalOrderScannerListener />}
<YourPages />
</NavigationContainer>
</AuthContext.Provider>
);
};
// 3. 看公告(在任何深层子组件里,比如个人中心页)
const ProfilePage = () => {
// 只要 AuthContext 里的值变了,这个组件就会自动获取最新值并刷新
const { isSignedIn, userInfo, setIsSignedIn } = useContext(AuthContext);
if (!isSignedIn) return <Text>请先登录</Text>;
return (
<View>
<Text>欢迎您,{userInfo?.name}</Text>
<Button title="退出登录" onPress={() => setIsSignedIn(false)} />
</View>
);
};
5. 总结
不要为了用某个技术而用它,最好的架构是"适合"。
- 要传动作/指令,怕影响性能 ➡️ 选 Event Bus
- 要存全局数据,需要持久记忆 ➡️ 选 Context
掌握了这个判断标准,你的 React Native 进阶之路就又稳健了一步!