React Hooks 状态管理方案探索
Posted Songlcy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React Hooks 状态管理方案探索相关的知识,希望对你有一定的参考价值。
React 中的状态声明,一般会这么写
local state hooks
const Button = () =>
const [number, setNumber] = useState(0);
return (
<Button onPress=setNumber((preState) => preState + 1)>number</Button>
)
状态逻辑和视图强耦合,违反视图归视图,逻辑归逻辑的常理。多组件内共享状态是通过层层传递的方式实现,带来冗余代码的同时,根组件的状态将会逐渐变成 “庞然大物”。
状态逻辑抽离成独立hook
function useNumber()
const [number, setNumber] = useState(0);
const increment = useCallback(() =>
setNumber((preState) => preState + 1)
, [number]);
return
number,
increment
;
const Button = () =>
const [number, increment] = useNumber();
return (
<Button onPress=increment>number</Button>
)
舒服多了,状态逻辑视图分离,符合 React Hooks 【逻辑复用】的口号。
Context 响应式能力
为了避免组件间层层传递状态,可以使用 Context 解决方案。Context 提供了在组件之间共享状态的方法,而不必在树的每个层级分别显式传递
// hook.js
export function useNumber()
const [number, setNumber] = useState(0);
const increment = useCallback(() =>
setNumber((preState) => preState + 1)
, [number]);
return
number,
increment
;
export default React.createContext(useNumber)
// app.js
import Number from 'hook.js';
const App = () =>
const useNumberService = useNumber();
<Number.Provider value=useNumberService>
<CustomButton xxx='123' />
</Number.Provider>
const CustomButton = () =>
const number = useContext(Number);
return (
<Button onPress=increment>number</Button>
)
很清爽,但是在使用的时候还是免不了要先定义很多Context,并且在子组件中进行引用,繁琐的体力劳动显得多余。可以对代码进行进一步的封装,将 Context 定义和引用的步骤抽象成公共的方法
function createContainer()
// 初始化Context
const Store = React.createContext();
// 子组件消费
function useContainer()
return React.useContext(Store);
// 父组件注入
function Provider(useHook)
const store = useHook();
const children, = props;
return <Store.Provider value=store>children</Store.Provider>
return Provider, useContainer ;
// app.js
const Store = createContainer();
const App = () =>
<Store.Provider useHook=useNumber><CustomButton /></Store.Provider>
const CustomButton = () =>
const number, incrment = useContainer();
Context 需要嵌套 Provider 组件,一旦代码中使用多个 context,将会造成层层嵌套的问题(嵌套地狱),组件的可读性和纯粹性会直线降低,从而导致组件重用更加困难。
unstated-next就是这么玩的!
export function createContainer<Value, State = void>(
useHook: (initialState?: State) => Value,
): Container<Value, State>
let Context = React.createContext<Value | typeof EMPTY>(EMPTY)
function Provider(props: ContainerProviderProps<State>)
let value = useHook(props.initialState)
return <Context.Provider value=value>props.children</Context.Provider>
function useContainer(): Value
let value = React.useContext(Context)
if (value === EMPTY)
throw new Error("Component must be wrapped with <Container.Provider>")
return value
return Provider, useContainer
export function useContainer<Value, State = void>(
container: Container<Value, State>,
): Value
return container.useContainer()
抛弃Context,自定义状态管理
export function useNumber()
// 代码省略
return
number,
increment
;
function createContainer(useHook)
let store;
const listeners = new Set();
// 获取hook返回的数据
const store = useHook();
// 子组件消费hook返回的数据
function useContainer()
return store;
return useContainer ;
const CustomButton = () =>
// 业务代码使用:API简洁
const useContainer = createContainer(useNumber);
const number = useContainer();
return (
<Text>number</Text>
)
useNumber中的number更新了,怎么触发CustomButton刷新?
定义一个 listeners 集合,在子组件初始化的时候往数组添加 listener 回调,订阅状态的更新。
在listener中如何更新?这里可以利用 useReducer hook 定义一个自增函数,使用 forceUpdate 方法即可让组件重刷。
const [, forceUpdate] = useReducer((c) => c + 1, 0);
useContainer()
const storeRef = useRef(store);
const [, forceUpdate] = useReducer((c) => c + 1, 0);
useEffect(() =>
function listener(newStore)
storeRef.current = newStore;
forceUpdate();
// 初始化的时候添加回调,订阅更新
listeners.add(listener);
// 组件销毁的时候移除回调
return () => listeners.delete(listener)
,[])
return storeRef.current;
如何监听 useNumber hook 中的值发生变化了?也就是在哪去遍历listeners集合中的listener?(listeners的实现,可视为典型的观察者模式)
需要要定义一个内聚函数,利用useEffect来监听数据变化
const Executor = (props) =>
const store = props.hook();
const mountRef = useRef(false);
// 状态管理库初始化
if (!mountRef.current)
props.onMount(store);
mountRef.current = true;
// store 一旦变更,就会执行 useLayoutEffect 回调
useLayoutEffect(() =>
props.onUpdate(store); // 一旦状态变更,通知依赖的组件更新
);
return null;
;
function createContainer(hook)
let store;
const listeners = new Set(); // 定义回调集合
const onUpdate = (store) =>
for (const listener of listeners)
listener(store);
;
const onMount = (val) =>
store = val;
;
此时Executor需要渲染器来触发执行,可以通过 react-reconciler 构造一个 react 渲染器来挂载 Executor 组件,来分别支持 React 环境下的不同架构实现。
import ReactReconciler from "react-reconciler";
const reconciler = ReactReconciler(
now: Date.now,
getRootHostContext: () => (),
prepareForCommit: () => (),
resetAfterCommit: () => ,
getChildHostContext: () => (),
shouldSetTextContent: () => true,
createInstance: () => ,
createTextInstance: () => ,
appendInitialChild: () => ,
appendChild: () => ,
finalizeInitialChildren: () => false,
supportsMutation: true,
appendChildToContainer: () => ,
prepareUpdate: () => true,
commitUpdate: () => ,
commitTextUpdate: () => ,
removeChild: () => ,
clearContainer: () => ,
supportsPersistence: false,
getPublicInstance: instance => instance,
preparePortalMount: () => ,
isPrimaryRenderer: false,
supportsHydration: false,
scheduleTimeout: setTimeout,
cancelTimeout: id => clearTimeout(id),
noTimeout: -1
);
export function render(reactElement)
const container = reconciler.createContainer(null, 0, false, null);
return reconciler.updateContainer(reactElement, container);
render(<Executor onMount=onMount hook=hook onUpdate=onUpdate />)
完美了。
总结一下,观察者模式listeners注册组件,建立依赖。利用useReducer的forceUpdate触发组件刷新。Executor 负责 useNumber hook中的数据状态管理,并在数据状态发生改变后,通知组件刷新。其中 想要监听Executor中数据状态的改变,需要渲染器来执行Executor,此处使用了react-reconciler执行render。
使用
export function useNumber()
const [number, setNumber] = useState(0);
const increment = useCallback(() =>
setNumber((preState) => preState + 1)
, [number]);
return
number,
increment
;
export default createContainer(useNumber);
const CustomButtonA = () =>
// 业务代码使用:API简洁
const number = useContainer();
return (
<Text>number</Text>
)
const CustomButtonB = () =>
// 业务代码使用:API简洁
const increment = useContainer();
return (
<Button onPress=increment />
)
从未见过如此完美的代码!
精确更新
在listener中我们执行了forceUpdate,当前组件依赖的数据状态没有发生变化,也会触发更新,如何做到精确更新?(组件依赖的数据发生变化时,再触发更新)
在forceUpdate之前加个diff比较一下
function listener(newStore)
const newValue = newStore[dep];
const oldValue = storeRef.current[dep];
// 仅仅在依赖发生变更,才会组件进行更新
if (compare(newValue, oldValue))
forceUpdate();
storeRef.current = newStore;
dep怎么拿到呢???
hox就是这么玩的!
// hox create-model.tsx
const useModel: UseModel<T> = depsFn =>
const [state, setState] = useState<T | undefined>(() =>
container ? container.data : undefined
);
// 记录新旧依赖
const depsFnRef = useRef(depsFn);
depsFnRef.current = depsFn;
const depsRef = useRef<unknown[]>(
depsFnRef.current?.(container.data) || []
);
useEffect(() =>
if (!container) return;
function subscriber(val: T)
// 无依赖函数声明,强制执行刷新
if (!depsFnRef.current)
setState(val);
else
const oldDeps = depsRef.current;
const newDeps = depsFnRef.current(val);
// diff数据变化
if (compare(oldDeps, newDeps))
setState(val);
depsRef.current = newDeps;
, [container]);
return state!;
;
在组件使用hook时,传递一个depsFn,该依赖函数会返回依赖项数组,在listener中进行diff比较即可
zustand就是这么玩的!
用 Executor 作为中间桥梁来感知状态变更,有点委屈求全,方案显得复杂化。如果变更状态的方法 setState 是由状态管理库自身实现,一旦执行该方法,内部即可感知状态的变更,并触发后续的diff更新操作
// 创建 store
import create from 'zustand'
const useStore = create(set => (
bears: 0,
increasePopulation: () => set(state => ( bears: state.bears + 1 )),
removeAllBears: () => set( bears: 0 )
))
从使用方式上可见,使用create方法,传入 createStore Fn 创建store
// zustand context.ts
const Provider = (
initialStore,
createStore,
children,
:
/**
* @deprecated
*/
initialStore?: TUseBoundStore
createStore: () => TUseBoundStore
children: ReactNode
) =>
const storeRef = useRef<TUseBoundStore>()
if (!storeRef.current)
if (initialStore)
console.warn(
'Provider initialStore is deprecated and will be removed in the next version.'
)
if (!createStore)
createStore = () => initialStore
storeRef.current = createStore()
return createElement(
ZustandContext.Provider,
value: storeRef.current ,
children
)
源码中可以看到,利用Context,创建Provider,并将create传入的参数作为Provider的value。从这里可以看出,只要value发生变化,useContext的地方将会立即刷新。接着往下看使用方式
// 使用 store
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>
使用useStore来获取数据或方法。
const useStore: UseContextStore<TState> = <StateSlice>(
selector?: StateSelector<TState, StateSlice>,
equalityFn = Object.is
) =>
// ZustandContext value is guaranteed to be stable.
const useProviderStore = useContext(ZustandContext)
if (!useProviderStore)
throw new Error(
'Seems like you have not used zustand provider as an ancestor.'
)
return useProviderStore(
selector as StateSelector<TState, StateSlice>,
equalityFn
)
源码中调用了useContext,并返回对应store。即将状态,操作状态的方法交给zustand自己管理。
当状态发生时,对比新旧数据是否发生改变,改变则调用对应listener回调,调用useReducer的forceUpdate触发组件刷新。
// useIsomorphicLayoutEffect = useLayoutEffect
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
, [])
可总算完美了...
文章中涉及的三方库地址:
以上是关于React Hooks 状态管理方案探索的主要内容,如果未能解决你的问题,请参考以下文章
react hooks的缺点(针对状态不同步和没有生命周期)
react hooks的缺点(针对状态不同步和没有生命周期)