详解React性能优化手段之useMemo和useCallback

1,871 阅读4分钟

概述

useMemo和useCallback都是React进行性能优化的手段,

这两个hooks的作用就是进行数据缓存,避免页面重新渲染时不必要的参数重复更新

它们的适用场景就是对于只需要单次进行复杂计算的内容进行数据存储,从而减少页面性能开销。

这里先放一张旧版的react生命周期图,便于后续讲解useEffect、useMemo、useCallback对应的钩子函数执行时期时,有更清晰的认识:

useMemo & useCallback & useEffect执行时期

那么useMemo和useCallback的执行时期是什么呢?

对应到react的生命周期来说,这两个hooks对应的钩子函数都是shouldComponentUpdate

由图可得,shouldComponentUpdate这个钩子函数的执行时期是在render()页面渲染之前的;

而useEffect对应的钩子函数根据useEffect的参数形式分别有componentDidMount,componentDidUpdatecomponentWillUnmount三种,

这三个钩子函数都是在render()页面渲染之后执行的。

由此我们可以得出的结论就是:useMemo和useCallback两个hooks的出现,针对的痛点就是useEffect的执行时期是在页面渲染之后执行的,因此即使是不需要进行重复更新的参数,也会在页面重新渲染时再次更新,这是非常不友好的。当然,我们可以通过useEffect的第二个参数来阻止第一个参数的执行,但是它的执行时期仍然是页面渲染之后。

但useMemo和useCallback这两个hooks也有自己的缺点,那就是它们为了进行数据存储需要一直维护第二个参数,也就是依赖数组。这个点我们在本文的最后再进一步讨论。

useMemo & useCallback 的区别

接下来,我们来谈谈useMemo和useCallback这两个hooks有什么区别,毕竟它们俩对应的钩子函数都是shouldComponentUpdate,干的事儿也是进行数据缓存,那它们的区别是什么呢?

首先说useMemo,我们来看一段这个hooks的代码:

import {useEffect, useMemo, useState} from "react";

export const UseMemoExample = () => {
    const [count, setCount] = useState(0);
    // 这里useMemo的依赖参数是count,因此count每次变化的时候,useMemo就会跟着变,那么如果第二个参数是一个固定值,或者其他变量的话,useMemo就不会在每次重新render的时候执行了,同理与useCallback
    const initCount = useMemo(() => {
        console.log('执行useMemo');
        return count + 1;
    }, [count]);


    useEffect(() => {
        console.log('执行useEffect');
    },[count]);

    return (
        <div>
            <p>count {count}</p>
            <p>initCount {initCount}</p>
            <button onClick={() => setCount(count + 1)}>click</button>
        </div>
    )
}

感兴趣的同学,可以放进react里运行一下看看页面输出&控制台输出。

useMemo()这个函数包含两个参数,第一个参数又称为工厂函数,这个工厂函数必须要有return,因此initCount其实被赋予的是一个表达式的值。第二个参数称为依赖数组,第二个参数存在的意义,就是表明useMemo的执行时期是根据第二个参数数组里值的变化来执行的。

对于useMemo的第一个参数,当我们的工厂函数只有一行return结果时,可以简写为如下形式:

const initCount = useMemo(() => count + 1, [count]);

这是匿名函数的一种简写。

接下来说说useCallback,我们来看一段这个hooks的代码:

import {useCallback, useState} from "react";

export const UseCallbackExample = () => {
    const [count, setCount] = useState(0);
    const logCount = useCallback(() => {
        console.log(count);
    }, [count]);

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                click me
            </button>
            <button onClick={logCount}>log count</button>
        </div>
    );
}

感兴趣的同学,可以放进react里运行一下看看页面输出&控制台输出。

useCallback()这个函数也包含两个参数,第一个参数又称为缓存函数,乍一看,貌似和useMemo没有什么区别,但其实useCallback第一个参数对应的函数是不需要返回值滴~,因此这里的logCount其实被赋予的是函数。第二个参数称为依赖数组,第二个参数存在的意义,就是表明useCallback的执行时期是根据第二个参数数组里值的变化来执行的。

由上,细心的同学可能已经得出了结论:
(1) useMemo返回的是一个变量的值,useCallback返回的是一个函数。对应到function组件最后要return的html代码部分,useMemo就是作为一个值来使用的,而useCallback则是被绑定的onClick,作为要执行的函数。这就是它俩的本质区别。
(2) useCallback(fn, deps) 等价于 useMemo(() => fn, deps).

总结

最后,我们来对useMemo和useCallback这两个hooks做一个总结

useCallback和useMemo主要用来做数据缓存,它们的更新依赖于第二个参数是否发生了变化。

       因此当我们的render重复渲染时,只要我们的useMemo和useCallback的第二个参数值没有发生变化的话,useMemo和useCallback是不会去执行第一个参数里的函数的。因此,我们就可以把一些需要进行复杂运算或者希望进行数据缓存给放在useMemo和useCallback里面。

       这是十分有用的,因为useSate中元素的改变会使得整个组件都重新进行render,那么在render的过程中,除了被useMemo和useCallback包裹的,其余的内容都会被重新执行一遍。对于只需要进行一次的高性能运算来说,这是十分消耗内存但没必要的事情。因此就需要useCallback和useMemo来帮我们做下数据缓存。

       不过useMemo和useCallback也有缺点,那就是需要一直维护第二个参数数组,也就是依赖数组。并且保留旧值的副本,以便于当子组件的值没有更新时直接返回这个旧值的副本。

       除此之外,因为useMemo和useCallback是在页面渲染前执行的hooks,因此这两个hooks中第一个参数执行的内容应该是纯函数,不会影响页面元素的更新,否则我们的页面就会陷入无限循环了呀~

以上内容,均为本人个人思考总结,如有不对之处,欢迎交流。转载请注明出处!