先记住这个答案
PureComponent 是类组件的基类,提供对 props 和 state 的浅比较;memo 返回一个具有 props 优化边界的组件,函数组件自己的 state 仍按 Hook 更新语义处理。普通类组件可以通过 shouldComponentUpdate 自定义更新判断,其 true 表示需要渲染,而 memo 的 arePropsEqual 返回 true 表示输入等价、可以跳过。它们都要求输入不可变,也不能阻止所消费的 Context 发生有效更新。
- PureComponent 浅比较 props 与 state
- memo 的比较器只负责 props 边界
- 两个自定义判断函数的布尔方向相反
名字相近,但不是同一份比较规则
PureComponent 会浅比较状态对象中的各项值。函数组件调用 useState setter 时,则由该 Hook 的更新机制处理值是否相同;memo 并不接收函数组件的整个内部状态,也不会替它实现类状态对象的浅比较。
因此将类中的对象 state 换成一个 useState 后,每次创建内容相同的新对象并不等于维持旧状态值。需要先设计状态边界,再讨论 memo,不能假设类组件的所有优化条件会随语法迁移自动保留。
同一输入下,两种组件仍能独立展开
下面两个组件接收同一个 score,却各自拥有 expanded。点击函数版展开不会改变类版,点击类版展开也不会被 PureComponent 拦住,因为状态项确实发生变化。父级无关计数则提供重复 props 的观察入口。
这个例子用简单文本是为了观察行为边界,没有宣称它值得性能优化。真实迁移还要核对生命周期、事件闭包和状态结构,先确认两种组件的用户行为一致,再测量不必要的渲染是否减少。
import { memo, PureComponent, useState } from 'react';
export class ClassScore extends PureComponent<{ score: number }, { expanded: boolean }> {
state = { expanded: false };
render() {
return <section>
<button onClick={() => this.setState(s => ({ expanded: !s.expanded }))}>
类版展开
</button>
<p>类版:{this.props.score} / {String(this.state.expanded)}</p>
</section>;
}
}
export const FunctionScore = memo(function FunctionScore({ score }: { score: number }) {
const [expanded, setExpanded] = useState(false);
return <section>
<button onClick={() => setExpanded(value => !value)}>函数版展开</button>
<p>函数版:{score} / {String(expanded)}</p>
</section>;
});
export default function ComparisonDemo() {
const [revision, setRevision] = useState(0);
const [score, setScore] = useState(80);
return <>
<button onClick={() => setRevision(n => n + 1)}>无关更新</button>
<output>父计数:{revision}</output>
<button onClick={() => setScore(n => n + 1)}>修改分数</button>
<ClassScore score={score} />
<FunctionScore score={score} />
</>;
}同分数的父级更新和组件自身展开是两类操作。比较应包含父级计数、修改分数以及两侧独立展开,避免只看某次跳过日志就误以为内部状态也被缓存冻结。
迁移比较函数时先写出真值含义
shouldComponentUpdate 的参数是下一份 props、state,旧值可以从类实例读取;arePropsEqual 的两个参数是旧 props 与新 props,且返回方向相反。直接复制原来的布尔表达式可能把更新和跳过完全颠倒。
类 setState 对对象进行浅合并,useState setter 则替换这一份状态值。如果原代码只提交一个字段,迁移后通常需要显式保留其他字段,或拆成独立状态;这属于数据语义变化,与 memo 是否存在无关。
容易答错的地方
- 认为 memo 会浅比较内部对象 state
- memo 的自定义比较器看不到 Hook 内部状态。向 useState 提交新对象时,需要依照该 Hook 的相等性语义分析;保留旧对象并修改字段也不是优化方案,会让状态快照和依赖关系失真。
- 把类更新判断原样传给 memo
- 两个接口的参数结构和布尔含义都有差异。应列出相同输入、字段变化和函数变化三个用例,明确哪些需要更新,然后重写比较表达式;简单复制甚至盲目取反都可能漏掉行为依赖。
面试官还会怎么问?
为什么不可变更新对两者都重要?
浅比较依赖新旧值仍然保有各自的身份和内容。直接修改共享对象会使旧输入也被污染,比较无法还原变化前状态;创建发生变化路径的新对象,才能让比较可靠地识别有效更新。
Context 变化会穿过这两种优化吗?
组件实际消费的 Context 值变化仍会触发相应更新。若大范围订阅导致高频重算,应拆分提供者或缩小消费范围;props 浅比较不能替代对订阅来源和状态位置的设计。
新代码应该为了优化改写成类组件吗?
不需要。函数组件可以通过合理状态边界和必要的 memo 获得优化,启用 React Compiler 的项目还可能获得自动缓存。选择类组件应有现存代码或框架约束,而不是把 PureComponent 当作更强的通用性能开关。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。