React子组件渲染与优化
1. 前言
本文主要记录个人关于子组件渲染时机,以及如何使用useCallback,useMemo,React.memo对其进行优化的一些思考与实践
2. 子组件渲染时机
首先“渲染”我这边先粗浅地将其定义为代码的执行,解析,浏览器的绘制。
那么子组件的渲染时机其实只有一个:当React检测到子组件的props发生改变之后,会触发子组件的重新渲染。
那么React如何判断props是否改变呢?实际上在第二次渲染之前,React会将当前子组件的props与上次渲染时的props进行一次浅比较,如果比较的结果相同,则不会渲染;但凡有任意一个props不同,则会触发组件的重新渲染。下面我将分类介绍一下不同的props类型下,React关于重新渲染时如何评判的。
2.1 props值为常量
下面的例子统一都使用下述表单弹窗组件作为子组件为例
interface CreateLineageModalProps {
visible: boolean;
onCancel?: () => void;
onCreate?: (values: LineageFormData) => void;
}
export const CreateLineageModal: React.FC<CreateLineageModalProps> = () => {
console.log('CreateLineageModal 渲染')
retrun <></>
}
当我们给visible的值从true手动改为false时,代码发生变更,父组件重新渲染;子组件发现props不同,因此也重新渲染
// 原始代码
<CreateLineageModal
visible={false}//
/>
// 手动修改代码
<CreateLineageModal
visible={true}//修改为true后 React浅比较时发现props变更,会触发重新渲染
/>
当visible的值保持为false,父组件因为其他原因重新渲染后,子组件是不会重新渲染的,因为基本数据类型是通过值传递 传递给子组件,子组件发现visible依旧是false,和上次相同,因此不会重新渲染
2.2 props值为state
前置知识:React中,传递给子组件的props,基本数据类型是值传递,对象 or 函数是引用传递,即在子组件中修改props中的对象是会导致父组件中的对象发生改变的(不过一般也不会这么做,最佳实践应该是受控组件,调用父组件传递的方法来修改父组件的状态),调用父组件的函数也是在父组件的上下文下进行调用
这是我们最经常使用的方式,例子如下
const [createModalVisible, setCreateModalVisible] = useState(false);
// 。。。
<CreateLineageModal
visible={createModalVisible}
/>
这种情况下,当createModalVisible
状态通过setState发生变更后,由于其是父组件的state,触发父组件的重新渲染;当代码执行到子组件时,子组件发现props变更,因此也重新渲染。
那么当父组件在其他情况下发生了重新渲染的情况(比如其他状态发生了改变),而createModalVisible
的值未改变,那么使用了该值的子组件是否会重新渲染呢,实践证明:其也会重新渲染。这是因为在父级代码重新执行时,会重新调用一遍useState来得到createModalVisible
的引用,这次的引用与之前的不同,因此其虽然实际值没变,但是子组件在浅比较props时,发现其存在不同,因此会触发重新渲染。
2.3 props值为函数
比如:
const [createModalVisible, setCreateModalVisible] = useState(false);‘
const handleCancel = () => setCreateModalVisible(false)
<CreateLineageModal
visible={false}
onCancel={handleCancel}
/>
那么当父组件发生重新渲染时,handleCancel会被重新赋值,赋予了一个新的引用,子组件发现props存在差异,因此会重新渲染
3. 如何优化
从上述情况我们可以知道,当父组件重新渲染时,即使子组件的props的值没有发生变化,只要其引用发生变化了,就会导致重新渲染,这显然是对性能的浪费。因此,我们需要想办法让传递给子组件的引用尽可能的稳定,即在符合需求的情况下保证其引用不变。如何实现?
3.1 针对函数 -> useCallback
useCallback接受一个回调函数和一个依赖项数组,在页面渲染时,只有当依赖项数组发生改变后,其才会返回新的回调函数的引用,否则返回的是上次渲染时回调函数的引用,这就保证了我们可以控制什么时候需要新的回调函数,保证了引用的稳定性
<CreateLineageModal
visible={false}
onCancel={useCallback(() => setCreateModalVisible(false), [])}
/>
3.2 针对计算对象 -> useMemo
useMemo第一个参数是回调函数,第二个参数是其依赖项数组,返回值是回调函数的返回值。和useCallback类似,仅当依赖项数组发生改变后,计算对象的引用才会改变,保证引用的稳定性。
3.3 针对state -> React.memo(子组件使用)
上述所说使用useState保存父组件的状态并将其传递给子组件时,即使实际值未发生改变,但是在父组件每一次重新渲染时,state的引用都会被更新,导致子组件的的重复渲染。
这一点可以通过用React.memo方法包裹子组件实现。一般情况下,React是对props的引用进行浅比较,而用memo包裹则是告诉react,你不仅仅需要检查引用,还需要检测实际值是否不一致,如果都不一致才是不同需要重新渲染,如果相同则不用。
代码示例如下:
<CreateLineageModal
visible={createModalVisibleMemo}
onCancel={useCallback(() => setCreateModalVisible(false), [])}
/>
export const CreateLineageModal: React.FC<CreateLineageModalProps> = React.memo(() => {
console.log('CreateLineageModal 渲染')
retrun <></>
})
如上可以实现当父组件的createModalVisibleMemo状态未发生改变而导致的重新渲染情况下,子组件不会发生无意义的重复渲染
4.最佳实践
综上所述,如果需要优化渲染此时,传递给子组件的函数可以使用useCallback包裹以保持其引用稳定性;计算对象可以通过useMemo保持稳定性;传递给子组件的状态则可以通过给子组件添加React.memo以强制其判断实际值的差异 来避免重复渲染。结合上述三者能够实现子组件渲染的优化
转载自:https://juejin.cn/post/7348654457428394035