likes
comments
collection
share

React子组件渲染与优化

作者站长头像
站长
· 阅读数 4

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
评论
请登录