灏天阁

在开发中减少 React 组件重渲染的方法

· Yin灏

现在大家基本上都在使用函数组件和 Hooks 来进行开发,相比于类组件而言,频繁的使用一些 hook 可能会导致函数组件频繁的更新,这也是函数组件的弊病之一,所以在项目中都会想方设法的减少组件更新的次数,从而达到性能优化的目的。看到几篇避免重渲染的文章,基本上都是在罗列一些方法,但是在开发中实战并不一定能用得上,甚至还可能达到相反的效果,今天就结合自己的实际体会来谈一下哪些方法比较可取。

useRef

先来看看 chatGPT 的对于它的解释

image.png

一般来讲,我们会使用 useRef存储一些访问一些 Dom 元素的属性,存储定时器 ID,保存一些数据,其他页面上如果触发的更新操作过于频繁的话,完全可以使用 useRef的返回值来代替,最后只需要使用一个 useState或者 useEffect来更新页面即可。

此外,useRef 还有其他作用,由于 useRef的返回值改变并不会触发页面的重渲染,可以使用它来保存一些不需要用户看到的数据或者状态,使用它可以有效地减少组件的重新执行次数,并且是立即生效的,上一行改变可以直接在下一行获取到最新的状态,并不像 useState 的返回值一样,需要在下次更新时才能拿到。所以考虑多在项目中使用 useRef是个不错的选择。

useSetState、useUpdateEffect、useImmer

前两个都是 ahooks 里的 hook:

useSetState:管理 object 类型 state 的 Hooks,用法与 class 组件的  this.setState  基本一致。

import React from "react";
import { useSetState } from "ahooks";

interface State {
  hello: string;
  [key: string]: any;
}
export default () => {
  const [state, setState] = useSetState<State>({
    hello: "",
  });

  return (
    <div>
      <pre>{JSON.stringify(state, null, 2)}</pre>
      <p>
        <button type="button" onClick={() => setState({ hello: "world" })}>
          set hello
        </button>
        <button
          type="button"
          onClick={() => setState({ foo: "bar" })}
          style={{ margin: "0 8px" }}
        >
          set foo
        </button>
      </p>
    </div>
  );
};

使用 useSetState可以显著减少组件更新次数,我们经常会有某一块内容需要多个状态控制的场景,比如打开一个弹框的时候,一般需要控制其打开与关闭的状态,以及存储的数据,甚至页面或按钮 loading 的状态,这样一个弹框就需要 3-4 个useState来进行控制,随着页面越来越复杂,后期 state 越多便越不好维护,我就曾见过一个关闭弹框的回调函数里,使用了 4 个 useState,可想而知,关闭一次弹框整个函数组件就需要执行 4 遍,这种级别的 state 写在 Redux 当中显然又不合适, 而使用 useSetState就可以轻松解决这个问题,可以将有关于弹框的状态写入一起进行维护,在关闭时一起将状态进行改变,优雅又具备更高的可读性和可维护性。

useUpdateEffectuseUpdateEffect  用法等同于  useEffect,但是会忽略首次执行,只在依赖更新时执行。这个就比较好理解了,在项目中经常会遇到各种需要忽略首次执行的情况,比如监听用户操作去请求数据,那么在首次渲染的时候,用户肯定是不能进行任何操作的,也就不需要执行,如果使用 useEffect的话,肯定不是最佳选择,此时useUpdateEffect就派上用场了。

useImmer:也是我经常使用的 hook 之一,对于复杂数据的精细化更新非常友好,个人建议数据只有一层可以使用 useSetState,层级较多可以使用 useImmer

useMemo 和 useCallback

这两个 hook,都可以固定其中的内容,使得在组件的一个更新中,保持变量或函数的地址不变,从而达到不需要重新生成的目的。

但是它真的有必要吗?我曾经见过项目中所有的函数都使用 useCallback进行包裹,但这样的做法,除了徒增代码量之外,还会导致一直异常的 bug 出现,比如在多人协作开发时,需要引用你的组件,但是,并不知道你的函数触发依赖了哪些变量,导致页面没有更新成功,而这种 bug 想要排查有时候也会比较困难。这样的做法使得开发效率变得低下。

因为 React 中,创建一个函数的代价是非常小的,而创建一个 hook 并且每次更新时都要计算他的依赖是否发生变化,也是一笔不小的开销,尤其是依赖较多,而所包裹函数却比较简单的时候,更是完全没有必要,即便是有子组件依赖于它。开发中完全不需要这种画蛇添足式的优化,那么在什么情况下可以使用它们呢?我们后来经过考虑觉得:只有在遇到性能问题的时候,可以考虑使用他们,否则,就是不需要。这也是我要写这篇文章的原因。

React.memo

它会在每次组件重渲染的时候对 oldPropsnewProps 进行浅比较,当 props 没发生改变的时候,组件就不会进行重渲染。 不过,当你使用这个 API 的时候要特别注意你的 props 中有没有对象或者函数,因为他们是引用数据类型,浅比较的时候实际比较的是他们的内存地址。此时就需要结合上面的useMemouseCallback来使用了。

useMemoizedFn

这也是 ahooks 中的一个,持久化 function 的 Hook,理论上,可以使用 useMemoizedFn 完全代替 useCallback。

在某些场景中,我们需要使用 useCallback 来记住一个函数,但是在第二个参数 deps 变化时,会重新生成函数,导致函数地址变化。

使用 useMemoizedFn,可以省略第二个参数 deps,同时保证函数地址永远不会变化。

const [state, setState] = useState("");
// func 地址永远不会变化
const func = useMemoizedFn(() => {
  console.log(state);
});

总结

React 开发中减少组件的重渲染是每一个开发者都在努力做的事,另外一些设计适应 react diff 的方法属于老生常谈,本篇只谈自己的心得体会,如果大家有更好的经验欢迎斧正。

- Book Lists -