Ant Design Pro V5精讲(基础篇七):useMemo和useCallback

1,363 阅读6分钟

应用场景

     有时候一些函数计算可能比较复杂,耗时比较长,但由于函数组件本身是没有mount和update之分,即只要调用setState,无论前后state的值是否不同,就会触发组件的重新渲染,第一次渲染后,如何把这个函数计算结果缓存下来,后面只要依赖项没有变化,它就不会创建函数新对象,而指向同一个引用,这时候就需要useMemo来实现,把这段代码用useMemo包裹起来。

     父组件更新,子组件也会自动的更新。我们希望是子组件的props和state没有变化时,即便父组件渲染,也不要渲染子组件。如何减少不必要的子组件重新渲染,这时候就需要useCallback。我们经常在父组件中定义一个回调函数传给子组件,如何避免这个回调函数造成子组件不必要的渲染呢。做法就是我们可以在父组件定义一段逻辑代码,用useCallback包裹起来,这个逻辑代码里可以写相关setState等引起父组件重新渲染的代码,然后把这个函数通过props传递给子组件,子组件不会因为函数内部执行的代码引起父组件渲染,而跟着渲染,这样有效避免了子组件不必要的重新渲染;只有函数依赖项变化了,函数重新创建了新对象,子组件才会重新渲染。(我的理解这个子组件要用到的父组件回调函数比较耗时,才需要考虑用useCallback去控制父子组件由于回调函数的传参造成的性能问题。)

 其实大部分情况由于react的效率比较高,暂时可以不考虑性能问题,有需要时,再去优化也不迟。

useMemo: 缓存函数的返回结果,定位在优化当前组件,即每次渲染时都进行高开销的计算的优化的策略。

useCallback: 用来缓存函数,常用在父子组件之间定义回调函数作为传参时,如何避免这个父组件中这个回调函数造成不必要的子组件渲染,其实缓存的是函数的内存引用地址,父组件定义好需要传给子组件的回调函数,这个回调函数内部的代码执行例如setState引起的父组件渲染,只要依赖项没有变化,子组件不会去重新渲染。即定位在父组件中这个回调函数当需要通过props给子组件调用时,这个回调函数本身进行高开销的计算的优化策略。

特性

  • 用法和useEffect差不多,但作用本质不同,useEffect是副作用函数,即它在组件渲染后去执行(它是可以控制的),而useMemo和useCallback其实无法控制要不要执行,每次渲染期间都会执行的函数(只要你在函数组件中定义并引用了它们,当setState执行时,组件就会重新渲染导致这个函数执行,如何控制依赖项不变化时,这个计算复杂的函数不执行,而是利用早期函数计算后的缓存结果,这就是useMemo和useCallback主要职能。

  • useMemo为了提升本组件的性能,避免组件渲染时不分青红皂白均去执行哪些计算耗时的函数(而这个函数大部分时间依赖项没有变化的情况不需要重新计算,依赖项(即第二个参数)要选择不经常变化的state)。

  • useCallback更多的时候用在子组件的渲染控制上,父组件渲染时,某些时候不需要去重新渲染子组件,这时候可以让子组件接收一个函数作为props(相当于告诉子组件,只有这个函数项依赖项发生变化时才需要渲染,否则父组件的渲染与子组件无关。

  • 共同点:useMemo和useCallback接受参数一样,第一个参数为回调函数,第二个参数为依赖项值组,只有依赖项变化了才需要创建一个新的函数对象,否则一直引用这个函数对象地址。

  • 不同点:useMemo定位本组件的优化,避免逻辑复杂的计算重复执行;useCallback定位如何有效避免子组件不必要的渲染。

  • 不管是useMemo还是useCallback的函数的内部执行,由于这个函数是在渲染期间执行,请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。

  • useMemo和useCallback的第二个参数,即依赖项值数组,这个要科学规划,尽量变化频率越小越好,不然缓存没有任何意义。

官方建议:先编写没有useMemo和useCallback情况下可以执行的代码,之后根据需要再去优化性能时,考虑他们。

语法

 有两个参数:

第一个参数是个函数,返回的对象指向同一个引用,不会创建新对象; 第二个参数是个数组,只有数组中的变量改变时,第一个参数的函数才会返回一个新的对象。 useMemo语法: 

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); useCallback语法:

const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );

用法示例

useMemo示例:某一个状态通过函数计算得来(这个函数比较耗时),缓存这个计算返回的数据,只有当依赖项数值组发生变化时,才需要重新执行这个函数,否则每次渲染时直接引用这个缓存的数据即可。

function Example() { const [count, setCount] = useState(1); const [val, setValue] = useState('');

const getNum = useMemo(() => {
    return Array.from({length: count * 100}, (v, i) => i).reduce((a, b) => a+b)
}, [count])

return <div>
    总和:{getNum()}
    <div>
        <button onClick={() => setCount(count + 1)}>+1</button>
        <input value={val} onChange={event => setValue(event.target.value)}/>
    </div>
</div>;

}

useCallback示例:父子组件的回调函数加上useCallback,能够避免子组件不必要的渲染,只有这个父组件的这个函数依赖项发生了变化(注:函数内部代码逻辑引起父组件的重新渲染与子组件无关,子组件只是对这个函数地址的一个引用,即函数实例与依赖项才是一对一关系)才需要重新渲染子组件,否则子组件不需要重新渲染,从而达到优化目的。

以下就是父组件中定义一个回调函数,用useCallback包裹,同时把这个回调函数通过props传给子组件,告诉子组件,只有这个回调函数的依赖项变化了才需要重新渲染,否则父组件的渲染与子组件无关。

父组件定义回调函数,传给子组件,子组件回调这个函数,把这个回调函数加上useCallback,避免父组件的回调函数内部setState造成父组件的渲染,让子组件不必要的渲染。我的理解时,只有这个子组件本身也比较耗时,才要必要这样优化,不然子组件的渲染速度不会造成性能问题。

function Parent() { const [count, setCount] = useState(1); const [val, setValue] = useState('');

const getNum = useCallback(() => {
    return Array.from({length: count * 100}, (v, i) => i).reduce((a, b) => a+b)
}, [count])

return <div>
    <Child getNum={getNum} />
    <div>
        <button onClick={() => setCount(count + 1)}>+1</button>
        <input value={val} onChange={event => setValue(event.target.value)}/>
    </div>
</div>;

}

const Child = React.memo(function ({ getNum }: any) { return总和:{getNum()} })