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

2024-07-11  本文已影响0人  joyer_li

如果你也喜欢使用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。且在实际使用情况下,性能会损失更大,因为:

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

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 />
  </>);
});

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

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

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年多

优势

上一篇 下一篇

猜你喜欢

热点阅读