React memo 对 props 的浅比较

FreeGuideOnline 最新 2026-07-05

javascript const obj1 = { name: 'Alice' }; const obj2 = { name: 'Alice' }; obj1 === obj2; // false,因为它们指向不同的对象

const arr1 = [1, 2, 3]; const arr2 = [1, 2, 3]; arr1 === arr2; // false

const sameObj = obj1; obj1 === sameObj; // true


React 内部对 props 的浅比较正是基于这个逻辑:逐个对比新旧 props 对象中的每个属性值。

## React.memo 何时认为 props 没有变化?

假设我们有一个被 `React.memo` 包裹的组件 `MyComponent`:

```jsx
const MyComponent = React.memo(function MyComponent({ user, onSave }) {
  // 组件逻辑
});

当父组件重新渲染时,React 会检查传入的 useronSave 是否与上一次相同。判断相同必须满足:

  • 新旧 props 对象的每个属性经过浅比较都相等。
  • 即对每个 key:prevProps[key] === nextProps[key]

如果有任何一个属性不满足(比如对象字面量每次都是新引用),React 就知道 props 变化了,会重新渲染组件。

常见的意外重渲染陷阱

初学者最常踩的坑就是:明明数据内容没变,组件还是在更新。根源往往来自浅比较未能“看穿”的引用类型。

陷阱1:在父组件中直接定义对象/数组

function Parent() {
  return (
    <MyComponent
      user={{ name: 'Alice' }}   // 每次父组件渲染都创建新对象
      roles={['admin']}          // 每次都是新数组
    />
  );
}

浅比较结果prevProps.user !== nextProps.user → 总是 trueReact.memo 失效。

解决方案:将对象/数组提到组件外部,或用 useMemo/useRef 稳定引用。

const staticUser = { name: 'Alice' };
function Parent() {
  return <MyComponent user={staticUser} />;
}

陷阱2:在渲染中声明函数

function Parent() {
  const handleSave = () => { /* ... */ }; // 每次渲染都是新函数

  return <MyComponent onSave={handleSave} />;
}

浅比较结果:每次都是新的函数引用 → 组件总是重渲染。

解决方案:使用 useCallback 包裹函数,使其在依赖不变时返回同一个引用。

const handleSave = useCallback(() => {
  // ...
}, [/* 依赖项 */]);

陷阱3:children prop 也是引用类型

如果组件通过 JSX 传递了 children,且 children 是一个内联的 React 元素,那么它每次也是新引用。

<MyComponent>
  <span>Hello</span>   {/* 每次渲染创建新的 React 元素 */}
</MyComponent>

解决方案:将 children 部分提升为常量,或同样使用缓存手段。

自定义比较函数:当浅比较不够用

有时我们需要比浅比较更“深”一点的对比,或者想忽略某些 props 的变化。这时可以向 React.memo 传入第二个参数:一个 areEqual 函数。

function areEqual(prevProps, nextProps) {
  /*
    返回 true 表示 props 相等 → 不重新渲染
    返回 false 表示 props 不相等 → 重新渲染
  */
  return (
    prevProps.user.id === nextProps.user.id &&
    prevProps.roles.length === nextProps.roles.length
  );
}

const MyComponent = React.memo(BaseComponent, areEqual);

自定义比较函数的编写要点

  • 它替代了默认的浅比较,因此你需要手动比较每一个你关心的 prop
  • 如果某个 prop 是对象,你可以选择性地比较其内部关键属性,而不是引用地址。
  • 注意不要使用深比较(deep equal),因为其性能开销可能抵消 memo 带来的收益。只对比那些真正影响渲染的值。
  • 可以在开发环境下利用 areEqual 打印日志来排查不必要的渲染。

示例:忽略不常变化的某些 prop

function arePropsEqual(prev, next) {
  // 只对比 user 和 settings,忽略 timestamp
  return (
    prev.user === next.user &&
    prev.settings === next.settings
  );
}

浅比较在 React 中的其他身影

了解浅比较有助于你触类旁通,因为 React 的多个 API 都依赖它。

  • PureComponent:类组件的 shouldComponentUpdate 内部默认实现就是浅比较 props 和 state。
  • useEffect / useMemo / useCallback 的依赖数组:React 使用类似的浅比较(通过 Object.is 算法)来决定是否重新执行副作用或重新计算值。
  • React.memo 本身就是函数组件的 PureComponent 等价物。

性能考量与最佳实践

什么时候使用 React.memo?

  • 组件经常被父组件无意义重渲染,且自身的渲染成本较高(大量子元素、复杂计算等)。
  • 组件的 props 大多是稳定的基本类型或通过缓存后的引用。
  • 不要无差别地包裹所有组件,因为浅比较本身也有开销,对于极轻量的组件,memo 可能得不偿失。

配合 useMemo 和 useCallback 构建稳定引用

这是让 React.memo 生效的黄金组合:

const Parent = () => {
  const [count, setCount] = useState(0);
  const config = useMemo(() => ({ step: 1, key: 'alpha' }), []);
  const handleClick = useCallback(() => setCount(c => c + 1), []);

  return (
    <>
      <MemoChild config={config} onClick={handleClick} />
      <button onClick={handleClick}>Count: {count}</button>
    </>
  );
};