Zustand 源码解读与最佳实践
date
Jul 7, 2025
slug
zustand-source-code-deep-dive
status
Published
tags
React
Zustand
React Fiber
type
Post
author
summary
本文系统对比 Redux、MobX 与 Zustand 的取舍,并解释 Zustand 为何能以极简 API 与精准订阅实现高性能状态管理。文章进一步结合 React Fiber 并发渲染背景,说明 useSyncExternalStore 如何解决外部状态“撕裂(tearing)”问题,随后深入拆解 Zustand 的 vanilla store、React 绑定与中间件实现。最后给出一套可落地的最佳实践与踩坑清单,用于在中大型项目中兼顾性能与可维护性。
为什么又要折腾状态管理工具?
问题的起源
业余时间在研究量化交易时,我开发了一个透视数据的 Dashboard 项目。起初本着「大道至简」的原则,我选择用
useState 和 props 来管理应用状态,希望做到简单轻量。这种原生方案对于简单场景确实足够,但现实往往比理想复杂。我的项目中需要使用大量 WebSocket 进行实时数据通信和计算,面临着以下挑战:
- 高吞吐量:WebSocket 消息频繁推送,少则每秒几十条,多则上百条
- 大数据量:需要处理和缓存大量实时数据
- 复杂计算:多个组件需要对同一份数据进行不同维度的计算和展示
- 状态复用:相同的状态需要在多个组件间共享
随着功能迭代,组件树越来越深,状态提升和 props 透传变得异常繁琐,更要命的是出现了严重的性能问题 —— 频繁的状态更新导致大量不必要的组件重渲染。
此时,引入专业的状态管理工具已是必然选择。
选型思考:好的状态管理工具应具备什么?
AI:好的状态管理工具本质上应该是一个自然流畅的「数据调度中心」,它能够让 UI 和数据便捷地实现更新同步,实现业务逻辑与视图层的解耦,让数据流动更加安全、简单、高效,并能适应项目从小到大、从简单到复杂的各种演进场景。
维度 | Redux | Redux Toolkit | MobX | Zustand |
学习曲线 | 较高
需掌握 reducer/action 等核心概念) | 中等
提供 API 简化了 Redux 原始写法 | 较低
响应式思想较易理解 | 非常低
API 简单,核心概念极少 |
模板代码 | 较多 | 较低 | 较低 | 非常低 |
性能优化 | 好
需手动使用 useSelector 与 shallowEqual 等优化,手动避免不必要渲染 | 好
内置了优化的 selector API | 很好
自动追踪依赖,只渲染变化组件 | 很好
内置 selector,浅比较自动优化 |
包体积 (gzip 后) | 较大 (~8-10KB, 含 react-redux) | 中等 (~5-7KB, 含 Redux RTK 全部功能) | 中等 (~5-6KB, 含 mobx 和 mobx-react-lite) | 很小 (~588B) |
中间件生态 | 丰富
至少 3 种以上广泛应用的中间件如 thunk、saga、observable 等 | 丰富
同 Redux 完全兼容 | 一般
较少现成中间件,大多手写 reaction | 一般
支持中间件功能,但生态不够丰富 |
异步处理 | 好
需使用额外中间件如 thunk / saga | 很好
内置 createAsyncThunk API 简化异步 | 很好
可直接写入 action,灵活 | 很好
完全自由,无额外限制 |
社区生态 | 非常好
Star: 61.3k
周下载量:13M | 非常好 (同 Redux 生态)
Star:11k
周下载量:5.3M | 好
Star:28k
周下载量:1.2M | 非常好
Star:53.4k
周下载量:8.1M |
并发渲染兼容 | 完全兼容 useSyncExternalStore | 完全兼容 (同 Redux) | 完全兼容 useSyncExternalStore | 完全兼容 useSyncExternalStore |
服务端渲染 (SSR) 支持 | 支持 (需手动实现 hydration) | 支持 (同 Redux,需要 hydrate) | 支持 (需额外注意依赖追踪方式) | 支持 (官方提供 SSR 示例代码) |
基于这个标准,我对比调研了主流的三个方案:Redux、MobX 和 Zustand,最后选择了 Zustand。
初识 Zustand
Zustand 是一款轻量级的 React 状态管理库,其名称来源于德语中的「状态」一词(发音为「粗」或「Zoo」)。
为什么选择 Zustand?
极简的API设计
- 无需繁琐的 action 类型定义和 reducer 函数
- 不需要 Context Provider 去包装组件树
- 使用范式更接近 React Hooks,学习成本极低
- 源码极其简洁,易于理解和调试
零模板代码
- 没有冗余的样板代码
- API 数量少且直观,学习曲线平滑
- 侵入性弱,支持渐进式接入和快速移除
性能优异
- 支持精准的 Selector 订阅,仅在相关状态变化时触发重渲染
- 避免了传统方案中常见的过度渲染问题
扩展性强
- 提供 Middleware 机制,实现十分轻巧
- 官方支持 DevTools、Persist 持久化存储、Immer 等常用扩展
体积极小
- Minified + Gzipped 后仅约 588B
- 对项目包体积几乎无影响
与其他现代状态管理库一样,基于 React 18 的
useSyncExternalStore 实现,确保了并发渲染下的数据一致性 useSyncExternalStore 是什么?
为什么会有这样一个 Hook ,我们需要先从 React 的渲染机制和并发特性说起……
React 渲染机制的演进:从同步到并发
React 15 — Stack Reconciler
在 React 15 及更早版本中,React 使用 Stack Reconciler 进行渲染:
特点:
- 数据一致性天然保证 - 所有组件在同一时刻渲染,读取的是同一份数据快照
- 逻辑简单 - 递归遍历,执行流程清晰
- 性能瓶颈 - 大型组件树会导致明显卡顿
- 无法中断 - 一旦开始渲染就必须完成
数据结构源码和示意图:
典型渲染流程:
React 16+ — Fiber Reconciler
Fiber 是 React 16 之后的新的渲染调度引擎,支持可中断渲染、优先级更新(异步、分片、流式渲染)、更细粒度的控制。
核心改进:
- 用链表替代递归,允许任务切片 / 中断 / 恢复
- 支持优先级调度(比如用户操作、高优先级动画)
- 引入时间切片(Time Slicing),避免长任务阻塞主线程
Fiber 结构
Fiber 是一个对象,每个 Fiber 对应 UI 树上的一个节点(
FunctionComponent / ClassComponent / Dom 节点)。特点:
- Fiber 树结构(return、child、sibling、index):让 Fiber 形成链表式的「多叉树」,可以高效遍历和操作。
- 双缓冲(alternate):workInProgress / current,核心就是允许并发渲染和切换,提高性能。
- 处理副作用(flags、subtreeFlags、deletions):驱动 React commit 阶段的 DOM 操作(插入 / 删除 / 更新)。
- props、state(pendingProps、memoizedProps、memoizedState、updateQueue):渲染和更新的核心数据。
渲染与调度流程
核心代码主要位置:
调度器:
github/react/packages/scheduler/src/forks/Scheduler.js核心循环:
github/react/packages/react-reconciler/src/ReactFiberWorkLoop.js优先级:
github/react/packages/react-reconciler/src/ReactFiberLane.js 关键函数及代码简化版示意:
- workLoopConcurrent
- performUnitOfWork
- shouldYield / shouldYieldToHost
- 调度(Scheduler)
- 根据优先级(如 transition、userBlocking、default、idle),决定先处理哪些任务;
- 利用浏览器的
MessageChannel、requestIdleCallback等机智实现异步分片。
- 协调(Reconciler)
- 比较新老 Fiber 树,找出变化,构建「effect list」(需要增 / 删 / 改 的节点);
- 遍历 Fiber 树,不是一次性递归,而是「一段一段」完成。
- 提交(Commit)
- 一次性批量把变更同步到真实 DOM。此过程不可中断。
- 副作用
- commit 后会依次执行 useEffect 等副作用 Hook
简化示意图:
并发渲染的挑战:UI 撕裂(Tearing)
Fiber 的可中断渲染虽然提升了性能,但也引入了新问题——UI 撕裂(Tearing):
撕裂的根本原因:
- React 15:所有组件同步渲染,读取同一时刻的数据快照
- React 16+:组件可能在不同时间点渲染,读取到不同版本的外部数据
这种数据不一致性会导致:
- 用户界面显示混乱
- 数据逻辑错误
- 用户体验下降
useSyncExternalStore:解决方案
useSyncExternalStore 正是为了解决并发渲染下的数据一致性问题而设计的:核心机制:
- 将外部 store 的订阅与快照读取严密绑定在 React 的调度机制内
- 确保同一渲染周期中,所有组件都读取到同一份数据快照
- 在 commit 未完成前,不管外部 store 如何变化,当前渲染树都使用固定快照
这样就从根源上解决了「并发渲染 + 外部状态管理」的数据同步问题。
Zustand 快速上手与核心原理
重温了 React 渲染机制后,让我们正式进入今天的主角——Zustand。
快速上手
创建 Store
使用
create() 函数创建 store,传入一个回调函数 (set, get) => ({ ...stateValues, ...stateActions }) 来定义初始状态和更新方法:组件中使用
Zustand 的
create() 返回一个 React Hook,在组件中直接调用即可:关键特性:
- 通过 selector 函数
(state) => state.xxx精确订阅状态片段
- 只有相关状态变化时组件才会重渲染,自动优化性能
- 无需 Provider 包装,任何组件都可直接使用
状态更新机制
调用
set 函数更新状态,支持两种方式:更新特点:
- 默认使用浅合并,未指定的属性保持不变
- 只有状态真正改变时才触发重渲染(通过
Object.is判断)
- 支持函数式更新,避免闭包陷阱
深入源码解析
现在让我们深入 Zustand 的源码,理解其核心实现原理。
Zustand 源代码非常好读,但为了便于理解,我们分析编译后的简化版本。
pnpm build:react
pnpm build:vanilla
pnpm build:middleware:immer
1. createStoreImpl(Zustand 核心 Store 实现)
pnpm build:vanilla核心设计理念:
- 闭包隔离:
state和listeners都是私有变量,只能通过返回的 API 访问
- 发布订阅模式:经典的观察者模式,解耦状态变化和视图更新
- 性能优化:使用
Object.is避免无效更新,使用Set管理订阅者
- 函数式设计:无副作用,易于测试和调试
2. useStore & create(React 绑定)
useSyncExternalStore 的核心作用:
- 解决并发渲染下的数据一致性:确保同一渲染周期内所有组件读到相同快照
- 自动订阅管理:组件挂载时订阅,卸载时自动清理
- 优化重渲染:只有 Select 选中的状态片段变化时才触发重渲染
React 绑定流程:
回顾上面的代码
详细执行流程:
- 初始化阶段:
create()创建 vanilla store- 执行
createState函数,初始化状态 - 返回绑定的 React Hook
- 组件渲染阶段:
- 调用
useCounterStore(selector) useSyncExternalStore订阅 store 变化- 读取当前状态快照并返回 select 到的的状态片段
- 状态更新阶段:
- 用户点击按钮,调用
increment increment内部调用set(),更新状态- 通知所有订阅者(包括
useSyncExternalStore) - React 重新渲染相关组件
3. middleware/immer
Zustand 的中间件采用了高阶函数的设计模式,通过函数组合的方式扩展 store 的功能。
Middleware 封装调用流程
使用 Immer 中间件前后的对比:
4. middleware/persist
5. middleware/devtools
6. 中间件组合使用
7. 自定义中间件实现
示例:日志中间件
示例:性能监控中间件
Zustand 踩坑实践
1)一次获取整个 Store
不要直接全量读取整个 state,这样写虽然简单,但任何一个 state 字段改变,都会导致组件重渲染。
2)拆分大 store 为多个更小、更易维护的独立 Slice
- 定义独立的 Store 模块
例如,我们分别定义两个小模块:
Fish Slice
Bear Slice
- 组合多个模块
使用 Zustand 提供的 create 方法组合这些模块:
- React 组件中方便地使用组合后的 store:
4. 同时更新多个模块
当需要在一个操作中同时更新多个模块时,可以定义组合方法:
组合时与之前一样简单:
5. 添加 Middleware
中间件应当仅应用在组合后的 store,而非单个模块内。
例如,使用持久化中间件:
通过上述方式可以精确控制渲染,又保证了复杂逻辑和大 Store 的可维护性。
3)重写 State
set 第二个参数(replace)是 true 时,会替换整个 state,不是 merge。
小心用法,否则一不小心把 action 一起删没了。
4)Async actions
异步 action 没有特殊写法,直接用 async / await 即可。
Zustand 完全支持异步,只需在最后 set 一下。
5) 在 Action 中获得最新的状态
action 里需要用到当前最新 state,直接用 get()。
6) 在组件外读写 state / 订阅变化
有时候你需要在组件外部同步获取或操作 state,比如工具函数、非 React 代码。Zustand 提供了一组原生方法:
7) 使用 subscribeWithSelector 精确监听
如果你只想订阅某个字段的变化,而不是全量 state,推荐使用 subscribeWithSelector middleware。
结语
1. 技术选型的思考
- 从实际问题出发,对比主流方案的优缺点,选择最适合项目的工具
- 考虑学习成本、维护成本
2. Zustand 的核心优势
- 简单、轻量、高效
3. 源码层面的理解
- Vanilla Store + React 绑定的分层设计很清晰
- 中间件的高阶函数设计模式值得学习
useSyncExternalStore是现代外部状态管理的标准方案
实际应用建议
- 小项目:用
setState/props足矣,不需要引入外部工具
- 中项目:需要全局共享状态,有性能需求,直接用 Zustand,简单直接
- 大项目:用 Slice 模式拆分,配合适当的中间件,兼顾性能和维护
- 复杂状态:加上 Immer 中间件,避免手写 Immutable 不可变更新
- 调试需要:开启 DevTools 中间件
如果大家在项目中遇到类似的状态管理问题,可以考虑尝试 Zustand,有什么问题也可以随时跟我交流讨论。
最后感谢 ChatGPT 和 Claude 辅助我阅读源代码,帮助我节约了大量时间!
参考链接:
Zustand:
React: