如何优雅地处理使用 React Context 导致的不必要渲染问题?

719 阅读2分钟

此处引用知乎答主 @buhi 的一段话:

一句话,react context是给你注入服务的,不是让你注入数据的,如果要注入具有数据的服务那你就注入个类似EventEmitter的东西,例如rxjs observable 其实,我们可以把目光放到隔壁 UI 构建方式和 React 差不多的 Flutter 身上,它提供了一些新的思路,我们来看看 Flutter 下是怎么实现遥远组件间的状态共享的:

首先,Flutter 和 React 一样有类似 Context/Provider 的组件和接口:InheritedWidget,都能在组件树上很方便的共享数据,而无需通过 props 一层层往下传。

其次,Flutter 大部分官方组件都提供了一个叫做 Widget Controller 的东西,它的作用其实就有点类似上面 @buhi 提到 EventEmitter(在 Flutter 中的实现被叫做 ChangeNotifier),它使用了 观察者模式 来分发数据/事件给监听的组件,从而实现 在组件以外对组件内的状态进行控制

在 React 下还原一下 Controller 的大概实现(部分为伪代码):

class Controller {
  private _listeners = new Array<(title: string) => void>();

  public addListener(listener: (title: string) => void) {
    this._listeners.push(listener);
  }

  public removeListener(listener: (title: string) => void) {
    this._listeners.remove(listener);
  }

  public updateTitle(title: string): void {
    for (const listener of this._listeners) {
      listener(title);
    }
  }
}

const Component: React.FC<{ controller: Controller }> = ({ controller }) => {
  const [title, setTitle] = useState<string>();

  const onChange = useCallback((newTitle: string) => {
    setTitle(newTitle);
  }, [setTitle]);

  useEffect(() => {
    controller.addListener(onChange);
    return () => {
      controller.removeListener(onChange);
    };
  }, [controller, onChange]);

  return <>{title}</>;
};

可以看出:

  1. 核心是观察者模式,Controller 是发布者,Component 是观察者
  2. 可以在组件外部创建 Controller,并传递进组件内对组件进行 setState 控制
  3. 组件内通过 useEffect hook 来在确保仅在挂载周期内对 Controller 进行监听

当我们有了 Controller 这种能在组件以外对组件内的状态进行控制的「代理」,那我们就可以创建一个 Controller 实例并通过 Context 把它分享出去,从而实现遥远组件之间的相互控制:当我们想要改变受控组件的 state 时,不需要更新 Controller 的实例(因为会触发整个 Provider 树的更新),而只需调用 Controller 实例内的 updateTitle 方法即可。

当然,上面只是一个最小的例子,如果你对这种状态控制/分享方法感兴趣的话,可以看看鄙人封装的 hook(点击图片跳转):

nekocode/use-shared-state

目前已在多个 2C/2B 项目中使用,效果十分好。羽量级代码但却性能强悍,支撑起了大部分需要状态控制/分享的场景(PS:仓库已被知名写作 App 「Typora」的作者 Star 了~