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 项目。起初本着「大道至简」的原则,我选择用 useStateprops 来管理应用状态,希望做到简单轻量。
这种原生方案对于简单场景确实足够,但现实往往比理想复杂。我的项目中需要使用大量 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
  1. 调度(Scheduler)
      • 根据优先级(如 transition、userBlocking、default、idle),决定先处理哪些任务;
      • 利用浏览器的 MessageChannelrequestIdleCallback 等机智实现异步分片。
  1. 协调(Reconciler)
      • 比较新老 Fiber 树,找出变化,构建「effect list」(需要增 / 删 / 改 的节点);
      • 遍历 Fiber 树,不是一次性递归,而是「一段一段」完成。
  1. 提交(Commit)
      • 一次性批量把变更同步到真实 DOM。此过程不可中断。
  1. 副作用
      • 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
核心设计理念:
  1. 闭包隔离statelisteners 都是私有变量,只能通过返回的 API 访问
  1. 发布订阅模式:经典的观察者模式,解耦状态变化和视图更新
  1. 性能优化:使用 Object.is 避免无效更新,使用 Set 管理订阅者
  1. 函数式设计:无副作用,易于测试和调试
 
 
2. useStore & create(React 绑定)
 
useSyncExternalStore 的核心作用:
  1. 解决并发渲染下的数据一致性:确保同一渲染周期内所有组件读到相同快照
  1. 自动订阅管理:组件挂载时订阅,卸载时自动清理
  1. 优化重渲染:只有 Select 选中的状态片段变化时才触发重渲染
React 绑定流程:
 
回顾上面的代码
详细执行流程:
  1. 初始化阶段
      • create() 创建 vanilla store
      • 执行 createState 函数,初始化状态
      • 返回绑定的 React Hook
  1. 组件渲染阶段
      • 调用 useCounterStore(selector)
      • useSyncExternalStore 订阅 store 变化
      • 读取当前状态快照并返回 select 到的的状态片段
  1. 状态更新阶段
      • 用户点击按钮,调用 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

  1. 定义独立的 Store 模块
例如,我们分别定义两个小模块:
Fish Slice
Bear Slice
  1. 组合多个模块
使用 Zustand 提供的 create 方法组合这些模块:
  1. 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:
 

© Chaoran Sun 2024 - 2026