React memo 对 props 的浅比较
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 会检查传入的 user 和 onSave 是否与上一次相同。判断相同必须满足:
- 新旧
props对象的每个属性经过浅比较都相等。 - 即对每个 key:
prevProps[key] === nextProps[key]。
如果有任何一个属性不满足(比如对象字面量每次都是新引用),React 就知道 props 变化了,会重新渲染组件。
常见的意外重渲染陷阱
初学者最常踩的坑就是:明明数据内容没变,组件还是在更新。根源往往来自浅比较未能“看穿”的引用类型。
陷阱1:在父组件中直接定义对象/数组
function Parent() {
return (
<MyComponent
user={{ name: 'Alice' }} // 每次父组件渲染都创建新对象
roles={['admin']} // 每次都是新数组
/>
);
}
浅比较结果:prevProps.user !== nextProps.user → 总是 true,React.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>
</>
);
};