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
, [])

可总算完美了...

文章中涉及的三方库地址:

unstated-next

hox

zustand

代码可见:react-hook-state-manager

以上是关于React Hooks 状态管理方案探索的主要内容,如果未能解决你的问题,请参考以下文章

React Hooks 状态管理方案探索

react hooks的缺点(针对状态不同步和没有生命周期)

react hooks的缺点(针对状态不同步和没有生命周期)

如何使用 React Hooks 实现复杂组件的状态管理

2-2-4 & 5 & 6 React Hooks基础API

基于 React Hooks 实现一个状态管理库