
一句话:引用不稳定是 React 性能问题最常见的根源——「每次渲染都新建的对象/数组/函数」会让
memo失效、让useEffect反复执行、让缓存失去意义;解法是「提到模块外 →useMemo/useCallback→ 依赖原始值 → 用ref→data-*+ 事件委托」。
一、五种「不稳定引用」的来源
function Parent({ items, onSelect }: Props) {
// ① 对象字面量(每次渲染都是新对象)
const options = { sort: 'asc', page: 1 };
// ② 数组字面量
const columns = ['name', 'age', 'email'];
// ③ 内联箭头函数
const handleClick = () => onSelect(1);
// ④ 渲染中计算出的新数组/新对象
const visible = items.filter((it) => it.active);
const stats = { total: items.length, active: visible.length };
// ⑤ 用对象作为默认参数
// function Child({ options = {} }) {} ← 每次渲染都是新的 {}
}
为什么它们是「新引用」?
因为「每次渲染都会重新执行组件函数」→
函数体里的「对象/数组/函数字面量」每次都会重新创建。
⚠️ 这不是 bug,而是「JS 的语义」——React 的设计就是「渲染 = 重新执行函数」。
所以「稳定引用」需要你显式处理。
二、三个后果
// ① memo 失效(子组件每次都重渲染)
const MemoChild = React.memo(Child);
<MemoChild options={{ a: 1 }} /> // ⚠️ 每次都是新对象 → 一直渲染
// ② useEffect 反复执行(甚至无限循环)
useEffect(() => {
fetchData(config);
}, [config]); // ⚠️ config 每次都是新对象 → 每次执行
// 更严重的:effect 里又 setState → 重新渲染 → 新对象 → 再执行 → 死循环
// ③ useMemo/useCallback 完全失效
const value = useMemo(() => compute(a), [a, { extra: 1 }]); // ⚠️ 依赖不稳定 → 每次重算
const fn = useCallback(() => f(a), [{ a }]); // ⚠️ 同理
| 后果 | 表现 |
|---|---|
memo 失效 |
子组件无谓重渲染(Profiler 里「Parent re-rendered」) |
useEffect 反复执行 |
重复请求、订阅被反复重建、内存增长 |
| 无限循环 | 「渲染 → effect setState → 渲染 → …」 |
useMemo/useCallback 失效 |
「看起来在优化」但毫无作用 |
| 子组件 local state 重置 | 如果配合 key 变化或组件重建 |
三、解法一:提到模块作用域(最优)
// ✅ 纯常量 → 提到组件外(引用天然稳定、无需 memo)
const OPTIONS = { sort: 'asc', page: 1 };
const COLUMNS = ['name', 'age', 'email'];
const EMPTY_ARRAY: never[] = [];
const noop = () => {};
function Parent({ items }: Props) {
return (
<>
<Child options={OPTIONS} columns={COLUMNS} />
<List items={items} emptyFallback={EMPTY_ARRAY} onRetry={noop} />
</>
);
}
// ✅ 默认参数的坑:默认值也要提到外面
// ❌ 每次渲染都创建新的 {}
function Child({ options = {} }: { options?: Options }) {}
// ✅
const DEFAULT_OPTIONS: Options = {};
function Child({ options = DEFAULT_OPTIONS }: { options?: Options }) {}
// ✅ 数组同理
const EMPTY: Item[] = [];
function List({ items = EMPTY }: { items?: Item[] }) {}
判断标准:
「这个值依赖任何 props/state 吗?」
├─ 不依赖 → 提到模块作用域(最优)
└─ 依赖 → 用 useMemo/useCallback(或依赖原始值)
四、解法二:useMemo / useCallback
function Parent({ items, sortBy, onSelect }: Props) {
// ✅ 依赖 props/state 时用 useMemo
const options = useMemo(() => ({ sort: sortBy, page: 1 }), [sortBy]);
const visible = useMemo(
() => items.filter((it) => it.active).sort(byScore),
[items]
);
// ✅ 函数用 useCallback
const handleSelect = useCallback((id: string) => onSelect(id), [onSelect]);
return <Child options={options} items={visible} onSelect={handleSelect} />;
}
⚠️ 两个常见的失效写法:
① 依赖里有新对象
useMemo(() => f(a), [a, { extra: 1 }]) // ❌ 依赖不稳定 → 每次都算
② 依赖里调用函数
useMemo(() => f(a), [getKey()]) // ❌ 每次渲染都调用 getKey()
五、解法三:依赖「原始值」而不是对象
// ❌ 依赖整个对象(且对象引用不稳定)
function UserCard({ user }: { user: User }) {
useEffect(() => {
loadProfile(user);
}, [user]); // ⚠️ user 每次都是新对象(如果上游没稳定它)
}
// ✅ 依赖用到的原始值
function UserCard2({ user }: { user: User }) {
useEffect(() => {
loadProfile(user.id); // ⭐ 只依赖 id
}, [user.id]);
}
// ✅ 列表依赖的常见写法
// ❌ 依赖 items(如果 items 每次都是新数组 → 每次重算)
const total = useMemo(() => items.reduce((s, i) => s + i.price, 0), [items]);
// ✅ 依赖「能代表内容的原始值」(在确有必要时)
const ids = useMemo(() => items.map((i) => i.id).join(','), [items]);
const total2 = useMemo(() => computeTotal(ids), [ids]); // 只在 id 集合变化时重算
// ⚠️ 这是「权衡」:牺牲了「内容变化时重算」的精确性,换取了稳定性
// 更推荐的做法是「让上游把 items 稳定下来」
六、解法四:用 ref 保存最新值
// 场景:Effect 需要「读最新值」但「不想因它重新执行」
function Component({ onSave }: { onSave: () => void }) {
const onSaveRef = useRef(onSave);
useEffect(() => {
onSaveRef.current = onSave; // ⭐ 每次渲染同步(不触发执行)
});
useEffect(() => {
const id = setInterval(() => onSaveRef.current(), 5000);
return () => clearInterval(id);
}, []); // ⭐ 依赖为空 → 不会因 onSave 变化重建
}
// 同理:把「不稳定的回调」通过 ref 稳定下来
function useStableCallback<T extends (...args: any[]) => any>(fn: T): T {
const ref = useRef(fn);
useEffect(() => { ref.current = fn; });
return useCallback((...args: Parameters<T>) => ref.current(...args), []) as T;
}
// 使用
function Parent({ onSelect }: { onSelect: (id: string) => void }) {
const stableSelect = useStableCallback((id: string) => onSelect(id));
// ⭐ stableSelect 引用永久稳定,但调用的是最新的 onSelect
return <MemoChild onSelect={stableSelect} />;
}
七、解法五:data-* + 事件委托(列表场景最优)
// ❌ 每行一个内联函数 → memo 失效
{items.map((it) => (
<MemoRow key={it.id} item={it} onClick={() => handleSelect(it.id)} />
))}
// ✅ 整列一个监听器 + 永久稳定的处理函数
const MemoRow = React.memo(function MemoRow({ item }: { item: Item }) {
return <li data-id={item.id}>{item.name}</li>; {/* ⭐ 用 data-* 携带身份 */}
});
function List({ items, onSelect }: { items: Item[]; onSelect: (id: string) => void }) {
const onSelectRef = useRef(onSelect);
useEffect(() => { onSelectRef.current = onSelect; });
const handleClick = useCallback((e: React.MouseEvent<HTMLUListElement>) => {
const el = (e.target as HTMLElement).closest('[data-id]');
const id = el?.getAttribute('data-id');
if (id) onSelectRef.current(id);
}, []);
return (
<ul onClick={handleClick}>
{items.map((it) => (
<MemoRow key={it.id} item={it} />
))}
</ul>
);
}
八、大对象的额外问题
// ① 用「大对象」作为依赖 → 浅比较本身也有成本
// ② 用「深比较」作为 memo 的比较函数 → 成本可能超过渲染
const Row = React.memo(RowComponent, (a, b) => {
return JSON.stringify(a.item) === JSON.stringify(b.item); // ❌ 大对象时非常慢
});
// ✅ 只比较「影响渲染的关键字段」
const Row2 = React.memo(RowComponent, (a, b) => {
return a.item.id === b.item.id && a.item.name === b.item.name;
});
// ③ 大对象传 props 时的「序列化/深拷贝」
const cloned = structuredClone(hugeObject); // ⚠️ 很贵,尽量别在渲染里做
// ✅ 用不可变更新(只复制路径上的对象)
// ④ Context value 含大对象 → 变化时所有消费者重渲染
const value = { user, permissions, menu, config }; // ⚠️ 任何一项变化都全量重渲染
// ✅ 拆分成多个 Context(见 5.9)
// ⑤ 一个容易忽略的:把「整个对象」放进 useEffect 依赖
useEffect(() => {
doSomething(user);
}, [user]); // ⚠️ user 可能频繁变化(即使只是引用变化)
// ✅ 依赖「用到的字段」
useEffect(() => {
doSomething(user.id, user.role);
}, [user.id, user.role]);
九、常见坑
// ① 对象/数组字面量直接传 props
<MemoChild options={{ a: 1 }} columns={['a']} /> // ❌
// ② 渲染中计算新数组传 props
<MemoList items={items.filter((x) => x.on)} /> // ❌
// ✅ useMemo
// ③ 默认参数是字面量
function Child({ options = {} }) {} // ❌
// ✅ 提到模块外
// ④ Effect 依赖整个对象
useEffect(() => {}, [config]); // ⚠️ 依赖不稳定 → 反复执行
// ✅ 依赖具体字段,或用 ref
// ⑤ useMemo 的依赖里有新对象 → 完全失效
useMemo(() => f(a), [a, { extra: 1 }]); // ❌
// ⑥ 用 JSON.stringify 做深比较(大对象时很慢)
// ⑦ 把「大对象」当 Context value(全量重渲染)
// ⑧ 在渲染里做「深拷贝」(structuredClone / JSON 复制)
// ⑨ 认为「直接写 useCallback 就能稳定」但依赖里有新对象
const cb = useCallback(() => f(id), [{ id }]); // ❌
// ⑩ 忘了「内联箭头函数」也是「不稳定引用」
// ⑪ 「所有回调都 useCallback」但子组件没 memo(无意义)
// ⑫ 把「ref 保存最新值」当成 hack 而不用(它是官方推荐的做法)
// ⑬ 依赖里放了「每次渲染创建的 Map/Set」
useEffect(() => {}, [new Map()]); // ❌
// ⑭ 「依赖数组」写了但传了非数组(如 [obj] 写成 obj)
useEffect(() => {}, config); // ⚠️ 语义错(不是数组)
十、面试延伸
- 「什么是『引用不稳定』?它会导致什么问题?」
「引用不稳定」指「每次渲染都产生新的对象/数组/函数引用」——因为 React 每次渲染都会重新执行组件函数,函数体里的字面量(对象、数组、箭头函数)都会重新创建。它会导致三类问题:①React.memo失效(浅比较发现 props 引用变了 → 子组件无谓重渲染);②useEffect反复执行(甚至「渲染 → effect setState → 渲染」的死循环);③useMemo/useCallback失效(依赖不稳定 → 每次重算)。它是 React 性能问题最常见的根源。 -
「怎么解决引用不稳定?」
五种手段:① 提到模块作用域——不依赖 props/state 的常量与函数(最优,无需任何 Hook);②useMemo/useCallback——依赖 props/state 时用;③ 依赖原始值而不是对象——[user.id]而不是[user];④ 用ref保存最新值——需要「稳定引用 + 读最新值」时(useEffect(() => { ref.current = fn; }));⑤data-*+ 事件委托——列表场景下让处理函数永久稳定。另外注意「默认参数也要提到模块外」(options = EMPTY_OBJECT)。 -
「为什么默认参数也要提到模块外?」
因为function Child({ options = {} })里的{}每次渲染都会创建一个新对象——这会让React.memo失效(props 引用每次不同)。所以规范是「把默认值定义为模块级常量」:const EMPTY = {}; function Child({ options = EMPTY }) {...}。数组同理(const EMPTY_LIST: Item[] = [])。 -
「
useMemo的依赖里有新对象会怎样?」
会让useMemo完全失效——因为每次渲染依赖都与上次不同(引用比较失败),React 每次都会重新执行计算函数。这不仅没有收益,还额外增加了「比较依赖 + 保存缓存」的开销,属于负优化。同理useCallback也一样。所以「先修依赖,再谈 memo」——如果发现useMemo没效果,第一件事就是检查依赖里是否有字面量。 -
「用
JSON.stringify作比较函数有什么问题?」
问题在于成本:JSON.stringify需要遍历整个对象并生成字符串——对于大对象,这个成本可能超过「重新渲染组件」的成本。所以「深比较」通常得不偿失。更好的做法是:① 只比较「影响渲染的关键字段」(a.item.id === b.item.id && a.item.name === b.item.name);② 或从源头保证引用稳定(让数据本身是不可变更新的,比较引用即可);③ 或改变结构,让需要比较的数据尽量小(如提前把「渲染所需的最小字段」提取出来)。 -
「为什么 Context 的 value 含大对象是问题?」
因为 Context 的订阅粒度是整个 value——value 里任何一项变化都会通知所有消费者重渲染。所以如果 value 是{ user, permissions, menu, config }这样的大对象,那么「改一下 menu」就会让「只用到 user 的组件」也重渲染。解法:① 拆分 Context(按变化频率/使用方);② 值与操作分离(ActionsContext永不更新);③ 用useMemo包装 value(至少消除「伪更新」)。详见 5.9。 -
「
ref保存最新值的模式为什么是官方推荐?」
因为它是「引用稳定 + 读取最新值」两个需求的唯一简洁解法:useEffect(() => { ref.current = fn; })(无依赖数组,所以每次渲染都更新、且不触发任何执行),然后在「只建立一次」的副作用里读ref.current。React 官方文档在useEffect的 FAQ 里有明确示例(如「如何在 Effect 里读最新 props」),useInterval、useEventListener等常用 Hook 也都用这个模式。它不是 hack,而是标准做法。 -
「引用稳定性和『不可变更新』有什么关系?」
两者是互补的:① 不可变更新保证「变化时一定产生新引用」(让 React 能通过引用比较发现变化);② 引用稳定保证「没变化时不要产生新引用」(让memo/useMemo能通过引用比较跳过工作)。如果只做不可变更新而不控制「无关的新引用」,就会出现「变化被发现了,但无关的也变了」;如果只追求引用稳定而不做不可变更新(如直接改对象),就会出现「变化没被发现」。两者一起才能让 React 的比较机制正常工作。
一句话速记
引用不稳定(每次渲染都新建对象/数组/函数)会让
memo失效、useEffect反复执行、useMemo完全失效,是 React 性能问题最常见的根源;五种解法按优先级是「提到模块作用域(含默认值)→useMemo/useCallback→ 依赖原始值(user.id而非user)→ 用ref保存最新值(useEffect(() => { ref.current = fn; }))→data-*+ 事件委托」;useMemo依赖里有字面量就等于没写(先修依赖);别用JSON.stringify深比较(成本可能超过渲染);Context value 含大对象会导致全量重渲染(拆分/分离/useMemo);「引用稳定」与「不可变更新」是一对互补要求。



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