能够做到组件透传,全局通信
任意两个或者多个组件之间可以利用状态管理工具互相通信,不需要通过 props 层层传递
能够做到任意两个或者多个组件之间可以利用状态管理工具互相通信,不需要通过 props 层层传递,达到全局通信的效果
能够做到的状态管理库包括
| 库 | 全局通信 | 描述 |
|---|---|---|
| Context API | React 内置 | |
| Redux | Flux 模式,单向数据流 | |
| Zustand | 轻量级 Hook 流派 | |
| Recoil | 原子化状态管理 | |
| Jotai | 原子化状态管理 | |
| Mobx | 响应式状态管理 | |
| Valtio | 响应式状态管理 | |
| dva | redux 的衍生品,基于 redux 的增强版 | |
| Hook/State | React 内置 |
假设,我们把所有的状态都放到全局状态管理中去管理,那么,项目变得越来越大之后,带来的一个主要的问题,就是如何方便的管理那么多的全局数据。
因此,好的状态管理器还需要提供数据隔离的思路或者方式,在做到全局状态共享的同时,又能避免状态冲突。
| 库 | 全局通信 | 描述 |
|---|---|---|
| Context API | 默认支持,由开发者控制状态的影响范围 | |
| Redux | 单独定义 reducer 来区分不同页面的不同状态 | |
| Zustand | 为每个页面单独创建一个 store 模块 | |
| Recoil | 添加唯一 key 值 | |
| Jotai | 每个页面都单独使用一个 Provider 包裹页面组件 | |
| Mobx | 为每个页面创建一个 Store 实例,然后在 RootStore 中合并 | |
| Valtio | 为每个页面创建一个单独的 proxy 实例 | |
| dva | 为每个页面创建一个单独的 model 实例,通过 namespace 唯一命名区分 | |
| Hook/State | 默认支持,由开发者控制状态的影响范围 |
简单展示几个常用状态库的处理方式
01/**02* 为每个页面创建单独的 reducer,然后合并成一个 root reducer03*/04import { combineReducers } from 'redux';05import homeReducer from './homeReducer';06import profileReducer from './profileReducer';07import settingsReducer from './settingsReducer';0809const rootReducer = combineReducers({10home: homeReducer, // 对应 state.home11profile: profileReducer, // 对应 state.profile12settings: settingsReducer // 对应 state.settings13});
React 的冗余渲染问题,是 React 性能差的主要原因,这也是我们在开发中需要解决的核心问题。一个好的状态管理器,应该能够帮助我们避免冗余渲染,从而提高性能。
| 库 | 全局通信 | 描述 |
|---|---|---|
| Context API | 完全无法避免冗余渲染,甚至他本身就是元凶之一 | |
| Redux | 可以避免,但要注意写法 | |
| Zustand | 可以避免,但要注意写法 | |
| Recoil | 可以避免,原子化细粒度更新,但在初始时收集依赖内存开销高,性能消耗大 | |
| Jotai | 可以避免,原子化细粒度更新,并且通过按需收集依赖的方式避免初始时的高性能消耗 | |
| Mobx | 可以避免,且非常优秀,Mobx 能够做到属性级别的细粒度更新,能轻松处理上万条数据,是复杂数据且高频交互场景的首选方案 | |
| Valtio | 可以避免,且非常优秀,与 Mobx 能力类似,但更切合 React,代码更简洁 | |
| dva | 可以避免,但要注意写法 | |
| Hook/State | 无法直接避免,需要手动调整组件的依赖关系,成本高,但效果最好 |
又称为状态隔离、沙箱隔离等
全局状态管理,并不是 React 项目中的唯一需求。在许多时候,我们可能仅需要一部分组件共享状态,并且这部分组件会在项目中创建多个实例,此时单例能力就无法满足需求,需要支持多例能力。
| 库 | 全局通信 | 描述 |
|---|---|---|
| Context API | 默认支持,在局部组件的顶层使用 Provider 包裹组件即可 | |
| Redux | 默认支持,在局部组件的顶层使用 Provider 包裹组件即可 | |
| Zustand | 结合 Context API 可以做到,将定义的单例 store 存储在 Context 的 useRef 中即可,大厂面试考点 | |
| Recoil | 在局部组件的顶层嵌套 RecoilRoot 即可 | |
| Jotai | 提供了单独 Provider 组件,嵌套在局部组件的顶层即可 | |
| Mobx | 配合 Context API 创建的 Provider 组件,嵌套在局部组件的顶层即可 | |
| Valtio | 配合 Context API 创建的 Provider 组件,嵌套在局部组件的顶层即可 | |
| dva | 在局部组件的顶层使用 Provider 包裹组件即可 | |
| Hook/State | 默认支持 |
只要同时满足了以上 4 个条件,那么,我们就认为这个状态管理库是一个能够在项目中使用的,剩下的,就是一些在使用过程中的细节、喜好问题。
他们在底层设计上,主要可以分为如下三类
| 单向数据流 | 原子化 | Proxy |
|---|---|---|
| redux | Recoil | Mobx |
| zustand | jotai | valtio |
在选择时,通常是选择 zustand、jotai、valtio,他们都是对应类型前者的优化版本。
单向数据流的优势是数据本身比较干净。负担轻。但是当数据结构非常复杂的时候,通常需要结合不可变数据集 immutable.js、immer.js 等才能做到最佳的性能表现。
原子化方式与 proxy 方式都是一致的,都是收集数据与 UI 的绑定关系,当数据发生变化时,UI 会自动更新。他们的区别就是,原子化在写法上,是先定义原子,然后通过原子来管理数据。
而 Proxy 是先定义一个大一点的对象,然后通过 Proxy 来劫持对象的属性,然后再将属性与 UI 进行绑定。因此在性能表象上,原子化的性能会略微好一些,他少了劫持的过程。但是当数据开始变得复杂时,原子化的写法可能也会比较繁琐。
在处理复杂数据时,初始化创建 Atom 对象的开销比较大,但是 Proxy 的包装开销也会比较大。他们在更新时的开销都比较小。
在处理大型复杂列表数据时,他们的表现如下所示
| 库 | 初始化速度 | 更新速度 | 内存占用 | 开发复杂度 |
|---|---|---|---|---|
| Zustand | 极快 | 快 | 极低,原生对象 | 较高 |
| jotai | 较慢 | 精准,快 | 最高,原子实例多 | 偏高 |
| valtio | 中等偏慢 | 精准,快 | 偏高,Proxy 开销大 | 低 |
在更新上的具体细节表现如下
| 库 | 通知复杂度 | 渲染复杂度 | 原理 |
|---|---|---|---|
| Zustand | O(N) | O(1) | 线性遍历订阅列表 + Selector 比对 |
| Jotai | O(1) | O(1) | 依赖图。原子 A 变了,直接找到订阅了 A 的那一个组件。 |
| Valtio | O(1) | O(1) | Proxy 追踪。属性 A 变了,直接精准通知访问过属性 A 的组件。 |
简单分析一下他们在内存中的表现
a、 Zustand:内存利用率的王者
Zustand 的 Store 本质上就是一个闭包里的普通 JavaScript 对象
如果你有 10,000 条数据,Zustand 占用的内存几乎等于这 10,000 条原始 JSON 数据的内存。它不会为每一条数据创建额外的包装对象。
在额外开销上,它仅有一个微小的订阅列表(Listeners Set),占用空间可以忽略不计
因此,在内存受限(如低端移动设备)或数据量极其巨大(万级以上)的场景下,Zustand 是首选
b、 Jotai:内存消耗的重灾区(针对长列表)
为了实现精准更新,Jotai 的 splitAtom 模式会为数组中的每一个元素创建一个 Atom 对象
如果你有 1000 个以上的列表项,Jotai 会在内存中创建同样个数的全新的 Atom 实例
在额外开销上,每个 Atom 实例都是一个对象,包含了自己的 key 和引用信息。此外,Jotai 内部的 store 还会维护一个巨大的 WeakMap 来映射这些原子的状态。
所以,在处理超大型列表时,Jotai 的内存占用会呈线性爆发式增长。如果列表项非常多,可能会导致浏览器频繁进行垃圾回收(GC),从而引发掉帧
c、 Valtio:Proxy 带来的额外负荷
Valtio 通过 ES6 Proxy 递归地包装你的原始对象,这里需要注意的是,由于列表已经需要渲染了,因此此时的按需收集依赖的方式,已经没有太大的用处了。
在内存表现上,Proxy 对象比普通对象更“重”。每一个嵌套的对象和数组都会被转化成 Proxy 实例。
并且在额外开销上,当你调用 useSnapshot 时,Valtio 会创建当前状态的一个“快照”。虽然它使用了结构共享(Structural Sharing)来复用未变动的部分,但在渲染瞬间依然会产生临时对象。
除此之外,Valtio 还需要存储额外的映射关系来追踪哪些属性被哪些组件访问了。
不过 Valtio 的内存占用虽然高于 Zustand,但在大多数中等规模(千级数据)应用中是完全可以接受的。
在不同的场景之下,可能有不同的答案,因此,我们需要根据具体场景来选择合适的状态管理库。
场景 A:大型数据量(如:在线 Excel、大型看板、大型轨迹数据)
建议选择:Zustand
此时内存和初始化速度是生死线。你不能承受为每个数据点创建 Proxy 或 Atom 的开销。你需要最原始的 JS 对象和手动优化的订阅逻辑(ID 订阅模式)
场景 B:中等规模列表,交互极复杂(如:多列配置的列表、带复杂逻辑的购物车)
建议选择:Valtio
虽然它比 Zustand 多占一点内存,但它带来的开发效率提升巨大。其“自动追踪”功能能让你省去写大量 Selector 的痛苦,且更新性能非常精准。
场景 C:动态增减频繁、需要自动清理状态(如:多页签编辑器、独立任务卡片)
建议选择:Jotai
Jotai 的内存开销虽然在大列表时是劣势,但它的生命周期管理是优势。当一个组件卸载,对应的 Atom 状态会被自动垃圾回收。如果你的列表是动态变动的,Jotai 能帮你保持内存的“新鲜度”。
当然,如果你的项目中,没有特别复杂的数据结构,那么,无论选择哪一种,其实都是可以胜任的,此时,可能我们更需要考虑的是编码喜好和开发复杂度的问题。因此
追求极致轻量(省内存、省 CPU) → Zustand
追求逻辑严密(原子组合、按需销毁) → Jotai
追求开发爽感(自动优化、代码最少) → Valtio