likes
comments
collection
share

React Hook重复渲染问题处理:useMemo, memo, useCallback

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

用过react的同学都知道在v16.8版本引入了全新的api,叫做React Hooks,它的使用与以往基于class component的组件用法非常的不一样,不再是基于类,而是基于函数进行页面的渲染,我们把它又称为functional component。

其实react渲染是非常快的,我们在开发过程中并不一定要做性能上做特别的优化。但是,在最近的项目中,因为重复渲染的组件中有图片,在浏览器disable cache的时候,会出现图片的重新加载现象,这种在体验上是非常不好的。因此处理函数组件不必要的重复渲染的问题需要开始好好学习一下了!

本文主要介绍的就是useMemo, memo, useCallback的使用,这三个方法基本上可以处理大部分的重复渲染问题了。其中useMemo和useCallback的用法是差不多的,需要传入两个参数,需要缓存的方法以及需要在改变时更新方法的依赖值。下面我们来详细介绍一下。

const value = useMemo(fnM, [a]); 
const fnA = useCallback(fnB, [a]);

首先,我们先明确一点,为什么会存在重复渲染?

这是因为react hook使用的是函数组件,父组件的任何一次修改,都会导致子组件的函数执行,从而重新进行渲染。那么下面我们考虑三种情况,父组件没有props传入子组件props,,父组件传入子组件的props都是简单数据类型父组件传入子组件的props存在复杂数据类型。我们该如何处理呢?

1. 父组件没有props传入子组件props --- 使用React.memo即可

先简单介绍一下这个方法: React.memo 为高阶组件。它与React.PureComponent非常相似。默认只会对复杂类型对象做浅层比较,如果需要控制对比过程我们可以将比较函数作为第二个参数传入。React.memo(MyComp, areEqual)

是不是感觉很眼熟? 在shouldComponentUpdate中也有同样的比较函数,但是! ⚠️注意:在shouldComponentUpdate中方法的返回值是相反的!

我们继续: 在这种情况下,子组件的渲染不需要依赖父组件值的变化,使用React.memo包裹子组件,即缓存下子组件即可。这样,父组件中的数值如何变化,都会使用缓存下来的子组件。问题解决~

2. 父组件传入子组件的props都是简单数据类型 --- 使用React.memo即可

由于上面说的React.memo会默认进行浅层比较,使用React.memo包裹的子组件,会浅层比对传入的props是否有变化。简单数值类型,浅层对比即可判断是否发生了变化。如果传入的props没有变化,则使用缓存的子组件,如果传入的props发生变化,则组件会重新渲染。问题解决~

3. 父组件传入子组件的props存在复杂数据类型 --- 使用memo, useMemo, useCallback

我们通过props向子组件传值时,可能需要传入复杂类型如object,以及function类型的值。而memo子组件进行渲染比对时进行的是浅比较,即使我们传入相同的object或function,子组件也会认为传入参数存在修改,从而子组件重新进行渲染。这个时候仅仅使用memo包裹子组件应该没办法解决问题了,是时候用上我们的useCallback以及useMemo了。

我们先来说更经常使用一些的useCallback:

// 仅使用了memo,父组件传递给子组件的prop为方法,
// 该方法在子组件中被调用,改变了父组件的值,导致父组件重新渲染。
// 又由于父组件重新渲染,传给子组件的方法因其引用地址的不同会被认为有修改,导致子组件出现了不必要的重新渲染。
const Child = memo((props) => {
    console.log('我是一个子组件');
    return (
        <button onClick={props.handleClick}>改变父组件中的年龄</button>
    )
})

const Father = () => {
    console.log('我是一个父组件')
    const [age, setAge] = useState(0);
    return (
        <div>
            <span>`目前的count值为${age}`<span>
            <Child handleClick={() => setAge(age + 1)}/>
        </div>
        
    )
}

// 使用了useCallback优化了传递给子组件的函数,只初始化一次这个函数,下次不产生新的函数
const Father = () => {
    console.log('我是一个父组件')
    const [age, setAge] = useState(0);
    return (
        <div>
            <span>`目前的年龄为${age}`<span>
            <Child handleClick={useCallback(() => setAge(age + 1), [])}/>
        </div>
        
    )
}

⚠️注意:在useCallback的第二个参数处要传入正确的依赖值,否则useCallback就不会重新执行,其中使用的变量就还是之前的值。useMemo也是如此

我们在方法中可能会使用一些组件中但是存在方法外的参数,我们一定要将这些参数放入依赖项中,否则会一直使用缓存的方法,里面的外部参数也会一直是旧值。

现在,来一个useMemo的例子吧!

// 使用了memo以及useCallback,我们会发现更新属性profile为对象时,
// 尽管子组件只改变了age的值且子组件并没有使用age字段,子组件还是执行了。
// 这是因为在父组件更新其他状态的情况下,子组件的profile作为复杂类型,
// 仅仅进行浅比较会被认为存在修改,从而会一直重新渲染改变,导致子组件函数一直执行,这也是不必要的性能浪费。
// 解决这个问题,就需要在profile属性上使用useMemo了
const Child = memo((props) => {
    console.log('我是一个子组件');
    const {profile, handleClick} = props;
    return (
        <div>
           <div>{`父组件传来的用户信息:姓名${profile.name}, 性别${profile.gender}`}</div>
           <button onClick={handleClick}>改变父组件age</button>
        </div>
    )
})

const Father = () => {
    console.log('我是一个父组件')
    const [age, setAge] = useState(0);
    const [name, setName] = useState('张三男');
    const 
    return (
        <div>
            <span>`目前的年龄为${age}`<span>
            <Child
                profile={{name, gender: name.indexOf('男') > -1 ? '男' : '女' }}
                handleClick={useCallback(() => setAge(age + 1), [])}
            />
        </div>
        
    )
}

// 使用useMemo,返回一个和原对象一样的对象,第二个参数是依赖性,仅当name发生改变的时候,才产生一个新的对象,注意:依赖项千万要填写正确,否则name改变时,profile依旧使用旧值,就会产生错误

const Father = () => {
    console.log('我是一个父组件')
    const [age, setAge] = useState(0);
    const [name, setName] = useState('张三男');
    const 
    return (
        <div>
            <span>`目前的年龄为${age}`<span>
            <Child
                profile={useMemo(() => ({
                    name, 
                    gender: name.indexOf('男') > -1 ? '男' : '女' }), [name])
                }
                handleClick={useCallback(() => setAge(age + 1), [])}
            />
        </div>
        
    )
}

最后,我们来小小的总结一下:

  1. 子组件没有从父组件传入的props或者传入的props仅仅为简单数值类型使用memo即可。

⚠️注意:这个高阶组件在function component以及class component都可以使用哦

  1. 子组件有从父组件传来的方法时,在使用memo的同时,使用useCallback包裹该方法,传入方法需要更新的依赖值。
  2. 子组件有从父组件传来的对象和数组等值时,在使用memo的同时,使用useMemo以方法形式返回该对象,传入需要更新的依赖值。