先记住这个答案
memo 默认对每一项 prop 使用 Object.is。父函数每次执行时创建的新对象和箭头函数,即使内容或源码相同,也会被当作变化的输入,子组件因此失去这次跳过的机会。可以优先改传简单值,或者用 useMemo 缓存对象、用 useCallback 缓存函数,并完整声明它们读到的响应式值。稳定引用只能减少无关更新,不能阻止分类、权限或回调真正变化时把新行为传给子组件。
- 对象内容相等不等于引用相等
- 依赖必须覆盖对象内容与回调闭包
- 能简化 props 时先减少缓存维护成本
一个永远变化的 prop 就足够打破比较
设想列表工具栏接收 filters 和 onApply。父组件因无关计数重新执行时,字面量对象与箭头函数都产生新引用;任意一项比较失败,默认 memo 就不能凭其他相等项跳过这轮工作。
不要通过删除函数 prop 或在自定义比较器中忽略回调来伪造稳定。回调可能捕获当前分类或登录身份,忽略它会把视觉上没有变化的按钮变成执行旧业务逻辑的按钮,这属于正确性问题。
按真实输入构造缓存对象和函数
示例让 filters 跟随 category,让 apply 跟随 category 与外部 onPick。增加无关计数时它们保持引用;切换分类时两者更新。外部替换 onPick 后,按钮也必须调用最新处理函数。
这里的 limit 是固定常量,因此不会额外成为组件内的响应式依赖。如果将它改成 prop 或 state,就应加入构造对象的依赖。手工省略依赖换来的稳定,是让新输入失去传播机会。
import { memo, useCallback, useMemo, useState } from 'react';
const Toolbar = memo(function Toolbar({ filters, onApply }: {
filters: { category: string; limit: number };
onApply: () => void;
}) {
return <button onClick={onApply}>
应用 {filters.category} / {filters.limit}
</button>;
});
export default function FilterDemo({ onPick }: {
onPick: (category: string) => void;
}) {
const [category, setCategory] = useState('react');
const [revision, setRevision] = useState(0);
const filters = useMemo(() => ({ category, limit: 20 }), [category]);
const apply = useCallback(() => onPick(category), [category, onPick]);
return <>
<button onClick={() => setRevision(n => n + 1)}>刷新外层</button>
<output>外层:{revision}</output>
<button onClick={() => setCategory(c => c === 'react' ? 'vue' : 'react')}>
切换分类
</button>
<Toolbar filters={filters} onApply={apply} />
</>;
}保持分类与处理函数不变时,两个缓存一起支持工具栏的 props 比较。验证必须包含切换分类和替换处理函数,确保优化只跳过无关工作,真实变化仍然进入按钮行为。
也可以消除需要稳定的大对象
如果工具栏只展示分类,接口直接接收 category 即可,limit 可以由合适的所有者提供。组件需要的字段越少,父级无关数据变化越不容易影响它,也少了一层依赖数组维护。
只在 Effect 内使用的配置对象通常可在 Effect 内创建,再依赖基本值;只在计算内部使用的临时对象也可移入计算函数。不要为每一个对象都建立缓存,否则简单代码会变成难以核对的依赖网络。
容易答错的地方
- 给所有回调都写空数组
- 空数组不会让闭包自动读取最新分类,它会保留初次缓存的函数及其捕获值。对会变化的输入应声明依赖,或者重构函数接口显式接收参数,不能以隐藏变化来换取比较成功。
- 把界面更新当成缓存失败
- 分类变化后工具栏重新执行是期望行为。需要排查的是无关父状态为什么穿透边界,而不是追求所有情况下都保持旧对象;缓存变化与业务数据变化一致才是可维护的优化。
面试官还会怎么问?
children 也是参与比较的 prop 吗?
是。父级每次创建的元素或数组也可能让 children 引用变化,不能只检查自己命名的业务字段。可以调整组件组合,让局部状态更靠近使用方,并结合实际渲染路径分析,而不是机械缓存全部 JSX。
保持引用能保证组件永远不更新吗?
不能,它仅支持 props 比较路径。子组件自身状态、订阅的 Context 以及重新挂载仍有各自语义;即使没有这些变化,业务正确性也不应依赖 React 必须保留某个优化结果。
同内容的新数组应该深比较吗?
先检查数组为什么被重建、能否保留不可变数据的结构共享,以及子组件计算是否足够昂贵。深比较本身要遍历数据,还容易漏掉回调语义,只有边界受控且实测有收益时才考虑自定义比较。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。