
一句话:
React.memo是一个「高阶组件」,它对 props 做浅比较(Object.is逐项),相等就跳过该组件的重渲染——但它只在所有 props 引用都稳定时才生效,而且拦不住 Context 更新。
一、作用与原理
// 基本用法
const Child = React.memo(function Child({ value }: { value: number }) {
console.log('Child 渲染');
return <div>{value}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<Child value={1} /> {/* ✅ props 没变 → 跳过渲染 */}
</>
);
}
原理(简化):
const MemoComponent = memo(Component);
function MemoComponent(props) {
const prevProps = /* 上一次渲染的 props */;
if (prevProps !== null && shallowEqual(prevProps, props)) {
// ⭐ 浅比较相等 → 复用上一次的元素与子树,跳过渲染
return prevPropsOrMemoizedElement;
}
return <Component {...props} />;
}
// 浅比较(shallowEqual)
function shallowEqual(a, b) {
if (Object.is(a, b)) return true;
const keysA = Object.keys(a);
if (keysA.length !== Object.keys(b).length) return false;
for (const key of keysA) {
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
if (!Object.is(a[key], b[key])) return false; // ⭐ 逐项用 Object.is
}
return true;
}
⚠️ 关键点:memo 只比较【当前组件的 props】
· 它不会阻止「组件自己 setState 导致的渲染」
· 它不会阻止「订阅的 Context 变化导致的渲染」
· 它的比较是【浅】的(不递归比较对象内部)
二、生效条件
两条缺一不可:
① 组件被 React.memo 包裹
② 本次渲染的所有 props 与上次「浅比较相等」
// ✅ 生效:原始值 props
const A = React.memo(function A({ value }: { value: number }) { ... });
<A value={1} /> // 1 === 1 → 跳过
// ✅ 生效:稳定的对象/函数引用
function Parent() {
const options = useMemo(() => ({ page: 1 }), []);
const handleClick = useCallback(() => { ... }, []);
return <A options={options} onClick={handleClick} />; // 引用稳定 → 跳过
}
// ❌ 失效:内联函数
const B = React.memo(function B({ onClick }: { onClick: () => void }) { ... });
<B onClick={() => {}} /> // 每次都是新函数 → 不跳过
// ❌ 失效:对象/数组字面量
<B options={{ page: 1 }} /> // 每次都是新对象 → 不跳过
<B items={[1, 2, 3]} /> // 每次都是新数组 → 不跳过
| props 类型 | 浅比较结果 | memo 是否生效 |
|---|---|---|
1 / 'a' / true / null |
相等(Object.is) |
✅ |
| 同一个对象引用 | 相等 | ✅ |
| 内容相同的新对象 | 不等 | ❌ |
| 同一个函数引用 | 相等 | ✅ |
| 内联箭头函数 | 不等 | ❌ |
每次 filter/map 产生的新数组 |
不等 | ❌ |
外部创建、父组件未重渲染的 children |
相等 | ✅ |
三、失效场景清单
// ① 内联函数
<MemoChild onSelect={() => handle(id)} /> // ❌
// ② 对象/数组字面量
<MemoChild style={{ color: 'red' }} /> // ❌
<MemoChild items={list.filter((x) => x.active)} /> // ❌
// ③ 上游传来的不稳定引用
function Middle({ onChange }: { onChange: () => void }) {
return <MemoChild onChange={onChange} />; // ⚠️ onChange 若上游不稳定,这里也没用
}
// ④ 上游自己没 memo(父组件渲染 → 它渲染 → 它的子组件也渲染)
function Parent() {
return <Middle />; // ⚠️ Parent 重渲染时 Middle 重渲染
}
// → 只给最底层的 Child 加 memo 是无效的,需要在「链路上」逐层阻断
// ⑤ 每次渲染重新创建的「派生数据」
const sorted = [...items].sort(); // ⚠️ 每次都是新数组
<MemoChild items={sorted} /> // ❌
// ✅ const sorted = useMemo(() => [...items].sort(), [items]);
// ⑥ Context 更新(memo 拦不住)
const Consumer = React.memo(function Consumer() {
const ctx = useContext(Ctx); // ⚠️ Ctx 变化 → 仍会渲染
return <div>{ctx.a}</div>;
});
// ④ 的完整示例:链路上的「层层阻断」
function App() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<Middle />
</>
);
}
function Middle() { // ❌ 没 memo → 每次 App 渲染都会执行
return <Leaf />;
}
const Leaf = React.memo(function Leaf() {
console.log('Leaf 渲染'); // ⚠️ 依然会渲染(因为 Middle 重新创建了 <Leaf />)
return <div>叶子</div>;
});
// ✅ 修复:给 Middle 也加 memo,或用 children 透传
const Middle2 = React.memo(function Middle2() {
return <Leaf />;
});
四、自定义比较函数
// memo 的第二个参数:(prevProps, nextProps) => boolean
// ⭐ 返回 true 表示「相等 → 跳过渲染」
const Child = React.memo(
function Child({ user, onSave }: ChildProps) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// 只比较我们关心的字段
return prevProps.user.id === nextProps.user.id;
// ⚠️ 注意:如果 user.name 变了但 id 没变 → 不会重渲染(可能不符合预期)
}
);
// 更安全的写法:显式列出「影响渲染的字段」
const Child2 = React.memo(
function Child2({ user, theme }: ChildProps) {
return <div>{user.name}</div>;
},
(prev, next) => {
return (
prev.user.id === next.user.id &&
prev.user.name === next.user.name && // ✅ 显式列出
prev.theme === next.theme
);
}
);
| 比较函数 | 风险 |
|---|---|
| 只比 id | ⚠️ 内容变化不会重渲染(bug 隐患) |
| 显式列出全部影响渲染的字段 | ✅ 安全(但要记得随需求更新) |
深比较(如 JSON.stringify) |
⚠️ 成本可能高于重渲染本身 |
| 直接用默认(浅比较) | ✅ 大多数情况够用 |
⚠️ 自定义比较函数的三个注意事项:
① 它是一个「性能优化点」,本身也要花时间(每次渲染都执行)
② 返回 true(相等)会跳过渲染 → 必须确保「真的不影响输出」
③ 深比较通常得不偿失(大对象比较的成本可能超过重新渲染)
五、与 PureComponent 的关系
// 类组件的等价物
class Child extends React.PureComponent {
render() {
return <div>{this.props.value}</div>;
}
}
// PureComponent = 在 shouldComponentUpdate 里做「浅比较 props 与 state」
// ⚠️ 两者有两个差异:
// ① memo 只能比较 props;PureComponent 还比较 state(但 state 变化时本来就该渲染)
// ② memo 支持自定义比较函数;PureComponent 需要自己写 shouldComponentUpdate
| 维度 | React.memo |
PureComponent |
|---|---|---|
| 适用 | 函数组件 | 类组件 |
| 比较对象 | props(浅比较) | props + state(浅比较) |
| 自定义比较 | ✅ 第二个参数 | 需要手写 shouldComponentUpdate |
| 现状 | 推荐 | 类组件遗留 |
六、什么时候不该用 memo
❌ 不该用的四种情况:
① 组件「本来就每次都要更新」(props 每次都不同)
→ memo 只是白白增加了比较开销
② 组件渲染成本极低(就几个 div)
→ 比较成本 ≈ 渲染成本,没有收益
③ 父组件只渲染一次(如只挂载一次的路由页面)
→ memo 永远不会生效(props 没变过,也没重渲染过)
④ props 里有「无法稳定的东西」(如第三方库给的回调)
→ 除非配自定义比较函数,否则一定失效
// ① 的反例
const Row = React.memo(function Row({ item, index }: RowProps) {
return <li>{index}. {item.name}</li>;
});
// ⚠️ 如果 index 每次都变(如每次重排),memo 永远失效
// ② 的反例(渲染成本极低)
const Label = React.memo(function Label({ text }: { text: string }) {
return <span>{text}</span>; // 成本 ≈ 比较成本
});
七、memo + 稳定引用的完整实践
// ✅ 一个完整正确的用法(三层配合)
interface RowProps {
item: Item;
onSelect: (id: string) => void;
}
const Row = React.memo(function Row({ item, onSelect }: RowProps) {
console.log('Row 渲染', item.id);
return (
<li onClick={() => onSelect(item.id)}>
{item.name}
</li>
);
});
function List({ items, onSelect }: { items: Item[]; onSelect: (id: string) => void }) {
// ① 稳定回调引用
const handleSelect = useCallback((id: string) => onSelect(id), [onSelect]);
// ② 稳定 items 引用(若 items 来自 props,由上游负责稳定)
const memoItems = useMemo(() => items, [items]); // 这里其实不需要(items 已稳定)
return (
<ul>
{memoItems.map((item) => (
<Row key={item.id} item={item} onSelect={handleSelect} />
))}
</ul>
);
}
// ⭐ 优化后的效果:
// 父组件因「其它状态」重渲染时,Row 不会重渲染
//(前提:items 里的元素引用不变、onSelect 稳定)
// 更彻底的做法:用「data-* + 事件委托」让处理函数彻底稳定
function List2({ items, onSelect }: { items: Item[]; onSelect: (id: string) => void }) {
// ⭐ 这个函数引用永久稳定(依赖为空)
const handleClick = useCallback((e: React.MouseEvent<HTMLUListElement>) => {
const id = (e.target as HTMLElement).closest('[data-id]')?.getAttribute('data-id');
if (id) onSelectRef.current(id);
}, []);
const onSelectRef = useRef(onSelect);
useEffect(() => { onSelectRef.current = onSelect; });
return (
<ul onClick={handleClick}>
{items.map((item) => (
<RowWithoutCallback key={item.id} item={item} />
))}
</ul>
);
}
// → RowWithoutCallback 的 props 只有 item(稳定)→ memo 永久生效
八、常见坑
// ① 加了 memo 但 props 里有内联函数/对象
<MemoChild onClick={() => {}} options={{ a: 1 }} /> // ⚠️ 失效
// ② 只给「最底层」加 memo,但链路上层没加
<Parent><Middle><MemoLeaf /></Middle></Parent> // ⚠️ Middle 没 memo → 依然渲染
// ③ 自定义比较函数写错(该更新时返回了 true)
(prev, next) => prev.user.id === next.user.id // ⚠️ name 变了也不更新
// ④ 认为 memo 能阻止 Context 引起的渲染
const Consumer = React.memo(function Consumer() {
const ctx = useContext(Ctx); // ⚠️ 无效
});
// ⑤ 认为 memo 能阻止「自己 setState 引起的渲染」
const Comp = React.memo(function Comp() {
const [n, setN] = useState(0); // ✅ setN 仍会渲染
return <button onClick={() => setN(n + 1)}>{n}</button>;
});
// ⑥ 用 memo 包裹「渲染成本极低」的组件(得不偿失)
// ⑦ 用深比较(JSON.stringify)作为比较函数(成本可能更高)
// ⑧ 依赖 memo 而忽略了「父组件传播」这个根源(更好的解法是 children 透传/状态下移)
// ⑨ 给「每次 props 都变」的组件加 memo(永远不会跳过)
// ⑩ 忘记 memo 比较的是「浅层」——对象内部的字段变化不会触发(引用没变时)
const [user, setUser] = useState({ name: 'a' });
user.name = 'b'; // ❌ 引用没变 → memo 认为「没变」
setUser({ ...user, name: 'b' }); // ✅ 新引用 → 正确更新
// ⑪ memo + forwardRef 的顺序写错(应该 memo(forwardRef(...)))
// ⑫ 在 memo 组件里用「默认参数创建的对象」作为默认值(每次都是新对象)
function Comp({ options = {} }: Props) { ... } // ⚠️ options 每次都是新对象
// ⑪ 的正确顺序
const Comp = React.memo(
React.forwardRef<HTMLDivElement, Props>(function Comp(props, ref) {
return <div ref={ref} {...props} />;
})
);
// ⑫ 的修复
const EMPTY = {};
function Comp({ options = EMPTY }: Props) { ... } // ✅ 引用稳定
九、面试延伸
- 「
React.memo是什么?它怎么工作?」
React.memo是一个高阶组件,它对组件的 props 做浅比较(对每个字段用Object.is);如果与上一次渲染的所有 props 都相等,就跳过这个组件的重新渲染(复用上一次的元素)。注意它只比较当前组件的 props:不会阻止「组件自己的setState」引起的渲染,也拦不住订阅的 Context 更新。 -
「
React.memo什么时候会失效?」
只要任何一个 prop 的引用发生变化就失效:① 内联箭头函数(onClick={() => {}});② 对象/数组字面量(options={{ a: 1 }});③ 每次渲染新创建的派生数据(items={list.filter(...)});④ 上游传递的不稳定引用(父组件没用useCallback/useMemo)。此外,如果链路上层组件没有 memo,即使最底层有 memo 也没用(上层重渲染会重新创建子元素 → props 变化)。 -
「
memo和useMemo/useCallback是什么关系?」
三者是配套使用的:React.memo决定「要不要跳过渲染」,它的判断依据是「props 是否浅相等」;而useMemo/useCallback是「稳定 props 引用」的手段。所以典型组合是「子组件 memo + 父组件用useCallback/useMemo稳定传给它的 props」——缺一不可。只加 memo 不稳定引用 → 失效;只稳定引用不加 memo → 没有意义。 -
「
memo能阻止 Context 变化引起的重渲染吗?」
不能。React.memo只比较 props,而 Context 的更新是「直接通知消费者组件」的(走的是 Context 传播路径,不经过 props 比较),因此 memo 无法拦截。要优化 Context 的重渲染只能靠「拆分 Context」「值与操作分离」「children透传」或改用「带选择器订阅的状态库」。 -
「自定义比较函数要注意什么?」
三个注意点:① 语义——函数返回true表示「相等、跳过渲染」(容易记反);② 完整性——只比较部分字段会导致「该更新时不更新」的 bug(如只比user.id,那么user.name变化就不会渲染);③ 成本——比较函数本身每次渲染都会执行,深比较(如JSON.stringify)的成本可能超过重新渲染。所以推荐「显式列出所有影响输出的字段」,并且只在确实必要时才用自定义比较。 -
「什么时候不该用
memo?」
四种情况:① 组件本来就每次都要更新(props 每次都不同,如index变化)→ memo 只会增加比较开销;② 组件渲染成本极低(几个div)→ 比较成本 ≈ 渲染成本;③ 父组件只渲染一次 → memo 永不生效;④ props 里有无法稳定的东西(第三方库的回调、Context 值)。判断标准是「Profiler 显示这个组件确实因为父组件重渲染而反复渲染,且渲染耗时不可忽略」。 -
「为什么『只给最底层组件加 memo』往往无效?」
因为 React 的更新是自顶向下传播的:父组件重渲染 → 它会重新创建子元素(<Middle />)→Middle没有memo所以会重新执行 →Middle又创建<Leaf />→ 即使Leaf有memo,它收到的 props(如果有)也是新创建的。所以阻断必须在「链路上」——从「状态变化的组件」到「目标组件」之间的每一层都要能跳过,或者用children透传 / 状态下移从结构上解决。 -
「
memo的代价是什么?」
两个:① 比较开销——每次渲染都要逐项浅比较 props(字段极多时不可忽略);② 维护成本——为了让 memo 生效,你必须在父组件里维护「稳定的引用」(useCallback/useMemo),这增加了代码复杂度与依赖数组的维护负担(且写错会失效)。所以「无脑加 memo」经常是负优化——应该先用 Profiler 确认这个组件是热点。
一句话速记
React.memo对 props 做浅比较(Object.is逐项),相等就跳过渲染——但只在所有 props 引用稳定时生效(内联函数、对象字面量、filter出的新数组都会让它失效),且拦不住 Context 更新与自己的setState;只给最底层加没用(上层要能一起跳过,或用children/状态下移);自定义比较函数返回true表示跳过(写错会导致「该更新不更新」,深比较通常得不偿失);与PureComponent等价(memo只比 props);判断标准是 Profiler 的耗时数据,而不是「感觉该加」。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!