Permormance Hooks - useMemo & useCallback
如果你曾经在中型或大型项目中使用过React,你一定遇到过组件过度重新渲染的问题。在某些情况下,这可能会引发性能问题,比如应用程序顶端附近的小更改可能会引发组件树自顶向下的大面积重新渲染。
在本文中我们将更深入的了解React组件重新渲染的确切原因。我们还将介绍三个React用于优化渲染的工具
React.memo
useMemo
useCallback
React为什么会重新渲染
在介绍useMemo
和useCallback
之前我们要先了解React为什么会重新渲染。
在之前的案例中,我们已经了解了如何通过调用set函数(例如:setCount
)来触发重新渲染。React需要捕获给定count
时组件的快照。
事实上我们对于重新渲染已经有了一定的认识,本节课我们尝试去更加详细的解释这一概念。
我们先给出我们的结论
组件的每一次重新渲染都由state
改变触发
有的同学就有疑问了: 难道props
发生变化时组件不会重新渲染吗?
其实,在React中,当父组件重新渲染时,通常情况下其子组件也会跟着重新渲染。这是因为React遵循的是“单向数据流”的原则,即状态和属性从上层组件传递到下层组件。当父组件的状态或属性发生变化时,这些变化会通过props传递给子组件,从而触发子组件的重新渲染。
让我们通过一个案例来解释这一结论:
在这个案例中,我们有三个组件: App
-->ParentComponent
-->ChildComponent
。
当我们点击按钮时count
发生变化,触发ParentComponent
重新渲染,而ChildComponent
又是其子组件,因此也会被触发重新渲染。
下面是一个交互式图例,用于解释上面的流程, 点击Increase
按钮触发重新渲染。
假设现在在ChildComponent
旁添加一个纯文本的装饰组件Decoration
, 当count
发生变化时,Decoration
会重新渲染吗❓
通过控制台打印信息能发现,当ParentComponent
组件中的count
发生变化时, ChildComponent
和Decoration
都会跟着ParentComponent
一切重新渲染。
这是因为React遵循的是“单向数据流”的原则,即状态和属性从上层组件传递到下层组件。当组件重新渲染时,它会尝试重新渲染所有后代,无论这些是否通过props
传递了特定的状态变量
下面同样用一个交互式图例来解释上面的结论:
父组件重新渲染会沿着组件树重新渲染所有子组件,必然会影响系统整体性能,因为并不是所有子组件都需要重新渲染,起码在上面的案例中Decoration
组件就没必要重新渲染。下面我们将介绍几种方式来规避这些不必要的渲染。
React.memo
使用React.memo
可以帮助我们跳过不必要的渲染。其用法如下
function Decoration() {console.warn('Decoration re-render...')return <div>❓我会重新渲染吗?</div>}const PureDecoration = React.memo(Decoration)export default PureDecoration;
React.memo
包装Decoration
其实就是告诉React, 除非传递给我的props
发生变化,否则不要重新渲染我。
当ParentComponet
中的count
变更时,React会重新渲染ParentComponent
,同时沿着组件树重新渲染所有子组件,当碰到Decoration
时,发现其被React.memo
包裹,而Decoration
的props
又没有发生变化,所以就会Decoration
就会跳过此次渲染。
下面代码是更新后的版本,此时再点击increase
按钮,控制台就不会打印Decoration
组件中的日志,因为Decoration
没有被重新渲染。
同样的,让我们用一个交互式的图例来解释上面的流程。
总结
- 触发重新渲染的唯一方式就是调用set函数(例如:setCount)更新state。
- 当组件重新渲染时,组件树中它的子组件也会被重新渲染。
- 我们可以用
React.memo
包装我们的组件来优化它,跳过不必要的渲染。
useMemo
useMemo
的基本思想是,它允许我们在渲染之间“记住”计算出的值。
通过缓存昂贵的计算来帮助提高性能
跳过繁重的计算
假设我们有一个很占内存的数组initialItems
, 它的长度是3_000
, 我们把数组最后一个元素的isSelected
设置为true,意为选中。
现在,我们要从initialItems
中遍历出这个被选中的元素, 如果这个数组长度不是3_000
而是3_000_000
,可想而知,这是一个多么繁重的计算。
const selectedItems = items.find(item => item.isSelected)
当我们点击Increment
按钮时, count
发生改变,触发App
的重新渲染,进而导致上面的计算又进行了一次
注意👇下面案例中的日志打印信息,你会发现,每点击一次都会循环上述步骤。
好在useMemo
能够帮我解决这个问题,它能在渲染之间缓存计算值。
下面是使用useMemo
的版本,点击按钮会发现,控制台不会再打印信息了。
前后两个版本的差异点在于后者使用了useMemo
const selectedItems = items.find(item => {if(item.isSelected) {console.log('重新计算...')return true}return false})
const selectedItems = useMemo( () => items.find(item => {if(item.isSelected) {console.log('重新计算...')return true}return false}), [])
useMemo
有两个参数:
calculateValue
: 要缓存计算值的函数,它应该是一个没有任何参数的纯函数,并且可以返回任意类型。dependencies
: 所有在 calculateValue 函数中使用的响应式变量组成的数组。
首次渲染时, useMemo
返回 calculateValue
函数的返回结果。
在后续渲染中,如果dependencies
中的依赖项没有发生改变,它将返回上次缓存的只。否则将再次调用calculateValue
,并返回最新结果。
减少重新渲染次数
在下面的案例中,我创建了一个Boxes
组件,它用来展示一系列五颜六色的盒子,每个盒子中显示渲染时的时间。
在App
组件中,使用React.memo
包装Boxes
组件得到PureBoxes
, 同时将数组boxes
作为props
传递给PureBoxes
。
点击按钮,注意三个盒子中的时间是否发生变化?
不难发现,每点击一次,三个盒子中的时间就刷新一次。这意为着点击按钮时PureBoxes
组件会重新渲染。但PureBoxes
是使用React.memo
包装Boxes
后得到,
这意为着只要Boxes
组件接收到的props
不发生变化,Boxes
就不会跟着App
重新渲染。
但结果是Boxes
会跟着App
重新渲染,原因只能是<PureBoxes boxes={boxes} />
中的boxes
在App
渲染时发生了变化。
结合JavaScript知识点对象引用,我们可以知道,每当App
重新渲染时, boxes
都会被重新创建,虽然他们看起来相同,但引用却不相同。
const boxes = [{ background: '#FF0040'},{ background: '#09B43A'},{ background: '#409EFF'},]
换句话说,每次渲染时传递给Boxes
的boxes
属性都是全新的数组,因此Boxes
即使使用React.memo
包装,也得重新渲染。
为了避免这个问题,我们可以使用useMemo
来缓存boxes
这个数组。这样一来,boxes
数组只有在App
首次渲染时会被创建,App
重新渲染时不再创建,而是返回缓存的值。下面是更新后的代码,现在,再点击按钮,会发现三个盒子中的时间不会再跟着变化了。
useCallback
前面介绍了useMemo
, 那useCallback
又是干什么的呢?
简单点描述就是:useMemo
用于缓存对象或数组,而useCallback
用于缓存函数。
与数组和对象相同,函数也是通过引用来进行比较的:
const functionOne = function(){ return 5; };const functionTwo = function(){ return 5; };console.log(functionOne === functionTwo); // false ❌
这就意味着,当我们在组件内定义一个函数时,组件每次渲染都会重新生成一个全新的函数。
让我们看一个案例, 点击Click me!
,观察控制台输出:
不难发现,每当点击Click me!
时,ResetButton
都会重新渲染。
其原因不难理解,当点击Click me!
时count
改变,触发App
重新渲染,handleRest
函数被重新创建,然后handleReset
作为props
被传递给ResetButton
,props
的改变导致ResetButton
即使被React .memo
包装,也会重新渲染。
此时,我们可以使用useCallback
来缓存handleRest
函数。
现在,再点击Click me
, ResetButton
就不会再跟着重新渲染了。
useCallback
和useMemo
作用相同,不同的是useCallback
是专门缓存函数的。我们交给它一个函数,它会缓存这个函数,直到其依赖发生变动。
下面两个表达式效果是相同的:
// This:React.useCallback(function helloWorld(){}, []);// ...Is functionally equivalent to this:React.useMemo(() => function helloWorld(){}, []);
useCallback
本质上是useMemo
的一个语法糖。它的存在是为了让我们更直观的缓存函数。