使用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

相关推荐
Myli_ing22 分钟前
HTML的自动定义倒计时,这个配色存一下
前端·javascript·html
dr李四维39 分钟前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
雯0609~1 小时前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
℘团子এ1 小时前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z1 小时前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
彭世瑜2 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund4042 小时前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish2 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue
小五Five2 小时前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序2 小时前
vue3 封装request请求
java·前端·typescript·vue