先记住这个答案
arePropsEqual 接收旧 props 和新 props,只有它们会产生相同输出和行为时才应返回 true。适合输入结构明确、比较成本可控,并且默认引用比较导致昂贵子树频繁重算的场景。比较范围必须包含函数 prop:新函数可能捕获新的权限或业务状态,忽略它会让旧闭包继续响应点击。比较器自身应当纯粹,且需要用真实生产负载证明比较比重新渲染更划算。
- true 表示允许复用旧输出和行为
- 函数 prop 也属于必须比较的输入
- 有界数据比较仍需要测量成本
从输出与行为的契约推导比较字段
下面的列表只读取候选人的 id、name 和 onPick。即便父级重新构建内容相同的数组,这几个字段仍可以有界地逐项比较;只比较数组长度显然不够,同长度列表也可能换人或改名。
onPick 同样不能省略。假设父级把录取回调换成另一个团队的处理函数,列表文字完全不变,用户下一次点击却应进入新的团队流程。旧函数的源码看起来相同,也无法证明捕获的数据相同。
import { memo } from 'react';
type Person = Readonly<{ id: string; name: string }>;
type Props = {
people: readonly Person[];
onPick: (id: string) => void;
};
function PeopleList({ people, onPick }: Props) {
return <ul>{people.map(person => <li key={person.id}>
<button onClick={() => onPick(person.id)}>{person.name}</button>
</li>)}</ul>;
}
export default memo(PeopleList, (previous, next) =>
previous.onPick === next.onPick &&
previous.people.length === next.people.length &&
previous.people.every((person, index) =>
person.id === next.people[index].id &&
person.name === next.people[index].name
)
);这个比较器只适用于示例声明的三类输入。增加头像地址、禁用状态或其他 prop 时,需要同步检查比较契约;不能直接把这段代码当作任意业务列表的通用等价判断。
可变输入会抹掉比较需要的过去
若父级直接修改同一个 person 对象,previous 与 next 可能指向已经被修改的同一份内容,比较器无法从中恢复旧名字。深比较也不能凭空找回历史值,因此不可变更新是输入设计的一部分。
比较器中不要调用 setter、写缓存系统或发送请求。React 可能重新执行或放弃一次渲染,比较逻辑必须只依据输入返回判断。读取会随时间变化的外部变量同样会让这个判断难以推理。
把比较耗时和节省的工作放在一起算
逐项扫描列表的时间随人数增长,复杂嵌套结构的深比较成本还可能更高。如果列表渲染本身很便宜,每次父级更新都做比较反而增加工作。应在生产模式、代表性数据规模和真实操作下比较两种实现。
正确性验收至少包含字段修改、顺序调整、成员增删和回调替换。性能验收再观察交互延迟及子树耗时,不能从一次日志变少推导出系统一定更快,也不应为了漂亮的次数牺牲新回调的生效。
容易答错的地方
- 忽略函数 prop 以提高命中率
- 忽略函数可能让子树保持旧事件处理器,从而使用旧租户、旧筛选或旧权限。应比较函数身份,必要时让父级正确稳定回调;不能通过比较器阻止新业务语义传播。
- 用 JSON.stringify 当通用比较
- 序列化会忽略某些值,无法表达函数闭包,还会分配字符串并遍历数据。即使某个纯数据对象可以序列化,也要单独证明业务等价与成本,不能把序列化相等视为任意 props 的安全替代品。
面试官还会怎么问?
arePropsEqual 与 shouldComponentUpdate 返回值一样吗?
方向相反。arePropsEqual 返回 true 表示新旧输入等价,可以考虑跳过;shouldComponentUpdate 返回 true 表示需要更新。迁移已有类组件比较逻辑时应先用具体输入列出预期,再决定是否取反及比较哪些字段。
比较器能挡住本地 state 更新吗?
不能,它是 props 更新的比较边界。组件自己的 state 或所消费的 Context 变化有独立更新路径;如果这些更新昂贵,应调整状态位置或订阅范围,而不是继续加大 props 比较器。
回调每次都是新函数怎么办?
先判断子树是否确实昂贵,再让父级通过完整依赖的 useCallback 保留无关更新时的函数身份。回调真实依赖改变后重新渲染是正确结果,也可以重构接口让必要输入更明确。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。