全局状态Zustand与事件总线Event Bus
简单来说,Zustand 是一个轻量级、高性能且易于上手的 React 状态管理库。
🤔 为什么我们需要 Zustand?
在 Zustand 出现之前,我们通常面临两种选择,但它们都有各自的痛点:
- Redux: 功能强大,但过于复杂。你需要编写大量的“样板代码”,比如
action、reducer、dispatch等,开发效率较低。 - React Context + useState: 虽然简单,但在处理复杂或频繁更新的状态时,容易导致不必要的组件重渲染,影响性能。而且,当组件层级很深时,数据传递(Props Drilling)会变得非常麻烦。
Zustand 的出现,就是为了完美解决这些问题,它在简洁和功能之间取得了极佳的平衡。
✨ Zustand 的核心优势
Zustand 之所以能成为当前最受欢迎的状态管理方案之一,主要归功于以下几个特点:
-
极致轻量
它的核心包体积非常小(压缩后仅 1-2KB),并且没有任何外部依赖,几乎不会给你的项目增加负担。 -
极简的 API
它的学习成本极低。你只需要掌握一个核心的create函数,就可以创建出一个全局的状态仓库(Store)。 -
无需 Provider
这是它最方便的特点之一。你不需要像使用 Context 那样,在应用顶层用<Provider>组件包裹一切。你可以在任何地方直接导入并使用 Store,摆脱了组件树的嵌套限制。 -
出色的性能
Zustand 支持细粒度的状态订阅。这意味着组件可以只订阅它关心的那部分状态。当状态更新时,只有真正使用了该状态的组件才会重新渲染,避免了性能浪费。 -
灵活且强大
它原生支持异步操作,并且通过中间件(Middleware)可以轻松实现状态持久化(如保存到localStorage)、连接开发工具(DevTools)等高级功能。 -
完美的 TypeScript 支持
它对 TypeScript 非常友好,能够提供出色的类型推导,让你在享受开发便利的同时,也能获得类型安全带来的保障。
💻 核心用法:三步上手
让我们通过一个经典的“计数器”例子,看看 Zustand 用起来有多简单。
第一步:创建 Store
我们通常在 src/store 目录下创建一个状态文件,比如 counterStore.ts。
// src/store/counterStore.ts
import { create } from 'zustand'
// 1. 定义状态的类型
interface CounterState {
count: number
increment: () => void
decrement: () => void
}
// 2. 使用 create 函数创建 store
const useCounterStore = create<CounterState>((set) => ({
// 初始状态
count: 0,
// 更新状态的方法
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
export default useCounterStore
第二步:在组件中使用
// Counter.tsx
import useCounterStore from './store/counterStore'
function Counter() {
// 从 store 中获取状态和方法
const count = useCounterStore((state) => state.count)
const increment = useCounterStore((state) => state.increment)
const decrement = useCounterStore((state) => state.decrement)
return (
<div>
<h1>当前计数: {count}</h1>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
</div>
)
}
在任何组件中,你都可以直接导入并使用这个 store,它本身就是一个 Hook。
一种非常典型的“跨组件通信”模式,通常被称为“发布-订阅”或“事件总线”模式
// 事件派发
<Button
className="nodrag"
type="text"
icon={<EditOutlined style={{ fontSize: 24 }} />}
onClick={() =>
window.dispatchEvent(
new CustomEvent("OPEN_CONFIG_MODAL", {
detail: { id, data },
}),
)
}
/>
// 事件监听
useEffect(() => {
const handleOpenModal = (e: any) => {
setActiveNode(e.detail);
setModalVisible(true);
};
// 对于不需要保留状态、不需要 UI 响应;事件总线是最简洁的。
window.addEventListener("OPEN_CONFIG_MODAL", handleOpenModal);
return () => {
window.removeEventListener("OPEN_CONFIG_MODAL", handleOpenModal);
};
}, []);
下面我为你详细拆解 window.dispatchEvent 和 new CustomEvent 的含义及用法:
1. new CustomEvent(...):制造一个“自定义信号”
浏览器原生自带很多事件,比如 click(点击)、scroll(滚动)、load(加载)。但在复杂的业务中,这些不够用,我们需要定义自己的业务事件,比如这里的 'OPEN_CONFIG_MODAL'。
CustomEvent 就是用来创建这种自定义事件对象的构造函数。
- 第一个参数
'OPEN_CONFIG_MODAL':
这是事件的名字(类型)。就像给快递贴个标签,告诉接收方这是什么类型的包裹。 - 第二个参数
{ detail: { id, data } }:
这是事件的配置项。detail是CustomEvent的核心属性,专门用来携带数据。- 在这里,我们将
{ id, data }放入detail中。这意味着,当其他组件接收到这个事件时,可以通过event.detail拿到这两个变量。
2. window.dispatchEvent(...):在全局“广播”信号
dispatchEvent 是一个标准的方法,用于触发某个元素上的事件。
- 为什么要加
window.?window代表浏览器的全局窗口对象。当你调用window.dispatchEvent时,意味着你是在全局范围内触发这个事件。 - 效果:
这就像拿着大喇叭在广场上喊话。无论你的代码在哪个组件里(哪怕是嵌套很深的子组件),只要在这个页面里,任何监听了'OPEN_CONFIG_MODAL'的地方都能收到通知。
3、dispatchEvent 依然存在“更新机制”上的性能劣势:
A. 无法利用 React 的“批处理” (Batching)
- Jotai/State 模式:当你修改状态时(例如
setCount(c => c + 1)),React 知道这是一个状态更新。如果你在同一个事件循环中修改了 10 次状态,React 会自动把它们合并(批处理)成一次渲染。 - Event Bus 模式:
dispatchEvent触发的是回调函数。如果你在回调里执行setState,这往往发生在 React 的合成事件系统之外。- 如果你连续触发 10 次
dispatchEvent,可能会导致 10 次独立的setState调用,进而触发 10 次组件重渲染。这会造成严重的性能浪费(即使你只监听了这一个事件)。
- 如果你连续触发 10 次
B. 丢失“并发模式”的优先级调度
- Jotai/State 模式:React 18 引入了并发模式。React 知道哪些更新是紧急的(如点击),哪些是不紧急的(如后台数据刷新)。React 可以暂停、中断或重排这些更新。
- Event Bus 模式:
dispatchEvent是浏览器原生的同步机制(稍后会详细解释)。React 无法感知这个事件的优先级。一旦事件触发,回调必须立即执行,这可能会阻塞主线程,导致高优先级的 UI 更新(如输入框打字)被卡住。
在某些场景下“弃用” Jotai/Zustand 而选择“原始”的 dispatchEvent,通常是因为以下 3 个不得不妥协的现实原因:
1. “依赖地狱”与“包体积”的博弈
场景: 我正在写一个基础组件库(比如 Button、Icon),或者一个微前端的子应用。
- 如果用 Jotai/Zustand:
这就意味着我的基础组件库强依赖了 Jotai/Zustand。- 如果你(使用者)的项目里没用这两个库,你就得为了用我一个按钮,额外安装它们的依赖。这叫“依赖污染”。
- 如果主应用用了 Jotai,子应用用了 Zustand,你的项目里就会同时存在两套状态管理库,包体积直接翻倍。
- 如果用
dispatchEvent:
这是浏览器原生 API,零依赖,零体积。无论你把组件放到 React、Vue 甚至原生 jQuery 项目里,它都能跑。- 结论: 为了极致的通用性和解耦,我宁愿牺牲一点类型安全和性能。
2. 不需要保留状态、不需要 UI 响应的“即抛型”操作(Fire-and-Forget)。
3. 跨越“框架边界”的通信
| 你的需求 | 推荐方案 | 理由 |
|---|---|---|
| 核心业务数据 (用户信息、购物车、表单值) |
Jotai / Zustand | 需要类型安全、响应式更新、高性能渲染。这是它们的主场。 |
| 全局 UI 状态 (侧边栏展开、暗黑模式) |
Zustand | 集中管理,方便调试,避免 Props Drilling。 |
| 基础组件/工具库 (Button, Toast, Notification) |
dispatchEvent | 避免强依赖,保持库的纯净和轻量。 |
| 非数据交互 (播放声音、埋点、第三方SDK通知) |
dispatchEvent | 既然是“发完不管”的事,就别占用状态库的资源了。 |
| 跨框架/微前端 (React 调 Vue) |
dispatchEvent | 唯一能打通“任督二脉”的方案。 |
最佳实践是: 用 Jotai 管理数据,用 dispatchEvent 处理突发的、非数据驱动的交互动作。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)