3、React.memo:函数组件的记忆化、浅比较与深比较、使用场景与注意事项

好,咱们来聊聊 React.memo。这个东西,说白了就是给函数组件加一层「缓存」。

我记得刚接触 React 时,最头疼的就是「明明数据没变,组件为啥又重渲染了?」。后来我才明白,React 默认情况下,只要父组件重新渲染,子组件就会跟着跑一遍。哪怕你传给子组件的 props 完全没变。

React.memo 就是来解决这个问题的。它像一个守门员,检查 props 有没有变化。没变?那就不重新渲染,直接用上次的结果。

React.memo 的基本用法

用法其实很简单。你写一个函数组件,然后用 React.memo 包一下:

const MyComponent = React.memo(function MyComponent(props) {
  // 你的组件逻辑
  return <div>{props.name}</div>;
});

或者用箭头函数:

const MyComponent = React.memo(({ name }) => {
  return <div>{name}</div>;
});

嗯,就这么简单。但真正用起来,坑可不少。

浅比较:React.memo 的默认行为

React.memo 默认用的是「浅比较」。什么意思?就是只比较 props 对象的第一层属性。

举个例子:

const Child = React.memo(({ user }) => {
  console.log('Child 渲染了');
  return <div>{user.name}</div>;
});

function Parent() {
  const [count, setCount] = useState(0);
  
  // 每次渲染都创建新对象
  const user = { name: '张三', age: 25 };
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>点击:{count}</button>
      <Child user={user} />
    </div>
  );
}

你猜怎么着?每次点按钮,Child 都会重新渲染。为什么?因为每次渲染时,user 对象都是新创建的。浅比较发现两个对象的引用不同,就认为 props 变了。

⚠️ 注意: 浅比较只比较引用地址,不比较对象内部的值。这是新手最容易踩的坑。

深比较:什么时候需要?

说实话,我在项目中很少用深比较。因为深比较本身有性能开销,你想想看,每次渲染都要递归遍历整个对象,这反而可能拖慢速度。

但有些场景确实需要。比如你从后端拿到的数据,每次都是新对象,但内容一样:

const areEqual = (prevProps, nextProps) => {
  // 手动实现深比较
  return JSON.stringify(prevProps) === JSON.stringify(nextProps);
};

const Child = React.memo(({ data }) => {
  return <div>{data.length} 条数据</div>;
}, areEqual);

我个人习惯用 lodash.isEqual 来做深比较,但只在确实需要的时候才用。别滥用。

使用场景:什么时候该用 React.memo?

不是所有组件都适合用 React.memo。我总结了几种典型场景:

  • 纯展示组件:只根据 props 渲染,没有内部状态
  • 频繁重渲染的组件:比如列表中的每一项
  • 计算量大的组件:渲染需要大量计算或数据处理的
  • 被多个父组件复用的组件:避免不必要的重复渲染

举个例子,一个商品列表:

const ProductItem = React.memo(({ product, onAddToCart }) => {
  console.log(`渲染商品:${product.name}`);
  return (
    <div>
      <h4>{product.name}</h4>
      <p>¥{product.price}</p>
      <button onClick={() => onAddToCart(product.id)}>加入购物车</button>
    </div>
  );
});

当列表有 100 个商品时,如果不用 React.memo,每次父组件更新都会重新渲染所有商品。用了之后,只有数据变化的商品才会重新渲染。

注意事项:避坑指南

我曾经在一个项目里,给所有组件都套上了 React.memo。结果性能反而更差了。为什么?因为 React.memo 本身也有开销——每次渲染都要做一次比较。

核心原则: 只在「比较开销 < 渲染开销」时使用 React.memo。

还有几个坑要注意:

  • 函数作为 props:每次渲染都会创建新函数,导致 React.memo 失效。需要用 useCallback 包裹
  • 对象/数组作为 props:同上,需要用 useMemo 或保持引用稳定
  • children 作为 props:如果父组件传了 JSX 作为 children,那每次都是新对象
  • 不要滥用:简单的组件,渲染开销很小,加 React.memo 反而得不偿失

来看一个典型的「函数 props 问题」:

function Parent() {
  const handleClick = () => {
    console.log('点击了');
  };
  
  return <Child onClick={handleClick} />;
}

// 这个 memo 基本没用,因为 handleClick 每次都是新函数
const Child = React.memo(({ onClick }) => {
  return <button onClick={onClick}>点击</button>;
});

正确的做法是用 useCallback:

function Parent() {
  const handleClick = useCallback(() => {
    console.log('点击了');
  }, []); // 依赖数组为空,函数引用不变
  
  return <Child onClick={handleClick} />;
}

性能测试:到底值不值得用?

我建议你在用之前先做个小测试。打开 React DevTools 的 Profiler,看看组件的渲染次数和耗时。

场景 不用 React.memo 用 React.memo 建议
简单文本渲染 0.1ms 0.15ms(含比较) 不用
复杂列表(100项) 50ms 5ms
图表组件 200ms 10ms 必须用

你看,对于简单组件,加 React.memo 反而更慢。但对于复杂组件,效果立竿见影。

💡 小技巧: 可以用 React.memo 的第二个参数自定义比较函数。但记住,自定义比较函数本身也有开销,别写太复杂。

总结一下

React.memo 是个好东西,但要用对地方。我的建议是:

  • 先分析性能瓶颈,别盲目加
  • 配合 useCallback 和 useMemo 一起用
  • 注意浅比较的局限性
  • 深比较慎用,除非你真的需要

嗯,记住一句话:React.memo 是优化工具,不是银弹。用对了事半功倍,用错了反而添乱。