4. React.memo:纯组件与浅比较
React 默认的组件重渲染机制是不加判断的——只要父组件状态变化,整个子树都会重新执行 render 函数,即使子组件的 props 没有发生任何改变。当子树中节点数量多或计算量大的时候,这种“无差别”重渲染会造成可感知的性能浪费。React.memo 是为函数组件提供的性能优化方案,它通过浅比较新旧 props 来决定是否跳过本次渲染,让组件表现出“纯组件”的行为。
工作原理
React.memo 是一个高阶组件,接收一个函数组件和一个可选的比较函数,返回被缓存的新组件。每当父组件渲染时,React 会先调用比较函数(默认为浅比较),判断新 props 与上一次渲染时的 props 是否相等。若相等,则直接复用上次的虚拟 DOM 结果,跳过本次 render 执行;若不相等,则正常重新渲染。
关键优化点:避免因父组件无关状态变化导致子组件不必要的 render 执行。memo 只针对 props 变化做拦截,不干扰内部 state 或 context 变化。
基本使用
使用 React.memo 包裹函数组件即可,通常不需要手动传递比较函数。
import React, { useState } from 'react';
// 未优化的子组件
const Child = ({ name }) => {
console.log('Child rendered');
return <div>{name}</div>;
};
// 使用 React.memo 包裹
const MemoChild = React.memo(Child);
const Parent = () => {
const [count, setCount] = useState(0);
const [name, setName] = useState('Alice');
return (
<div>
<p>计数: {count}</p>
<button onClick={() => setCount(c => c + 1)}>增加计数</button>
<button onClick={() => setName('Bob')}>修改名称</button>
<MemoChild name={name} />
</div>
);
};
上述示例中,点击“增加计数”时,count 变化触发父组件重新渲染,但 name 未变,MemoChild 的浅比较返回 true,控制台不会打印 “Child rendered”,从而避免无效渲染。
浅比较的限制
React.memo 默认使用 Object.is 对 props 对象的每个属性进行浅比较。当 props 中包含对象、数组、函数引用时,浅比较会失效,因为每次渲染都会生成新的对象引用。
典型陷阱:如果给 memo 组件传递内联对象或箭头函数,每次父组件渲染都会创建新引用,导致浅比较永远返回 false,memo 完全失效。
// 以下写法会让 memo 失效
<MemoChild name={{ firstName: 'Alice' }} />
<MemoChild onClick={() => handleClick()} />
解决方案:使用 useMemo 稳定对象引用,使用 useCallback 稳定函数引用。
自定义比较函数
当浅比较无法满足需求时,可以通过 React.memo 的第二个参数传递自定义比较函数。该函数接收 (prevProps, nextProps),返回 true 表示 props 相等(跳过渲染),返回 false 表示不等(重新渲染)。
示例:深比较
import React from 'react';
import isEqual from 'lodash.isequal';
const DeepMemoChild = React.memo(
({ data }) => {
return <div>{JSON.stringify(data)}</div>;
},
(prevProps, nextProps) => {
// 使用 lodash 深比较
return isEqual(prevProps.data, nextProps.data);
}
);
性能警告:深比较代价较高,每次渲染都会执行完整递归对比。仅在确实需要比较深层嵌套数据且数据量不大时使用。对高频变化的场景,更推荐设计扁平的数据结构并配合 useMemo 生成稳定引用。
自定义比较函数注意事项
- 必须严格按照
(prevProps, nextProps) => boolean签名。 - 如果比较函数返回
true,React 会完全跳过 render 以及后续的 diff 过程。 - 不要尝试在比较函数中执行副作用或异步操作。
- 比较函数本身不应产生额外渲染开销,建议保持轻量。
React.memo 工作流程
调试技巧
- 在 React DevTools 中,被 memo 包裹的组件会显示 “Memo” 标记,点击后可以查看 “Renders” 信息。
- 启用 DevTools 的 “Highlight updates when components render” 可以直观看到哪些组件没有被 memo 阻止。
- 在比较函数中加入
console.log可辅助判断比较结果。
常见错误与避坑
| 错误模式 | 表现 | 修复 |
|---|---|---|
| 传内联对象 / 函数 | memo 失效,子组件总重渲染 | 使用 useMemo / useCallback 稳定引用 |
| 忘记包裹子组件 | 性能问题未解决 | 对 每个 可能因父组件无关状态重渲染的组件添加 memo |
| 自定义比较函数返回 false 导致从不更新 | 组件卡在旧状态 | 确保比较逻辑正确,考虑非 props 变化(如 context)的处理 |
| 在 memo 组件中使用内联样式对象 | 每次渲染生成新对象,memo 失效 | 将样式对象提取到组件外部或用 useMemo 缓存 |
性能优化建议
- 配合 useCallback / useMemo:传递给 memo 组件的回调函数和数据对象必须稳定引用,否则 memo 形同虚设。
- 避免滥用:memo 本身有比较开销,仅对渲染成本高且 props 变化频率低的组件使用。
- 优先扁平化 props:将复杂对象拆解为基本类型属性,让浅比较能正常工作。
- 测量后再优化:使用 React Profiler 找出实际造成性能瓶颈的组件,针对性地添加 memo。
对比:React.memo vs PureComponent vs shouldComponentUpdate
| 特性 | React.memo | PureComponent | shouldComponentUpdate |
|---|---|---|---|
| 适用组件类型 | 函数组件 | 类组件 | 类组件 |
| 默认比较方式 | 浅比较 props | 浅比较 props + state | 无(需手动实现) |
| 自定义比较 | 第二个参数:areEqual | 不可直接自定义(需重写 lifecycle) | 返回 true / false |
| 性能开销 | 每次渲染执行比较函数 | 与 memo 类似 | 开发者控制 |