字节都在用的 React 状态管理

背景

目前我们的应用很多是采用 createContainer 来管理状态的,实现逻辑和 unstated-next 类似,但是它依然存在 Context 状态变化时会渲染所有消费者组件的问题。

有一种通过双 Context 实现依赖收集,按需渲染的设计,一起来学习一下。

状态管理

首先来实现一个状态管理的功能,类似于 unstated-next。本质上是 Context 和 Hooks 组合的模式,其中 useHook 是用户自定义的 hooks,用来生产数据;Provider 用来提供数据;useContainer 用来消费数据。

function createContainer(useHook) {



  const Context = createContext(null); 

  // 用来提供数据
  const Provider = function Provider({ children }) {
    const state = useHook();
    return (
      <Context.Provider value={state}>
        {children}
      </Context.Provider>
    );
  };


  // 消费数据
  const useContainer = function () {
    const value = useContext(Context);
    return value;
  };


  return {
    Provider,
    useContainer,
  };
}

用一个示例来演示如何使用 createContainer:

const { Provider, useContainer } = createContainer(function useHook() {
  const [counter, setCounter] = useState(0);
  return {
    counter,
    setCounter
  };
});

function Child() {
  const { counter, setCounter } = useContainer();
  return <div onClick={() => setCounter(counter + 1)}>{counter}</div>
}



function App() {
  return <Provider>
    <Child />
  </Provider>
}

依赖收集

通过 Context 管理状态,如果 Context 状态变化,每个消费 Context 状态的组件都会重新渲染,但是我们希望如果组件只用到了数据中的某个字段,只有当这个字段变化时才重新渲染。

举个例子:Context 的状态是 { a: 1, b: 1 },组件 A 用到了字段 a,我们期望的是只有当 a 变化时组件 A 才重新渲染,b 变化时不重新渲染。

如果想实现上面的功能,就需要收集依赖,当数据变化时,定向触发依赖的执行。

既然 Context 状态变化会引起消费者渲染,那么消费者就不能依赖有状态 Context,所以我们加一层无状态 Context 来提供数据,据此来调整一下 createContainer:

function createContainer(useHook) {



  // 有状态

  const IntrinsicContext = createContext(null);

  // 无状态,下面会通过 ref 来保存最新的数据
  const ObservableContext = createContext(null);




  const Provider = function Provider({ children }) {

    // ....
    
    return (
      <IntrinsicContext.Provider value={state}>
        <ObservableContext.Provider value={observableValue}>
          {children}
        </ObservableContext.Provider>
      </IntrinsicContext.Provider>
    );
  };

为了在 IntrinsicContext 状态变化时,通知组件渲染,我们引入观察者模式,其中 state 保存最新数据,observers 保存消费数据的回调函数:

function createContainer(useHook) {



  // 有状态

  const IntrinsicContext = createContext(null);

  // 无状态,通过 ref 来保存最新的数据
  const ObservableContext = createContext(null);




  const Provider = function Provider({ children }) {

    // 通过 ref 来保存数据,不会触发组件渲染
    const { current: observableValue } = useRef({ state: null, observers: [] });

    const state = useHook();
    // 保存最新的数据
    observableValue.state = state;

    useEffect(() => {
      // 通知观察者
      observableValue.observers.forEach(observer => observer());
    });


    return (
      <IntrinsicContext.Provider value={state}>
        <ObservableContext.Provider value={observableValue}>
          {children}
        </ObservableContext.Provider>
      </IntrinsicContext.Provider>
    );
  };
  
  // ....
  const useContainer = function (_depCb) {
    // ...
  }
}

上面实现了 Provider 可以提供数据了,下面来看如何消费数据,消费数据的两个重点是:1、需要知道哪些字段(依赖)变化时,需要触发渲染;2、字段(依赖)变化时如何处理,也就是 observer:

function createContainer(useHook) {



      // ....
      
      // depCb 用来返回依赖列表,比如 () => [field1, field2]
      const useContainer = function (_depCb) {
        const depCp = useCallback(_depCb, []);
    
        const observableValue = useContext(ObservableContext);
        const [state, setState] = useState(observableValue.state);
        const prevDepsRef = useRef([]);
    
        useEffect(() => {
          const observer = () => {
            const prev = prevDepsRef.current;
            const cur = depCp(observableValue.state);
            // 通过浅比较,来判断依赖是否有变化
            if (!shallowCompare(prev, cur)) {
              // 触发渲染
              setState(observableValue.state);
            }
            prevDepsRef.current = cur;
          }
    
          observableValue.observers.add(observer);
    
          return () => {
            observableValue.observers.delete(observer);
          };
        }, []);
    
        return state;
      };
}

代码优化

为了让代码更具可读性和可维护性,可以把 { observers: [], state: null } 封装成 ObservableValue,比如:

class ObservableValue {
  constructor(state) {
    this.observers = new Set();
    this.state = state;
  }



  notify() {
    for (const observer of this.observers) {
      observer();
    }
  }
}

总结

本文通过 Context + Hooks 实现了对开发者友好的状态管理工具,然后我们又通过双 Context 的模式实现了 React 的依赖收集,两种实现都很巧妙。

如果对你有启发,欢迎点赞、评论。

参考

github.com/jamiebuilds…

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYPJn9PM' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片