React状态管理库
React状态管理库
月晕状态管理的实现机制
在React中我们是数据驱动视图,可以用一个公式来描述状态和UI的关系,即:
1 | UI = f(state) |
React中的一个核心概念就是幂等,即是在React Component中,输入相同的props & state & context
总是会得到相同的UI
通常状态管理库是可以不结合任何框架用来使用,即为vanilla,可以在(zustand、jotai)等库中清晰的看到vanila实现,这些包含了基本的JavaScript实现,而我们平常中使用的react框架,则是这些实现的积类 + Hooks,即(useMemo & useCallback & useState & useEffect
) 中触发更新UI的hooks
状态管理库的整个变动流程基于如下的过程
其中订阅的含义是组件需要定于 Store 状态的变化,这样当状态发生改变时可以触发组件完成re-render,订阅和更新 UI 是结合React中的useState
/ useReducer
/ useSyncExternalStore
来完成的,订阅的过程其实就是收集这些Hooks调用过程中的dispatcher的过程,用来更新订阅的UI
useState
: 返回setState
调用setState
来更新状态并使组件re-render1
const [state, setState] = useState(initialState)
useReducer
: 返回dispatch
,调用dispatch
可以更新状态并使组件re-render1
const [state,dispatch] = useReducer(reducer,initialArg)
useSyncExternalStore
: 需要传入subscribe
,useSyncExternalStore
会传入一个函数,这个函数使组件去re-render1
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
状态管理库的实习形式会把这些更新状态的函数保存起来,通过是一个Set
的数据结构,这样更新Store时,会遍历整个Set
中的函数来触发各个组件的re-render,如下
这样更新Store状态时才能正确的通知组件re-render,其实就是一种发布订阅的设计模式
在订阅了 Store 状态的组件中,当 Store 发生改变,所有订阅了该 Store 的事件都会得到更新去通知组件re-render
即全局状态管理库可以保证组件之间共享状态和维持状态的一致性
React Context的问题
在早期的React中,要使用全局状态管理一般是使用的context,确实是可以解决组件之间共享状态的问题,但是之所以会出现很多的状态管理库(zustand、mobx、jotai)就是为了解决context带来的麻烦和性能
产生性能问题
当在多个组件中传递同一个属性时,这个属性变化会导致所有组件发生re-render,即使某些组件没有真正使用到该值,会导致不必要的开销,造成性能上的问题
1 | function updateContextProvider ( |
React会调用Object.is(oldValue, newValue)
来对比context前后的状态是否一致,如果不一致 React 会沿着当前节点遍历Fiber树来寻找消费了当前 context 的组件,并对其进行标记代表这个组件应该被重现渲染
心智负担
要不断地写provider和context,且值很难跟踪到
全局状态管理库的特点
原子化/非原子化
一个前端应用由多个组件组合而成,组件之间有了共享状态的需求,状态管理库就出现了,但是对于管理数据的方式出现了两种case
- Store:
Redux | zustand
为首的单一状态树 - Atom:
jotai
为首的原子化状态管理,原子化指的是整个应用外部状态被拆分为了一个个分片的状态片段,每个状态片段保存在各自的atom中,各自atom组合交错共同组成了整个应用的状态
Mutable/Immutable
根据全局状态库,状态的可变性可以分成Mutable(可变)
和Immutable(不可变)
Mutable: 指创建对象之后任何状态更新都是基于对原先对象的基础上进行的,比如
mobx | valtio
就是这种1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import { proxy, useSnapshot } from 'valtio';
const state = proxy({
count: 0,
});
export default function App() {
const snapshot = useSnapshot(state);
return (
<div>
<div>{snapshot.count}</div>
<button onClick={() => (state.count += 1)}>+1</button>
</div>
);
}在上面例子中可以发现,点击按钮后会直接在原始状态上进行修改来完成状态的更新,即
state.count+1
Immutable:指的是对象一旦创建就不能被修改,需要基于这个对象生成新的对象而保证原始对象不可变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// Zustand
import { create } from "zustand";
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
export default function App() {
const { count, increment } = useStore();
return (
<div>
<div>{count}</div>
<button onClick={increment}>+1</button>
</div>
);
}对于Immutable模型的状态管理库而言,不能直接在原先的对象上修改,而需要生成一个新的对象,包含修改后的
count
值
对于复杂的多层嵌套数据结构来说,Immutable方式处理比较麻烦,且容易出错,要写很多层的解构,而Mutable方式则更加的自然,符合代码的逻辑更改,且基于 Mutable 方案下的状态管理库内部基于 Proxy 来实现,会监听组件对于状态的使用情况,从而在更新状态时正确触发对应的组件完成 re-render,因此这种方案下我们可以认为性能默认就是最优的,不需要手动来优化。
Immutable的好处在于保证可预测性,而Mutable很难保证这点,状态每次发生变化时,这些变化可以被追踪以及最终状态的更新的结果是明确已知的
手动/半自动/自动优化
- Zustand、React Redux等基于 selector 优化的状态管理库,背后会使用
use-sync-external-store
库,需要传入store使用selector选出组件关系的状态 - Jotai 基于原子化模型的状态管理,每个原子维护自己的小状态片段,并通过原子之间的相互依赖关系阻止re-render,提高性能,不需要手动传递selector
- MobX基于Mutable的状态管理库,内部采用Proxy来实现,自动监听组件使用了那些状态,只有状态变化才会触发组件re-render
全局状态管理/服务器状态管理
对于React中的状态我们可以粗略的分成三类
- 局部状态: 指的是组件内使用和维护的状态,通过使用useState和useReducer钩子来创建和管理这些状态,控制组件内的行为,比如用户输入、表单控制等的
- 全局状态: 当状态需要在多个组件中共享的时候,我们使用全局状态,这些状态不属于某个特定的组件,而是整个应用的共享资源,React中提供了React Context API,但是我们通常选择更加专业的
zustand、jotai、mobx
- 服务器状态: 这种状态涉及到与服务器的交互,服务器状态是从服务器获取的数据,并在前端中展示和管理,管理服务器状态的流行库比如SWR、TanStack Query 等,提供获取数据、缓存、更新等功能,简化了与后端数据交互的复杂性
通常我们在大型项目中,组合使用可以达到良好的效果
zustand实践
通常可以使用immer中间件配合使用,可以状态一套use、update、subscribe方法去实现 store的共享
1 | import { create } from "zustand"; |