用 React 写函数组件,如何避免重复渲染?

  • Post author:
  • Post category:小程序

一句话概括:memo、useMemo、useCallBack主要用于避免React Hooks中的重复渲染,作为性能优化的一种手段,三者需要组合并结合场景使用。

在使用memo、useCallBack、useMemo前,我们需要先了解React组件的更新机制。

React组件的更新机制

React组件在默认情况下,父组件或兄弟组件触发更新后,会按照父组件、子组件的顺序重新渲染,并且即使子组件本身没有发生任何变化,也会重复触发更新。

举一个简单的例子:

目前我们有A、B、C三个组件。A组件中包含B、C两个组件,即A组件为B、C组件的父组件,B、C组件互为兄弟组件。

样式如下:

很"简约",因为样式并不是我们这次关注的重点

相关代码如下:

A组件:

A组件包含一个state以及B、C两个子组件

B组件:

B组件需要传入一个handleClick回调函数,每次button被点击时触发调用

C组件:

C组件很简单,仅在触发更新时,在控制台打印相关字符串

整体层次结构我们可以用下图表示:

整个代码执行后,控制台打印(VM6的信息重复,可以忽略)如下:

A、B、C组件在第一次加载后,均在控制台打印了相关字符串

预期的结果

当我们点击按钮后,预期的结果是什么?三个组件中哪些组件会更新?

界面上,点击按钮后,count从0变成了1

我们观察控制台,发现A、B、C三个组件的function全部被重新执行了,即使在这次更新中,B、C组件的内容完全没有发生变化:

怎样避免重新渲染?

参考上面的例子,如果在实际项目中,B、C组件包含高开销的计算,A组件的更新会导致B、C组件不断地重复渲染,这样会对性能产生比较大的影响。

那么有没有什么办法可以避免这种情况下的重复渲染,从而达到性能优化的目的?这个就是我们使用memo、useCallBack、useMemo的原因。

先说memo:

如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在React.memo中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。React memo官方文档

与Class Component中的PureComponent类似,在React Hooks中,可以通过memo来避免组件的重复渲染。

以刚才的代码为例,我们改写C组件,将它用memo包裹起来:

memo是一个高阶组件。它的功能我们可以这么理解:

  • 被调用时返回传入的组件
  • 每次传入的组件要执行更新时,组件的新props和之前的老props会进行一次浅比较:
    • 结果相等,不触发更新
    • 结果不相等,触发更新,重新渲染

这样改写后,我们重新运行代码,点击按钮,查看控制台的打印结果:

按钮点击后,只有A、B组件重新进行了渲染

C组件没有包含任何props,每次浅比较的结果都相等。因此,在被memo包裹后,无论父组件是否更新,C组件始终都不会触发更新,每次都会返回第一次渲染的结果。

同样的,我们再用memo将B组件包裹起来:

这时我们再次重新运行代码,观察控制台:

一个奇怪的现象:B组件依然更新了

B组件已经被memo包裹了,为什么还是会触发更新?

我们刚才有提到,memo在判断props是否变化时,是进行的浅比较。而我们再次观察A组件的代码:

由于我们点击了按钮,触发了state的更新,A组件的function被重新执行。

也就是说,在每次点击按钮触发更新后,handleClick函数都会被重新定义一遍,并作为一个新的变量传递给B组件。这时memo内部认为props发生了变化,因此重新渲染了B组件。如何避免A组件每次重新渲染时,都会生成新的handleClick?

useMemo正用于解决这样的问题:

把“创建”函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。React useMemo文档

useMemo的用法和useEffect的用法类似,它需要接收两个参数。

  1. 第一个参数要求为一个function,function需要return一个变量
  2. 第二个参数为一个数组,和useEffect类似,作为第一个参数的依赖项数组

它的功能可以理解为:

在检测到依赖项数组中的变量发生变化时,重新执行传入的function,并返回传入function执行后的结果。

我们将A组件中的handleClick用useMemo包裹起来:

由于handleClick方法内部本身没有依赖任何变量,因此它的依赖数组项为空。

这样做可以保证无论A组件是否更新,handleClick变量始终都会是同一个。

这时我们再次执行代码,点击按钮:

结果符合我们的预期,这次只有A组件更新了

至此,我们成功通过使用memo、useMemo的组合达到了我们最终的目标。

useCallBack:useMemo的语法糖

把内联回调函数及依赖项数组作为参数传入useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如shouldComponentUpdate)的子组件时,它将非常有用。React useCallBack官方文档

useCallBack和useMemo唯一的区别是:useMemo返回的是传入的回调函数的执行结果,useCallBack返回的是传入的回调函数。本质上就是useMemo的语法糖。

因此上面的代码我们完全可以这样改写:

最终验证后,我们发现输出的结果和之前也是保持一致的:

注意:不要滥用useMemo、useCallBack、

使用useMemo、useCallBack时,本身会产生额外的开销,并且这两个方法必须和memo搭配使用,否则很可能会变成负优化。

因此,在实际项目中,需要结合实际场景,评估重复渲染和创建useCallBack/useCallBack的开销来判断到底用不用useCallBack、useMemo。

总结

  1. memo与class组件中的pureComponent类似,通过props浅比较来判断组件需不需要重新渲染
  2. useMemo、useCallBack通过浅比较依赖数组项中的变量,判断对应变量/function需不需要重新生成
  3. useMemo、useCallBack不要滥用,需要结合具体场景

发布于 2022-07-24 21:28・IP 属地中国香港