前言
在 React 推出 React Hooks 之前我们最常会使用 Redux 来做状态管理方案,集中化管理数据,可预测性,丰富的周边工具。几乎就是 React 下状态管理的正确答案。但是使用广泛并不意味着没有缺点:相对复杂的概念,繁重的模板代码,在大型工程下姑且可以作为最佳实践一般,规范不同成员的编写。但是小工程用起来总感觉是 短袖套棉袄,工程本身没多少逻辑代码,硬加了很多 Redux 的模板代码
React Hooks 来临,得益于良好的逻辑封装能力和简洁的 API(最突出的就是使用极其方便的 useContext ),近似代数效应的表现。因此一大批基于 Hooks 的状态管理库诞生,包括 React-Redux 也提供了基于 Hooks 的 API 使用,推出了 redux-toolkit
-
扩展:什么是代数效应?
代数效应
是函数式编程
中的一个概念,用于将副作用
从函数
调用中分离,将代码中的what
和how
分开一个例子:
function getTotalPicNum(user1, user2) { const num1 = getPicNum(user1); const num2 = getPicNum(user2); return picNum1 + picNum2; }
在这个函数里我们计算图片总数,但是不需要关心
getPicNum
的实现,单一职责。假如获取图片总数需要请求的方式呢?
async function getTotalPicNum(user1, user2) { const num1 = await getPicNum(user1); const num2 = await getPicNum(user2); return picNum1 + picNum2; }
但是
async / await
是具有传染性的,为此你不得不也改变调用getTotalPicNum
函数的方式,它从同步变成了异步我们假设有一个实现了代数效应的语法
perform
,它能够在执行到的时候自动暂停原有代码,在获得picNum
之后再恢复原有代码的执行function getPicNum(name) { const picNum = perform name; return picNum; } try { getTotalPicNum('xiaoming', 'xiaohong'); } handle (who) { // 或者执行一个请求 switch (who) { case 'xiaoming': resume with 230; case 'xiaohong': resume with 122; default: resume with 0; } }
React Hooks 在 React 函数编程中实现了类似代数效应的效果,你无需关心
useState
、useReducer
、useRef
这样的Hook
是如何保存你的数据。 但它不是真正的代数效应
为什么会有这篇文章
一次偶然的机会我遇见了 unstate-next ,轻巧简洁,借助 useContext 就可以实现基础的状态管理能力。虽然使用上依然会遇到一些小问题,但是具有十足的启发性,那 Hooks 下什么是理想的状态管理方案,这里会介绍下现在主流的几个状态管理仓库
State
什么是 状态?
在 jQuery
时代,JS 代码混杂 DOM 操作,代码长且难以理解
面条式代码,程序代码就像面条一样扭曲纠结
$("#btn1").click(function(){
$("p").append(" <b>Appended text</b>.");
var d = Math.floor(t/1000/60/60/24) <= 0 ? 0 : Math.floor(t/1000/60/60/24)
$("#t_d").html(d);
if (d <= 0) {
$("#day").hide();
$('#second').show();
}
});
$("#btn2").click(function(){
$("p").hide();
$("#test2").html("<b>Hello world!</b>");
});
$("#btn3").click(function(){
$("#test3").val("Dolly Duck");
$("p").show();
});
随着现代前端框架的来临,它们都有一个重要的理念:数据驱动视图
由原来编写过程的 “命令式代码“ 改为 “操作各种数据”,而这些变化的数据就是 状态(State)
React State
Class Component
时的 state 是 this.state
Function Component
时的 state 是 useState
/ useReducer
为了避免过于复杂的应用代码,我们会通过“拆分”,形成多个组件,之间通过 props
传递 state
进行通信。同时遵守“单向数据流”
React
也引入 Context
解决组件间状态通信导致 props
层层传递复杂的问题
Redux
核心流程图:
概念:
-
Store:保存数据的地方,你可以把它看成一个容器,整个应用只能有一个 Store。
-
State:Store 对象包含所有数据,如果想得到某个时点的数据,就要对 Store 生成快照,这种时点的数据集合,就叫做 State。
-
Action:State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。
-
Action Creator:View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦,所以我们定义一个函数来生成 Action,这个函数就叫 Action Creator。
-
Reducer:Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。
-
dispatch:是 View 发出 Action 的唯一方法。
一个例子:
// action.js export const INCREMENT = 'INCREMENT'; export const DECREMENT = 'DECREMENT'; export const RESET = 'RESET'; export function increaseCount() { return ({ type: INCREMENT}); } export function decreaseCount() { return ({ type: DECREMENT}); } export function resetCount() { return ({ type: RESET}); } // reducer.js import {INCREMENT, DECREMENT, RESET} from '../actions/index' const INITIAL_STATE = { count: 0, history: [], } function handleChange(state, change) { const {count, history} = state; return ({ count: count + change, history: [count + change, ...history] }) } export default function counter(state = INITIAL_STATE, action) { const {count, history} = state; switch(action.type) { case INCREMENT: return handleChange(state, 1); case DECREMENT: return handleChange(state, -1); case RESET: return (INITIAL_STATE) default: return state; } } // store.js export const CounterStore = createStore(reducers)
原理:
核心代码在
createStore.js
,主要关注两个函数 -
subscribe 函数
注册监听事件,然后返回取消订阅的函数,所有的订阅函数统一放一个数组里(
nextListeners
),只维护这个数组 -
dispatch 函数
- 调用 Reducer,传参(currentState,action)。
- 按顺序执行 listener。
- 返回 action。
redux-tookit 简化的例子
// counterSlice.js import { createSlice, PayloadAction } from '@reduxjs/toolkit' export interface CounterState { count: number } const initialState: CounterState = { count: 0, } export const counterSlice = createSlice({ name: 'counter', initialState, reducers: { increment: (state) => { // Redux Toolkit allows us to write "mutating" logic in reducers. It // doesn't actually mutate the state because it uses the Immer library, // which detects changes to a "draft state" and produces a brand new // immutable state based off those changes state.count += 1 }, decrement: (state) => { state.count -= 1 }, incrementByAmount: (state, action: PayloadAction<number>) => { state.count += action.payload }, }, }) // Action creators are generated for each case reducer function export const { increment, decrement, incrementByAmount } = counterSlice.actions export default counterSlice.reducer // store.js import { configureStore } from '@reduxjs/toolkit' import counterReducer from '../features/counter/counterSlice' export const store = configureStore({ reducer: { counter: counterReducer, }, }) // Infer the `RootState` and `AppDispatch` types from the store itself export type RootState = ReturnType<typeof store.getState> // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} export type AppDispatch = typeof store.dispatch
我想用 Context 做状态管理方案
-
假设现在我们有一个 Foo 组件,组件内部有一个 count state
const Foo = () => { const [count, setCount] = React.useState(0); return ( <> <h1>{count}</h1> <button onClick={() => setCount(v => v + 1)}>Click Me!</button> </> ); };
-
现在我们希望其他组件也能访问到这个 count state
const CountContext = React.createContext(null); const CountProvider = ({ children }) => { const [value] = React.useState(0); return ( <Context.Provider value={value}> {children} </Context.Provider> ); }; const Foo = () => { const count = React.useContext(CountContext); return ( <> <h1>{count}</h1> </> ); }; const Bar = () => { const count = React.useContext(CountContext); return <h2>{count % 2 ? 'Even' : 'Odd'}</h2> }; const Buz = () => ( <CountProvider> <Foo /> <Bar /> </CountProvider> );
-
进一步,我们更好的方式是将 Provider 提升到顶部
const App = () => ( <CountProvider> <Layout /> </CountProvider> );
-
进一步的我们发现,虽然我们共享了状态,但是没办法修改它?
将修改 state 的方法跟随 state 一起通过 Context 传下去
这时候我们写的代码已经非常接近 unstate-next 了
unstate-next
unstate-next 可以理解为提供了一种使用 context 作为状态管理的范式
源码非常简单,只有不到40行,并且仅 2 个 API(****useContainer
, createContainer
**)
-
Example
import React, { useState } from "react" import { createContainer } from "unstated-next" function useCounter(initialState = 0) { let [count, setCount] = useState(initialState) let decrement = () => setCount(count - 1) let increment = () => setCount(count + 1) return { count, decrement, increment } } let Counter = createContainer(useCounter) function CounterDisplay() { let counter = Counter.useContainer() return ( <div> <button onClick={counter.decrement}>-</button> <span>{counter.count}</span> <button onClick={counter.increment}>+</button> </div> ) } function App() { return ( <Counter.Provider> <CounterDisplay /> <Counter.Provider initialState={2}> <div> <div> <CounterDisplay /> </div> </div> </Counter.Provider> </Counter.Provider> ) }
使用的时候我们传入 封装了 state 和修改 state 的自定义 Hooks,返回 container,带有 Provider 和 useContainer
问题
-
provider 不支持 displayName
优化 DevTools 的显示
-
需要特别注意 context 重复渲染导致的性能问题
Context 下的组件会因为 父组件的 render 而 render,为了避免重复渲染,可以采取的措施
-
子组件提升为 提升为 props.children
-
“W memo”,对 context value 使用 useMemo,对子组件使用 React.memo 包裹
但是如果我们把所有 value 都放在一个 context 中,会导致不管子组件是否用到它,都会导致重渲染,需要采用拆分
-
Split contexts
-
拆分 Contexts,对于不同上下文背景的 Contexts 进行拆分
-
将多变的和不变的 Contexts 分开,让不变的 Contexts 在外层,多变的 Contexts 在内层
-
借助 Split contexts 依然无法改变的就是 state 和 setState 的单一依赖重复渲染问题
随着你的应用 Provider 越来越多,它会变成这个样子,多美妙的图形【误
const App = () => ( <Context1Provider> <Context2Provider> <Context3Provider> <Context4Provider> <Context5Provider> <Context6Provider> <Context7Provider> <Context8Provider> <Context9Provider> <Context10Provider> <Layout /> </Context10Provider> </Context9Provider> </Context8Provider> </Context7Provider> </Context6Provider> </Context5Provider> </Context4Provider> </Context3Provider> </Context2Provider> </Context1Provider> );
-
constate
constate 使用方式和 unstate-next 非常相似,在 unstate-next 的基础上改进了几个问题
-
提供了 displayName
-
提供了selectors API,更加精细的使用 context value
-
Example
import React, { useState, useCallback } from "react"; import constate from "constate"; function useCounter() { const [count, setCount] = useState(0); // increment's reference identity will never change const increment = useCallback(() => setCount(prev => prev + 1), []); return { count, increment }; } const [Provider, useCount, useIncrement] = constate( useCounter, value => value.count, // becomes useCount value => value.increment // becomes useIncrement ); function Button() { // since increment never changes, this will never trigger a re-render const increment = useIncrement(); return <button onClick={increment}>+</button>; } function Count() { const count = useCount(); return <span>{count}</span>; }
核心代码
根据 传入的 selectors 为每个创建 context
if (selectors.length) {
selectors.forEach((selector) => createContext(selector.name));
} else {
createContext(useValue.name);
}
根据创建的 contexts 自动生成嵌套的 Provider
const Provider: React.FC<Props> = ({ children, ...props }) => {
const value = useValue(props as Props);
let element = children as React.ReactElement;
for (let i = 0; i < contexts.length; i += 1) {
const context = contexts[i];
const selector = selectors[i] || ((v) => v);
element = (
<context.Provider value={selector(value)}>{element}</context.Provider>
);
}
return element;
};
zustand
-
2021最🔥的状态管理库
zustand 采用的全局的状态管理方案。基于观察者模式 。API 清晰简单,不需要额外处理 Context 的重复渲染的问题,不需要 Provider ,相对的通过 forceUpdate 实现组件的更新。脱离于 React 自身的状态,支持非 React 环境使用。
-
Example
import create from 'zustand' const useStore = create(set => ({ bears: 0, increasePopulation: () => set(state => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }) })) function BearCounter() { const bears = useStore(state => state.bears) return <h1>{bears} around here ...</h1> } function Controls() { const increasePopulation = useStore(state => state.increasePopulation) return <button onClick={increasePopulation}>one up</button> }
核心代码
-
Vanilla.ts
观察者模式的实现,提供了 setState、subscribe 、getState 、destroy 方法。并且前两者提供了 selector 和 equalityFn 参数
-
setState
代码const setState: SetState<TState> = (partial, replace) => { const nextState = typeof partial === 'function' ? partial(state) : partial if (nextState !== state) { const previousState = state state = replace ? (nextState as TState) : Object.assign({}, state, nextState) listeners.forEach((listener) => listener(state, previousState)) } }
-
getState
代码const getState: GetState<TState> = () => state
-
subscribe
代码const subscribe: Subscribe<TState> = <StateSlice>( listener: StateListener<TState> | StateSliceListener<StateSlice>, selector?: StateSelector<TState, StateSlice>, equalityFn?: EqualityChecker<StateSlice> ) => { if (selector || equalityFn) { return subscribeWithSelector( listener as StateSliceListener<StateSlice>, selector, equalityFn ) } listeners.add(listener as StateListener<TState>) // Unsubscribe return () => listeners.delete(listener as StateListener<TState>) }
-
createStore
function createStore(createState) { let state: TState const setState = /** ... */ const getState = /** ... */ /** ... */ const api = { setState, getState, subscribe, destroy } state = createState(setState, getState, api) return api }
-
-
react.ts
基于 React Hooks API 接口的封装,实现状态的注册和更新,通过 forceUpdate 对组件重渲染
-
组件重渲染的方式
const [, forceUpdate] = useReducer((c) => c + 1, 0) as [never, () => void]
-
判断状态是否更新
const state = api.getState() newStateSlice = selector(state) hasNewStateSlice = !equalityFn( currentSliceRef.current as StateSlice, newStateSlice ) useIsomorphicLayoutEffect(() => { if (hasNewStateSlice) { currentSliceRef.current = newStateSlice as StateSlice } stateRef.current = state selectorRef.current = selector equalityFnRef.current = equalityFn erroredRef.current = false })
-
状态更新通知组件重渲染
useIsomorphicLayoutEffect(() => { const listener = () => { try { const nextState = api.getState() const nextStateSlice = selectorRef.current(nextState) if (!equalityFnRef.current(currentSliceRef.current as StateSlice, nextStateSlice)) { stateRef.current = nextState currentSliceRef.current = nextStateSlice forceUpdate() } } catch (error) { erroredRef.current = true forceUpdate() } } const unsubscribe = api.subscribe(listener) if (api.getState() !== stateBeforeSubscriptionRef.current) { listener() // state has changed before subscription } return unsubscribe }, [])
-
-
context.ts
借助
React Context
分发store
,实现创建多个互不干扰的 store 实例const useStore: UseContextStore<TState> = <StateSlice>( selector?: StateSelector<TState, StateSlice>, equalityFn = Object.is ) => { const useProviderStore = useContext(ZustandContext) return useProviderStore( selector as StateSelector<TState, StateSlice>, equalityFn ) }
-
middleware.ts
中间件的本质是各种函数根据顺序互相嵌套调用,在 Zustand 中,中间件是将 setState 函数包裹起来,Zustand 天生就是使用这种函数式的设计
以内置的持久化状态中间件为例:
// 使用的例子 export const useStore = create(persist( (set, get) => ({ fishes: 0, addAFish: () => set({ fishes: get().fishes + 1 }) }), { name: "food-storage", // unique name getStorage: () => sessionStorage, // (optional) by default, 'localStorage' is used } )) // middleware.ts 实现缩略代码 // 修改传入 options 的 set 函数 const configResult = config( ((...args) => { set(...args) void setItem() }) as CustomSetState, get, api )
jotai
同样的全局状态管理方案,跟 zustand
是同一个作者。借鉴了 Recoil
的原子化概念
同样 API 也十分简单, atom
和 useAtom
。通过 atom
创建原子化的数据源,调用useAtom
传入 atom
返回对应的状态值和修改方法。
import { atom } from 'jotai'
const countAtom = atom(0)
const countryAtom = atom('Japan')
const citiesAtom = atom(['Tokyo', 'Kyoto', 'Osaka'])
const mangaAtom = atom({ 'Dragon Ball': 1984, 'One Piece': 1997, Naruto: 1999 })
function Counter() {
const [count, setCount] = useAtom(countAtom)
return (
<h1>
{count}
<button onClick={() => setCount(c => c + 1)}>one up</button>
</h1>
)
}
原子化的数据有非常强的派生能力
const doubledCountAtom = atom((get) => get(countAtom) * 2)
function DoubleCounter() {
const [doubledCount] = useAtom(doubledCountAtom)
return <h2>{doubledCount}</h2>
}
同时可以通过 get
和 set
修改原子状态的值和修改的方法
const decrementCountAtom = atom(
(get) => get(countAtom),
(get, set, _arg) => set(countAtom, get(countAtom) - 1),
)
function Counter() {
const [count, decrement] = useAtom(decrementCountAtom)
return (
<h1>
{count}
<button onClick={decrement}>Decrease</button>
</h1>
)
}
-
虽然是全局状态管理方案,但同时支持 Provider 使用
方便查看源码的理解:
-
atom
类比key
-
state
类比store
-
atomState
类比value
-
原理:官方文档给的最简版本
import { useState, useEffect } from "react"; // atom 函数返回一个配置对象,其中包含初始值 export const atom = (initialValue) => ({ init: initialValue }); // 跟踪原子的状态 // 使用 WeakMap 存储 atomState // 把 atom 当做key, atomState 当做 value 存储 const atomStateMap = new WeakMap(); const getAtomState = (atom) => { let atomState = atomStateMap.get(atom); if (!atomState) { atomState = { value: atom.init, listeners: new Set() }; atomStateMap.set(atom, atomState); } return atomState; }; // useAtom 钩子类似 useState export const useAtom = (atom) => { const atomState = getAtomState(atom); const [value, setValue] = useState(atomState.value); useEffect(() => { const callback = () => setValue(atomState.value); // 添加 atom 的更新回调,就是更新当前 Hooks 的 Value atomState.listeners.add(callback); callback(); return () => atomState.listeners.delete(callback); }, [atomState]); const setAtom = (nextValue) => { atomState.value = nextValue; // 更新时执行所有 listeners atomState.listeners.forEach((l) => l()); }; return [value, setAtom]; };
-
支持派生 atom
const atomState = { value: atom.init, listeners: new Set(), dependents: new Set() };
valtio
借助 Proxy & Reflect API 代理状态
import { proxy, useSnapshot } from 'valtio'
const state = proxy({ count: 0, text: 'hello' })
// This will re-render on `state.count` change but not on `state.text` change
function Counter() {
const snap = useSnapshot(state)
return (
<div>
{snap.count}
<button onClick={() => ++state.count}>+1</button>
</div>
)
}
-
核心代码
const handler = { get(target: T, prop: string | symbol, receiver: any) { // 省略部分代码 return Reflect.get(target, prop, receiver) }, deleteProperty(target: T, prop: string | symbol) { const prevValue = Reflect.get(target, prop) const childListeners = prevValue?.[LISTENERS] if (childListeners) { childListeners.delete(popPropListener(prop)) } const deleted = Reflect.deleteProperty(target, prop) if (deleted) { notifyUpdate(['delete', [prop], prevValue]) } return deleted }, is: Object.is, canProxy, set(target: T, prop: string | symbol, value: any, receiver: any) { const prevValue = Reflect.get(target, prop, receiver) if (this.is(prevValue, value)) { return true } const childListeners = prevValue?.[LISTENERS] if (childListeners) { childListeners.delete(popPropListener(prop)) } if (isObject(value)) { value = getUntracked(value) || value } let nextValue: any if (Object.getOwnPropertyDescriptor(target, prop)?.set) { nextValue = value } // 省略部分代码 Reflect.set(target, prop, nextValue, receiver) notifyUpdate(['set', [prop], value, prevValue]) return true }, } const proxyObject = new Proxy(baseObject, handler)
总结
对于一个 状态管理方案 我们知道一定存在两个关键的概念:
- 数据源
- 数据消费
对于数据源:
基于 Context API 实现 value 传递,同时由于 Provider 可以放在应用的任何地方,也可以称作 局部状态管理方案,与之对应的就有 全局状态管理方案
(基于 Context API 自带 state 变化 重新渲染的机制,因此 全局状态管理方案 需要额外处理如何跟踪 state 的变化并重新渲染组件)
对于数据消费:
类似上面基于 context 和 redux 的不可变(immutable)数据消费
还有 Mobx,valtio 为代表的可变(mutable)数据
数据消费方式的不同也决定了他们 如何追踪 state 的变化,不可变(immutable)数据基于 发布订阅/观察者模式,而可变(mutable)数据基于 Proxy 代理 的方式