美文网首页
react跨组件状态流:用事件流实现一个极其轻量高效的状态流工具

react跨组件状态流:用事件流实现一个极其轻量高效的状态流工具

作者: joyer_li | 来源:发表于2024-07-11 11:55 被阅读0次

    如果你也喜欢使用react的函数组件,并喜欢使用react原生的hook进行状态管理,但为了跨组件状态流而不得不引入redux,MboX这种具有自己独立的状态管理的重量级/对象级的状态流框架的话,本文会给你提供一种新的极其轻量的解决跨组件状态流方案。

    Context的问题

    首先探讨如果不采用redux,mobx,使用原生的react的跨组件共享状态方案Context,会具备那些问题?

    react原生的跨组件通信为Context。在使用Context进行组件之间通信时,需要进行状态提升,提升到需要通信的组件的公共的祖先节点之中。这会导致当数据的变化时祖先节点产生re-render, 从而祖先节点中的整个组件树都会re-render,带来非常大的性能损失。react官方推荐使用React.memo包裹函数,降低非必要组件渲染。如:

    const Context = React.createContext<any>({})
    const SubCompA: React.FC<{}> = React.memo(() => {
      console.log('渲染了A');
      const { number } = React.useContext(Context);
      return (<div>
        {number}
      </div>);
    });
    const SubCompC: React.FC<{}> = React.memo(() => {
      console.log('渲染了C');
      const { setNumber } = React.useContext(Context);
      return (<button className='__button' onClick={() => {
        setNumber(10);
      }}>我是按钮</button>);
    });
    const SubCompB: React.FC<{}> = React.memo(() => {
      console.log('渲染了B');
      return (<div>
        <SubCompC />
      </div>);
    });
    const SubCompD: React.FC<{}> = React.memo(() => {
      console.log('渲染了D');
      return (<div></div>);
    });
    const Root: React.FC<{}> = React.memo(() => {
      console.log('渲染了Root');
      const [number, setNumber] = React.useState(1);
      return (<Context.Provider value={{ number, setNumber }}>
        <SubCompA />
        <SubCompB />
        <SubCompD />
      </Context.Provider>);
    });
    

    在本案例中,点击按钮后,会导致组件SubCompA, SubCompC, Root组件re-render,但SubCompC, Root都是不受期望的re-render。且在实际使用情况下,性能会损失更大,因为:

    • 不会把每一个状态单独放到一个的Context中。当Context中包含多个状态时,任何一个状态发生变化后,不管有没有依赖具体发生变化的那个状态,所有使用了该Context的组件都会更新,导致re-render的非法扩散(不受期望的re-render)。
    • 非常依靠React.memo发挥效果,但在实际开发过程,使React.memo保持完美运行是一件非常困难的事情。如不应该传递给组件的属性值使用对象和函数的字面量。

    如下面的对于组件的使用:

    const CompA: React.FC<{}> = React.memo(() => {
      return (<div>1</div>);
    });
    
    const Root: React.FC<{}> = React.memo(() => {
      return (<CompA objectProp={{ name: 'joy' }} onClick={() => {
        // ....
      }} />);
    });
    

    在本案例中,上文对于CompA进行React.memo包裹将没有一点意义。需要调整为:

    const CompA: React.FC<{}> = React.memo(() => {
      return (<div>1</div>);
    });
    
    const Root: React.FC<{}> = React.memo(() => {
      const objectProp = React.useMemo(() => ({ name: 'joy' }));
      const handleClick = React.useCallback(() => {
        // ....
      }, []);
      return (<CompA objectProp={objectProp} onClick={handleClick} />);
    });
    

    这里并不是想说memo没有必要。memo是提升性能的一个很重要的手段,在平常开发过程中,非常需要严格遵循,努力使memo发挥作用。

    综上所述,Context中的性能损失,主要的原因是状态提升导致更大范围的组件re-render造成。

    新的方案

    为了解决原生Context的问题,不能进行状态进行提升,而是在不同的组件中存在多个相同含义的状态,然后通过统一的机制管理这些状态的值,使它实际效果跟Context状态提升的状态一致即可。管理机制可以采取事件。

    如:

    const eventEmitter = new EventEmitter();
    const CompA: React.FC<{}> = React.memo(() => {
      const [age, setAge] = React.useState(0);
      React.useEffect(() => {
        eventEmitter.addListener('updateAge', setAge);
      }, []);
      return (<div>{state}</div>);
    });
    
    const CompB: React.FC<{}> = React.memo(() => {
      return (<div onClick={() => {
        eventEmitter.emit('updateAge', 10);
      }}>1</div>);
    });
    
    const Root: React.FC<{}> = React.memo(() => {
      return (<>
        <CompA />
        <CompB />
      </>);
    });
    

    但实际场景中,不能这样使用,因为:

    • 在复杂系统中,需要的管理的状态流非常庞大,随着迭代事件名也非常难以管理,为解决重名问题慢慢也会蜕变成redux或者MboX那种采取对象命名空间;
    • 相同意义的状态,实际上还是会存在多个状态(不同组件上),这些状态除了受到受到事件的管理,还能自己控制,极易带来数据没有保持一致的风险;

    解决事件名的问题,可以采取动态创建随机的事件名来解决。在需要通信的组件共同的祖先节点中,封装一个事件监听管理器中,屏蔽掉内部事件名的逻辑:

    const eventEmitter = new EventEmitter();
    
    function useSharedState() {
      const eventNameRef = React.useRef<string>(`SHARE_STATE_${String(Math.random()).slice(2)}`);
    
      React.useEffect(() => {
        const eventName = eventNameRef.current;
    
        return () => {
          // 注销事件
          if (emitter.eventNames().includes(eventName)) {
            emitter.removeAllListeners(eventName);
            emitter.off(eventName);
          }
        };
      }, []);
    
      const emit = React.useCallback((value) => {
        emitter.emit(eventNameRef.current, value);
      }, []);
    
      const addListener = React.useCallback((callback) => {
        eventEmitter.addListener(eventNameRef.current, callback);
      }, []);
    
      const channel = React.useMemo(() => ({
        emit, addListener,
      }), []);
    
      return channel;
    }
    
    const Context = React.createContext<any>({});
    const CompA: React.FC<{}> = React.memo(() => {
      const { channel } = React.useContext(Context);
      React.useEffect(() => {
        channel.addListener(setAge);
      }, []);
      return (<div>{state}</div>);
    });
    
    const CompB: React.FC<{}> = React.memo(() => {
      return (<div onClick={() => {
        channel.emit(10);
      }}>1</div>);
    });
    
    const Root: React.FC<{}> = React.memo(() => {
      const channel = useSharedState();
      return (<Context.Provider value={{ channel }}>
        <CompA />
        <CompB />
      </Context.Provider>);
    });
    

    为了节省内存的使用,所有的事件通信将使用同一个事件流。

    为了保证状态值一致性更加可控,也为了使「状态」看起来更加像一个状态,还需要将每个组件中的状态的使用和更新进行封装起来:

    const eventEmitter = new EventEmitter();
    
    function useSharedState() {
      const eventNameRef = React.useRef<string>(`SHARE_STATE_${String(Math.random()).slice(2)}`);
    
      React.useEffect(() => {
        const eventName = eventNameRef.current;
    
        return () => {
          // 注销事件
          if (emitter.eventNames().includes(eventName)) {
            emitter.removeAllListeners(eventName);
            emitter.off(eventName);
          }
        };
      }, []);
    
      const setValue = React.useCallback((value) => {
        emitter.emit(eventNameRef.current, value);
      }, []);
    
      const addListener = React.useCallback((callback) => {
        eventEmitter.addListener(eventNameRef.current, callback);
      }, []);
    
      const useValue = React.useMemo(() => {
        return () => {
          // eslint-disable-next-line react-hooks/rules-of-hooks
          const [state, setState] = React.useState(valueRef.current);
    
          React.useLayoutEffect(() => {
            addListener(setState);
          }, []);
          return state;
        };
      }, []);
    
      const channel = React.useMemo(() => ({ useValue, setValue }), []);
    
      return channel;
    }
    

    在组件的共同祖先节点中,会创建一个复杂的状态通信管理器,可以称之为通道。通道通过Context下传到各个需要的组件,由于通道都是常量值,本身是不会触发任何组件的re-render。利用通道可以创建状态,此时才会创建一个真正的react状态,状态的更新将会导致当前的组件的re-render。同时通道封装了对这个状态的值更新逻辑,当在任何一个组件中更新当前react状态时,都会通过事件同步到其他组件的同样业务含义的react状态,达到「感觉就是一个状态」的效果。

    至此,一个跨组件的react状态流就已经实现。然后为了提高可用性,参考一些signal相关设计添加一些api,支持一些特殊场景,在增加亿点点细节,变为:

    import * as React from 'react';
    import EventEmitter from 'eventemitter3';
    import isFunction from 'lodash.isfunction';
    
    export type Value<A> = (A | ((prevState: A) => A));
    export type Dispatch<A> = (value: Value<A>) => void;
    export type UseValue<A> = () => A;
    export type GetValue<A> = () => A;
    export type SubscribeCallback<A> = (value: A) => void;
    export type Subscribe<A> = (callback: SubscribeCallback<A>) => () => void;
    
    const emitter = new EventEmitter();
    
    export interface Channel<S> {
      /**
       * 获取信号最新值,该值不支持响应式
       */
      getValue: GetValue<S>;
      /**
       * 获取信号值的hook,注意符合hook的使用规范
       */
      useValue: UseValue<S>;
      /**
       * 设置信号值
       */
      setValue: Dispatch<S>;
      /**
       * 信号值变化的订阅函数
       */
      subscribe: Subscribe<S>;
    }
    
    export default function useSharedState<S>(
      initialState: S | (() => S),
    ): Channel<S> {
      const eventNameRef = React.useRef<string>(`SharedState_${String(Math.random()).slice(2)}`);
      const initialValue: S = React.useMemo(() => {
        if(isFunction(initialState)) {
          return initialState();
        }
        return initialState;
      // eslint-disable-next-line react-hooks/exhaustive-deps
      }, []);
      const valueRef = React.useRef<S>(initialValue);
    
      React.useEffect(() => {
        const eventName = eventNameRef.current;
    
        return () => {
          if (emitter.eventNames().includes(eventName)) {
            emitter.removeAllListeners(eventName);
            emitter.off(eventName);
          }
        };
      }, []);
    
      const dispatch: Dispatch<S> = React.useCallback<Dispatch<S>>((value) => {
        valueRef.current = isFunction(value) ? value(valueRef.current) : value;
        emitter.emit(eventNameRef.current, valueRef.current);
      }, []);
    
      const subscribe: Subscribe<S> = React.useCallback<Subscribe<S>>((callback) => {
        // 避免重复注册
        emitter.off(eventNameRef.current, callback);
        emitter.addListener(eventNameRef.current, callback);
        // 注销
        return () => {
          emitter.off(eventNameRef.current, callback);
        };
      }, []);
    
      const useValue: UseValue<S> = React.useMemo<UseValue<S>>(() => {
        return () => {
          // eslint-disable-next-line react-hooks/rules-of-hooks
          const [state, setState] = React.useState<S>(valueRef.current);
          const subscribeFn = React.useCallback<SubscribeCallback<S>>((value) => {
            setState(value);
          }, []);
    
          // eslint-disable-next-line react-hooks/rules-of-hooks
          React.useLayoutEffect(() => {
            const unsubscribe = subscribe(subscribeFn);
            return () => {
              unsubscribe();
            };
          }, [subscribeFn]);
          return state;
        };
      // eslint-disable-next-line react-hooks/exhaustive-deps
      }, []);
    
      const getValue: GetValue<S> = React.useCallback<GetValue<S>>(() => {
        return valueRef.current;
      }, []);
    
      const sharedState = React.useMemo<Channel<S>>(() => ({
        useValue, getValue, setValue: dispatch, subscribe,
      }), []);
    
      return sharedState;
    }
    

    相关库已经发布到npm上,为@joyer/react-use-shared-state, 欢迎体验。

    支持react>16.18, 特别声明支持18版本, 本人项目中已经使用并上线2年多

    优势

    • 非常轻量,改方案想要解决的问题非常简单,本质上也就是一个事件流工具;
    • 由于轻量,所以灵活。
    • 不依赖react.memo,连equals计算消耗都没有;
    • 保持跟useState同样的颗粒度。当你不需要redux,mobx这些基于对象的状态流,不喜欢抽象什么领域,模型的情况下,使用改方案体验非常友好,使用体验也是非常接近于useState;
    • 性能卓越,非常容易做到「真正需要渲染的地方才渲染」的效果;
    • 非常容易集成到已有系统。就算接手的系统已经是一座「屎山」,使用react-use-shared-state进行改造也非常简单,只需要对跨组件的状态进行一一改造即可,还可以渐进式慢慢调整。对于不考虑后续可维护性和可读性的话,可以简单的将一个页面的跨组件状态都放在同一个地方,且这种行为不会影响性能。

    相关文章

      网友评论

          本文标题:react跨组件状态流:用事件流实现一个极其轻量高效的状态流工具

          本文链接:https://www.haomeiwen.com/subject/dwcxhjtx.html