盘点React常用Hooks(3)—— useMemo、useCallback | 青训营

120 阅读6分钟

React Hook

上篇文章中我们介绍了 useRef ,本次的文章我们将继续介绍 React 中常见的两个Hook——useMemouseCallback。在复杂的 React 应用中,性能问题常常是挥之不去的影响因素。频繁的渲染和计算可能导致应用变得缓慢,为了解决这些问题,React 提供了 useMemouseCallback 这两个钩子函数,用于优化渲染过程中的计算和函数传递。

1. useMemo - 优化计算

函数定义

useMemo 用于优化渲染过程中的计算操作。通过使用 useMemo对某个值进行缓存和记忆,以避免在每次渲染时重复计算。通过使用 useMemo,我们可以在依赖项不变的情况下重复使用计算结果,从而提高应用的性能。

useMemo 的基本语法如下:

const memoizedValue = useMemo(() => computeValue(dependencies), dependencies);
  • computeValue:是一个计算函数,用于计算并返回值。
  • dependencies:是一个数组,表示该计算函数所依赖的值。只有在 dependencies 中的值发生变化时,useMemo 才会重新计算并返回新的值。

使用示例

当我们在函数组件中需要渲染复杂运算值的时候,我们经常通过函数调用来返回计算结果,但是这个时候会出现一些问题:

import React, { useState } from 'react';

function MemoExample() {
    const [a, setA] = useState(5);
    const [b, setB] = useState(10);
    const [c, setC] = useState(15);
  
    useEffect(()=>{  
        console.log('render')  
    })

    const sumOfSquares = () => {
        console.log('Calculating sum of squares');
        return a * a + b * b;
    };

    return (
        <div>
            <p>Sum of squares: {sumOfSquares()}</p>
            <button onClick={() => setA(a + 1)}>Increment A</button>
            <button onClick={() => setB(b + 1)}>Increment B</button>
            <button onClick={() => setC(c + 1)}>Increment C</button>
        </div>
    );
}

在上述的代码中函数 sumOfSquares 与变量 c 并没有关联,但是从下方的动图可以看到,在我们改变 c 的值时,组件重新渲染,因此也触发了 sumOfSquares ,造成不必要的渲染。

usememo1.gif

下面演示如何通过 useMemo 进行优化计算:

import React, { useState, useMemo } from 'react';

function MemoExample() {
    const [a, setA] = useState(5);
    const [b, setB] = useState(10);
    const [c, setC] = useState(15);
  
    useEffect(()=>{  
        console.log('render')  
    })

    const sumOfSquares = useMemo(() => {
        console.log('Calculating sum of squares');
        return a * a + b * b;
    }, [a, b]);

    return (
        <div>
            <p>Sum of squares: {sumOfSquares}</p>
            <button onClick={() => setA(a + 1)}>Increment A</button>
            <button onClick={() => setB(b + 1)}>Increment B</button>
            <button onClick={() => setC(c + 1)}>Increment C</button>
       </div>
    );
}

通过使用 useMemo 绑定依赖项,我们在组件重新渲染时阻止了不必要的性能损耗。

usememo2.gif

在这个例子中,只有当 ab 发生变化时,才会重新计算平方和。如果 ab 的值没有改变,sumOfSquares 将会使用之前的缓存值,避免了重复计算。

2. useCallback - 优化函数传递

函数定义

useCallback 用于优化函数传递过程中的性能问题,使用此钩子函数可以返回一个记忆化的回调函数,也就是确保在依赖项不变的情况下,该回调函数保持不变。通过使用 useCallback,我们可以避免在每次渲染时都重新创建回调函数,从而减少不必要的子组件重新渲染。

useCallbackuseMemo的语法基本相似:

const memoizedCallback = useCallback(callbackFunction, dependencies);
  • callbackFunction:是要进行优化的回调函数。
  • dependencies:是一个数组,表示该回调函数所依赖的值。只有在 dependencies 中的值发生变化时,useCallback 才会重新创建回调函数。

使用示例

通常情况下,如果父组件更新了,子组件也会执行更新;但是有的场景下,更新是没有必要的,这个时候就我们可以使用 useCallback 来返回函数,再传递给子组件。

当不使用 useCallback 优化回调函数传递时,可能会导致子组件的不必要重复渲染:

import React, { useState } from 'react';

// 子组件
function ChildComponent({ onClick }) {
    console.log('ChildComponent re-rendered');
    return <button onClick={onClick}>Click me</button>;
}

// 父组件
function ParentComponent() {
    const [a, setA] = useState(0);
    const [b, setB] = useState(0);

    // 不使用 useCallback,每次渲染都会创建新的回调函数
    const handleClick = () => {
        setCount(count + 1);
    };

    return (
        <div>
            <p>Count: {count}</p>
            <ChildComponent onClick={handleClick} />
            <button onClick={()=>setB(b+1)}>Click parent</button>
        </div>
    );
}

export default ParentComponent;

在上述的代码中无论点击哪个按钮都会触发父组件的渲染,而每次渲染时都会创建函数 handleClick ,由于子组件每次接收到不同的函数引用触发组件更新,造成了不必要的重新渲染。

usecallback1.gif

而这个时候我们就可以通过使用 useCallback 对组件进行优化。它可以帮助我们避免不必要的子组件重新渲染,提高应用的性能。

import React, { useState, useCallback } from 'react';

// 使用React.memo高阶组件包裹子组件
const ChildComponent = React.memo(({ onClick }) => {  
    console.log('ChildComponent re-rendered');  
    return <button onClick={onClick}>Click child</button>;  
});

// 父组件
function ParentComponent() {
    const [a, setA] = useState(0);  
    const [b, setB] = useState(0);  

    const handleClick = useCallback(() => {  
        setA(a + 1);  
    }, [a]);  

    return (  
        <div>  
            <p>Count: {a}</p>  
            <ChildComponent onClick={handleClick} />  
            <button onClick={()=>setB(b+1)}>Click parent</button>  
        </div>  
    );  
}

export default ParentComponent;

通过为useCallback绑定依赖项并使用React.memo包裹子组件切记,我们避免了子组件不必要的重复渲染。

React.memo是高阶组件,如果您的组件在给定相同的 props 的情况下呈现相同的结果,则在某些情况下,您可以通过记住结果将其包装在调用中以提高性能。这意味着 React 将跳过渲染组件,并重用上次渲染的结果。

usecallback2.gif

在这个示例中,由于父组件中的 handleClick 回调函数通过 useCallback 进行了优化,并且子组件使用 React.memo 进行了包装,所以子组件只会在传递给它的属性(包括回调函数)发生变化时才会重新渲染。因此,即使父组件重新渲染,子组件也不会因为优化后的回调函数没有发生变化而重新创建和重新渲染。

应用场景

上文介绍了 useMemouseCallback 的基本语法并举了两个简单的例子,下面再推荐一下这两个钩子函数的主要应用场景,希望大家在需要的时候可以想起它们。

  • useMemo 的应用场景:

    • 昂贵的计算: 当需要进行复杂或昂贵的计算以生成某个值时,可以使用 useMemo 来缓存计算结果,避免在每次渲染时重新计算。这在避免性能问题的同时保持代码清晰性很有帮助。
    • 渲染性能优化: 当一个组件的渲染依赖于一些值,但这些值在渲染周期内不会改变时,可以使用 useMemo 来缓存组件的渲染结果,避免不必要的渲染。
  • useCallback 的应用场景:

    • 传递给子组件的回调函数: 当将回调函数传递给子组件时,使用 useCallback 可以确保回调函数只在依赖项变化时重新创建。这可以避免不必要的子组件重新渲染。
    • 优化 useEffect 的依赖项: 当在 useEffect 中使用回调函数时,使用 useCallback 可以避免在依赖项变化时触发不必要的副作用。
    • 防止冗余更新: 当使用类似 setState 的更新函数时,使用 useCallback 可以防止不必要的组件重新渲染,从而提高性能。

总结

本次的文章简单地举例并介绍了 useMemouseCallback。通过使用这两个钩子,我们可以优化 React 应用的性能,避免不必要的计算和函数重新创建。但同时滥用也可能导致代码变得更加复杂,在实际开发中,对于一些简单的场景,可能不需要使用这些钩子函数,而对于复杂的情况,使用它们可以有效地优化性能。

[第一篇](盘点React常用Hooks(1)—— useState、useEffect | 青训营 - 掘金 (juejin.cn))

[第二篇](盘点React常用Hooks(2)—— useRef | 青训营 - 掘金 (juejin.cn))