useMemo到底有没有用?我差点被面试官问懵了!

82 阅读5分钟

useMemo到底有没有用?我差点被面试官问懵了!

在开发 React 应用时,性能优化是一个不可忽视的环节。React 提供了多个工具来帮助我们避免不必要的渲染和计算,其中 React.memouseMemouseCallback 是三个非常关键的优化手段。搞懂它们三非常有必要!!!

一、React.memo

  • 当有两个组件,父组件App,子组件Button。父组件中有两个状态count、buttonCount,子组件依赖于buttonCount,父组件依赖于count,每次父组件 App状态count发生更新的时候,子组件 Button 也会跟着重新渲染,但它的内容根本没有变化(它依赖于buttonCount)。这样会造成不必要的渲染,影响性能。使用React.memo便可以解决这个问题。

    原始父子组件传参示例:

    function App() {
      console.log("App组件渲染");
      const [count, setCount] = useState(0);
      const [buttonCount, setButtonCount] = useState(0);
      return (
        <>
          <button onClick={() => setCount(count + 1)}>App组件计数 {count}</button>
          <Button buttonCount={buttonCount} />
        </>
      );
    }
    
    const Button = ({ buttonCount }) => {
      console.log("Button组件渲染");
      return (
        <>
          <button>Button组件计数 {buttonCount}</button>
        </>
      );
    };
    

    1.webp

  • 什么是 React.memo?

    React.memo 是一个高阶组件(HOC),用于避免函数组件在 props 没有变化时的重复渲染。它通过浅比较 props 来决定是否跳过本次渲染。

    使用 React.memo 包裹你想要优化的函数式组件即可实现

    const MyComponent = React.memo(function MyComponent(props) {
      // 只有当 props 变化时才会重新渲染
      return <div>{props.value}</div>;
    });
    

    示例

    function App() {
      console.log("App组件渲染");
      const [count, setCount] = useState(0);
      const [buttonCount, setButtonCount] = useState(0);
      return (
        <>
          <button onClick={() => setCount(count + 1)}>App组件计数 {count}</button>
          <ButtonMemo buttonCount={buttonCount} />
        </>
      );
    }
    
    const Button = ({ buttonCount }) => {
      console.log("Button组件渲染");
      return (
        <>
          <button>Button组件计数 {buttonCount}</button>
        </>
      );
    };
    
    const ButtonMemo = memo(Button);
    
    

    2.png

  • React.memo 浅比较(shallow compare)组件的 props的规则。

    • 如果 props 是基本类型(如 stringnumberboolean),比较值是否一样。
    • 如果 props 是对象、数组或函数,只会比较引用地址(浅比较),不会深入比较里面的值。由于这个对象可能定义在父组件中,每次父组件更新时这个对象就会被重新声明,其引用地址会发生变化。这会导致子组件的重新渲染。

    可以通过自定义比较函数来解决这个问题

    function UserCard(props) {
      return <div>用户名:{props.user.name}</div>;
    }
    
    function areEqual(prevProps, nextProps) {
      // 如果 name 没变,就不重新渲染
      return prevProps.user.name === nextProps.user.name;
    }
    
    export default React.memo(UserCard, areEqual);
    
  • 适用场景

    • 组件接收的 props 是稳定的数据(例如来自 useMemouseCallback)。
    • 组件渲染开销较大,例如包含大量 DOM 或复杂计算。
    • 父组件频繁更新,但子组件的 props 没有变化。

二、useMemo

  • 什么是 useMemo?

    useMemo 是一个 Hook,用于缓存计算结果,具体来说,useMemo 通过记忆计算结果来避免在每次渲染时都进行高开销的计算,只有当依赖项发生变化时,才会重新计算。

    使用方式useMemo( function , dev ),第一个参数是一个函数(开销大的函数),该函数返回需要记忆的计算结果。

    如下computeExpensiveValue 是一个模拟的高开销函数。通过使用 useMemo,我们可以确保这个函数只在 ab 变化时才执行,而不是在组件的每一次渲染过程中都执行。

    import React, { useMemo } from 'react';
    
    function ExpensiveComponent({ a, b }) {
      // 使用 useMemo 来记忆计算结果
      const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
      return <div>{memoizedValue}</div>;
    }
    
    function computeExpensiveValue(a, b) {
      console.log('start expensive computation...');
      // 模拟一个耗时操作
      let result = 0;
      for (let i = 0; i < 100000000; i++) {
        result += a + b;
      }
      return result;
    }
    

    不使用useMemo:

    function ExpensiveComponent({ a, b }) {
      // 使用 useMemo 来记忆计算结果
      const memoizedValue =  computeExpensiveValue(a, b)
      return <div>{memoizedValue}</div>;
    }
    

3.png

使用useMemo:

```jsx
function ExpensiveComponent({ a, b }) {
  // 使用 useMemo 来记忆计算结果
  const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  return <div>{memoizedValue}</div>;
}
```

4.png

  • useMemo适用场景

    • 计算密集型操作,例如数据转换、排序、过滤等。
    • 需要将计算结果作为 props 传递给优化后的子组件(如 React.memo)。

三、useCallback

  • 什么是 useCallback?

    在 React 中,每次组件重新渲染时,函数组件中的函数都会重新定义,也就是说它们的引用地址会变

    例如:

    function App() {
      const handleClick = () => {
        console.log("点击了");
      };
    
      return <ChildComponent onClick={handleClick} />;
    }
    

    在这个例子中,每次 App 重新渲染,handleClick 都会是一个新的函数引用。如果 ChildComponent 用了 React.memo 来优化渲染,但由于 onClick 是一个新的函数,React 会认为 props 改变了,于是子组件还是会重新渲染 —— 即使逻辑上它并不需要重新渲染

    useCallback可以解决这个问题。useCallback 是一个 Hook,用于缓存函数的引用只有在依赖项变化时才生成新的函数。所有它常用于避免子组件因函数 props 变化而不必要的重新渲染。

    useCallback( 函数, 依赖项)

    const memoizedCallback = useCallback(() => {
      doSomething(a, b);
    }, [a, b]);
    
  • 对比有无 useCallback

    没有用 useCallback 的情况:

    const ParentComponent = () => {
      const [count, setCount] = useState(0);
    
      const handleClick = () => {
        console.log("点击了");
      };
    
      return (
        <div>
          <button onClick={() => setCount(count + 1)}>增加</button>
          <ChildComponent onClick={handleClick} />
        </div>
      );
    };
    

    每次点击按钮,handleClick 都是一个新函数,即使内容没变,也会导致 ChildComponent 重新渲染。

    使用 useCallback 后:

    const ParentComponent = () => {
      const [count, setCount] = useState(0);
    
      const handleClick = useCallback(() => {
        console.log("点击了");
      }, []);
    
      return (
        <div>
          <button onClick={() => setCount(count + 1)}>增加</button>
          <ChildComponent onClick={handleClick} />
        </div>
      );
    };
    

    此时,handleClick 的引用不变,如果 ChildComponent 用了 React.memo,它就不会在 count 改变时重新渲染。

  • 适用场景

    • 将函数作为 props 传给 React.memo 优化过的子组件。
    • 函数作为依赖项传入其他 Hook(如 useEffectuseMemo)中,避免不必要的副作用触发。

四、三者结合使用示例

下面是一个结合 React.memouseMemouseCallback 的完整示例:

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

// 优化后的子组件
const MemoizedChild = React.memo(({ data, onClick }) => {
  console.log('Child rendered');
  return (
    <div>
      <h3>Memoized Child</h3>
      <p>{data}</p>
      <button onClick={onClick}>Click Me</button>
    </div>
  );
});

// 父组件
const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('hello');

  // 缓存计算结果
  const processedText = useMemo(() => {
    return text.toUpperCase();
  }, [text]);

  // 缓存函数引用
  const handleClick = useCallback(() => {
    alert('Button clicked!');
  }, []);

  return (
    <div>
      <h2>Parent Component</h2>
      <p>Count: {count}</p>
      <button onClick={() => setCount(prev => prev + 1)}>Increment</button>
      <input value={text} onChange={e => setText(e.target.value)} />

      <MemoizedChild data={processedText} onClick={handleClick} />
    </div>
  );
};

export default ParentComponent;

分析:

  • processedText 使用 useMemo 缓存了处理后的文本,避免每次渲染都调用 toUpperCase()
  • handleClick 使用 useCallback 缓存函数引用,确保 MemoizedChild 不会因函数变化而重新渲染。
  • MemoizedChild 使用 React.memo,仅在 dataonClick 变化时才重新渲染。

五、总结

Hook / API用途适用场景注意事项
React.memo避免组件重复渲染子组件频繁渲染、props 稳定默认浅比较,复杂对象需自定义比较函数
useMemo缓存计算结果数据处理、作为 props 传给 memo 组件不保证缓存,不能依赖其保持状态
useCallback缓存函数引用作为 props 传给 memo 组件、作为依赖项不保证引用稳定,注意依赖项完整性