使用inject-context解决react中context的性能问题

context 的性能问题

React 在 render 流程中通过全等判断props是否改变,如果判断为有改变组件将会重新渲染,并且会影响到其子节点,这就是 React 渲染的"传染性"。有两种方式解决这个问题:

  1. 根据"变与不变分离"的原则优化组件结构
  2. 使用memo

具体操作可浏览另一篇文章 react性能优化|bailout策略

使用其一或结合使用,通常能解决问题。

React 在 render 流程中通过 Object.is 判断 context 的 value 是否改变,这也会造成类似上面所说的问题,具体来说,当一个组件通过 useContext 读取了一个来自 context 的状态,就意味着它一直会跟着 context 的更新而渲染,即使它订阅的状态没有真的改变,因为 Object.is 不能深入到对象内部比较。 并且,我们并不能像 props 一样通过调整组件结构来解决问题,也没有性能优化API可以使用。

翻译:从 provider 接收到不同的 value 开始,React 会自动重新渲染所有使用特定 context 的所有子级。 先前的只和新的值通过 Object.is 进行比较。 使用 memo 跳过重新渲染并不会阻止子级接收新的 context 值。

来看一个例子:

tsx 复制代码
const Context = createContext<ContextValue>({});

function App() {
  const context = useContext(Context);

  return (
    <>
      <button
        onClick={() => {
          context.setContext?.(prev => ({ ...prev, count: prev.count! + 1 }));
        }}
      >
        改变context的count
      </button>
      <div className="wrapper">
        <Child1></Child1>
        <Child2></Child2>
      </div>
    </>
  );
}

const ContextApp = () => {
  const [state, setState] = useState<ContextValue>({
    count: 0,
    someString: '一个没有被更新的字符串',
  });
  return (
    <Context.Provider value={{ ...state, setContext: setState }}>
      <App />
    </Context.Provider>
  );
};

这是一个使用 context 的简单例子,App 内会更新 context 的 count 值。 其中 Child1 和 Child2 组件使用了来自 context 的 value:

tsx 复制代码
function Child1() {
  const ref = useRef<HTMLDivElement>(null);
  useHighlight(ref);
  const { count } = useContext(Context);
  return (
    <div ref={ref} className="child">
      <div>使用了context的count:</div>
      <div>{count}</div>
    </div>
  );
}
function Child2() {
  const ref = useRef<HTMLDivElement>(null);
  useHighlight(ref);
  const { someString } = useContext(Context);
  return (
    <div ref={ref} className="child">
      <div>使用了context的someString:</div>
      <div>{someString}</div>
    </div>
  );
}

其中 useHighlight 提供了组件渲染时 dom 高亮的功能。

Child2 并没有使用 count 也重渲染了。

解决方法 inject-context

针对这个问题,很容易能想到通过一个 selector 机制来解决,只不过这个 selector 不能写在使用 context 的组件内部,因为要选择状态必然要先通过 useContext 拿到所有状态,然而一旦使用了就意味着已经订阅了context,选择也就没有了意义。

可以做一个上层高阶组件,在这里做 selector 操作,把选择的值通过 props 传到目标组件,结合 memo 就可以解决问题

我把这段逻辑封装起来发布了一个 npm 包,名为 inject-context,意思是给组件注入 context 。 使用方式:

tsx 复制代码
// 单个context
const Componnet = injectContext({
  context: Context,
  selector: (s) => ({....})
})(你的组件)

// 多个context
const Componnet = injectContext([
  {
    context: Context1,
    selector: (s) => ({....})
  },
  {
    context: Context2,
    selector: (s) => ({....})
  }
])(你的组件)

可见 inject-context 支持接收多个 context,实现方式是在内部循环调用了 useContext,这看起来违背了 hooks 的使用规则:

钩子比其他功能更具限制性。您只能在组件(或其他 Hooks)的顶部调用 Hooks。 如果您想useState在条件或循环中使用,请提取一个新组件并将其放在那里。

hooks 有这个限制主要是因为函数组件的 fiber 节点中有一个存放所有使用的 hook 的链表,这些 hook 在函数组件每次运行时都要按顺序执行并且数量不可以改变,如果在条件或循环中使用可能造成数量或顺序改变从而造成错误。

但这里在 inject-context 定义多个 context 实际上是定义死的,并不会改变数量和顺序,也就不会造成问题。

使用 inject-context 重写上面的例子,增加两个 Child 组件:

tsx 复制代码
const InjectContextChild = injectContext({
 context: Context,
 selector: s => ({ count: s.count }),
})(function (props) {
  const ref = useRef<HTMLDivElement>(null);
  useHighlight(ref);
  return (
    <div ref={ref} className="child">
      <div>使用了context的count:</div>
      <div>{props.count}</div>
    </div>
  );
});

const InjectContextChild2 = injectContext({
  context: Context,
  selector: s => ({ someString: s.someString }),
})(function (props) {
  const ref = useRef<HTMLDivElement>(null);
  useHighlight(ref);
  return (
    <div ref={ref} className="child">
      <div>使用了context的someString:</div>
      <div>{props.someString}</div>
    </div>
  );
});

再看效果:

深度比较

memo 默认的比较函数只能对比第一层属性,inject-context 扩展了这个默认函数,支持选择性的深度比较,如果选择的某个 context 状态想要深度比较的话,可以通过如下方式:

tsx 复制代码
const InjectContextChild2 = injectContext<{
  someString: ContextValue['someString'];
}>({
  context: Context,
  selector: s => ({ obj: s.obj }),
  // selector 返回的对象的键名
  deepKeys: ['obj']
})(....);

指定deepKeys来告诉 inject-context 你需要深度比较的属性,值就是 selector 返回的对象的键名。

⚠️注意,不要滥用这个特性,深度比较带来的开销可能大于重渲染的开销。

TS 支持

props 类型提示

count 实际被注入到 props 里了,但是并没有良好的类型提示,可以通过给 inject-context 传泛型解决,它能接收两个泛型,第一个是注入的值的类型,第二个是原本 props 的类型。

selector 参数类型

默认的 selector 参数是 any,没有良好的类型提示

可以通过 inject-context 提供的 defineSelector 来提供类型提示。

写在最后

github链接: inject-context

相关推荐
m0_748254881 分钟前
vue+elementui实现下拉表格多选+搜索+分页+回显+全选2.0
前端·vue.js·elementui
星就前端叭1 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
m0_748234521 小时前
前端Vue3字体优化三部曲(webFont、font-spider、spa-font-spider-webpack-plugin)
前端·webpack·node.js
Web阿成1 小时前
3.学习webpack配置 尝试打包ts文件
前端·学习·webpack·typescript
jwensh2 小时前
【Jenkins】Declarative和Scripted两种脚本模式有什么具体的区别
运维·前端·jenkins
关你西红柿子2 小时前
小程序app封装公用顶部筛选区uv-drop-down
前端·javascript·vue.js·小程序·uv
益达是我2 小时前
【Chrome】浏览器提示警告Chrome is moving towards a new experience
前端·chrome
济南小草根2 小时前
把一个Vue项目的页面打包后再另一个项目中使用
前端·javascript·vue.js
聪小陈2 小时前
圣诞节:记一次掘友让我感动的时刻
前端·程序员
LUwantAC2 小时前
CSS(一):选择器
前端·css