一、先搞懂一个问题:为什么需要记忆化?
在 React 中,每次组件重新渲染时,函数组件内部的所有变量和函数都会被重新创建。对于简单逻辑这毫无影响,但在以下场景中会带来性能问题:
- 昂贵计算重复执行:遍历、筛选、聚合大量数据,每次渲染都重新算一遍
- 子组件无效重渲染 :父组件渲染时创建了新的函数/对象引用,即使逻辑没变,
React.memo也会认为 props 变化而触发子组件重渲染
useMemo 和 useCallback 就是 React 提供的两个记忆化(Memoization) Hook,核心思想一致:依赖不变,就复用上次结果,跳过重复工作。
二、useMemo:缓存计算结果
2.1 基本语法
jsx
const cachedValue = useMemo(() => {
// 耗时计算逻辑
return result;
}, [dep1, dep2]);
- 第一个参数:一个无参的纯函数,返回需要缓存的计算结果
- 第二个参数:依赖数组,数组中任意值变化时重新计算
- 返回值:首次渲染返回计算结果;依赖不变时返回上次缓存的值
2.2 底层原理
要真正理解 useMemo,必须先了解 React 的 Fiber 架构 和 Hook 链表机制。
2.2.1 数据存储:Hook 链表
在 React 的 Fiber 架构中,每个函数组件对应一个 Fiber 节点,该节点的 memoizedState 属性指向一个单向链表,按调用顺序串联组件中所有的 Hook 节点。
js
// Hook 节点的数据结构(简化版)
interface Hook {
memoizedState: any, // 存储当前 Hook 的状态值
baseState: any, // 基础状态
baseQueue: any, // 基础更新队列
queue: any, // 待处理更新队列
next: Hook | null, // 指向下一个 Hook
}
对于 useMemo,其 memoizedState 存储的是一个**数组 ****[value, deps]**,其中:
value:上次计算得到的结果deps:上次的依赖项数组
scss
fiber.memoizedState → hook1(useState) → hook2(useMemo) → hook3(useEffect) → null
[count, null] [cachedValue, deps] [cleanup, deps]
💡 这就是为什么 Hook 必须在顶层调用、不能放在条件语句中------React 靠调用顺序来定位链表中的节点,一旦顺序变化,就会读取到错误的状态。
2.2.2 双缓存机制:Current 与 WorkInProgress
React 采用双 Fiber 树机制:
- Current Tree:当前屏幕上正在显示的 Fiber 树
- WorkInProgress Tree:正在构建中的新 Fiber 树
每次组件重新渲染时,React 会基于 Current Tree 克隆出 WorkInProgress Tree,在 WorkInProgress 上执行组件函数、调用 Hook,完成后通过指针交换(root.current = finishedWork)完成更新。
useMemo 的缓存读取正是利用了这一机制:
js
// 简化版:从 alternate(旧的 Current Fiber)读取上次的 Hook 状态
const oldHook = workInProgress.alternate?.memoizedState;
const cachedValue = oldHook?.memoizedState[0]; // 取出上次缓存的值
const cachedDeps = oldHook?.memoizedState[1]; // 取出上次的依赖
2.2.3 挂载阶段:mountMemo
组件首次渲染时,调用 mountMemo:
js
function mountMemo<T>(nextCreate: () => T, deps): T {
// 1. 创建新的 Hook 节点,追加到 Hook 链表末尾
const hook = mountWorkInProgressHook();
// 2. 标准化依赖数组(undefined → null)
const nextDeps = deps === undefined ? null : deps;
// 3. 立即同步执行计算函数(在 Render 阶段)
const nextValue = nextCreate();
// 4. 将 [计算结果, 依赖数组] 存入 memoizedState
hook.memoizedState = [nextValue, nextDeps];
// 5. 返回计算结果供组件使用
return nextValue;
}
关键点 :挂载阶段没有历史缓存可比较,所以无条件执行计算函数,建立初始缓存记录。
2.2.4 更新阶段:updateMemo
组件重新渲染时,调用 updateMemo,这是缓存逻辑的核心:
js
function updateMemo<T>(nextCreate: () => T, deps): T {
// 1. 从 Hook 链表中按顺序取出对应的 Hook 节点
const hook = updateWorkInProgressHook();
// 2. 标准化新依赖数组
const nextDeps = deps === undefined ? null : deps;
// 3. 读取上次缓存的状态 [cachedValue, cachedDeps]
const prevState = hook.memoizedState;
// 4. 依赖比较:如果依赖未变化,直接返回缓存值
if (nextDeps !== null) {
const prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0]; // 🎯 命中缓存,跳过计算
}
}
// 5. 依赖变化了,重新执行计算
const nextValue = nextCreate();
// 6. 更新缓存
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
2.2.5 依赖比较算法:Object.is 浅比较
areHookInputsEqual 是决定缓存是否命中的关键函数:
js
function areHookInputsEqual(nextDeps, prevDeps) {
if (prevDeps === null) return false;
// 长度不同,直接判定为变化
if (nextDeps.length !== prevDeps.length) return false;
// 逐项使用 Object.is 进行浅比较
for (let i = 0; i < prevDeps.length; i++) {
if (!Object.is(nextDeps[i], prevDeps[i])) {
return false;
}
}
return true;
}
React 使用 Object.is 而非 ===,主要差异在于:
Object.is(NaN, NaN)→true(NaN === NaN为false)Object.is(+0, -0)→false(+0 === -0为true)
这也解释了为什么引用类型作为依赖时必须保持引用稳定 ------即使对象内容完全相同,只要引用地址变了({} !== {}),Object.is 就会判定为"变化",触发重新计算。
2.2.6 执行时机:Render 阶段同步执行
useMemo 的计算函数在 Render 阶段同步执行,这意味着:
-
✅ 可以读取 props、state 等响应式数据
-
❌ 不能包含副作用(API 请求、DOM 操作、定时器等),因为 Render 阶段可能被 React 中断并重试
-
📌 副作用应该用
useEffect,它在 Commit 阶段异步执行,且保证只执行一次。
2.3 典型使用场景
✅ 场景一:昂贵计算缓存
js
function ProductList({ products, searchTerm }) {
// 每次渲染都 filter 上千条数据 → 卡顿
const filtered = useMemo(() => {
return products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [products, searchTerm]); // 仅当这两个值变化时才重新筛选
return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
✅ 场景二:稳定对象/数组引用,配合 React.memo
js
function Parent({ user }) {
// 每次渲染都创建新对象 → 子组件 memo 失效
const config = useMemo(() => ({
theme: 'dark',
locale: user.locale,
permissions: calcPermissions(user.role),
}), [user.locale, user.role]);
return <ChildComponent config={config} />;
}
const ChildComponent = React.memo(({ config }) => {
// 只有 config 引用真正变化时才重渲染
return <div>{config.theme}</div>;
});
✅ 场景三:避免 Effect 频繁触发
js
const options = useMemo(() => ({ method: 'GET', headers }), [headers]);
useEffect(() => {
fetch(url, options); // options 引用稳定,Effect 不会重复执行
}, [options]);
2.4 ❌ 不该用 useMemo 的场景
js
// ❌ 简单运算,缓存开销 > 计算开销
const value = useMemo(() => count + 1, [count]);
// ✅ 直接计算即可
const value = count + 1;
// ❌ 在 useMemo 中执行副作用
const data = useMemo(() => {
fetchData(); // 副作用!
return processedData;
}, [deps]);
// ✅ 副作用用 useEffect
三、useCallback:缓存函数引用
3.1 基本语法
jsx
const memoizedFn = useCallback(() => {
doSomething(a, b);
}, [a, b]);
- 第一个参数:需要缓存的回调函数
- 第二个参数:依赖数组
- 返回值 :依赖不变时,返回同一个函数引用
3.2 底层原理
useCallback 的底层实现与 useMemo 几乎完全一致 ,甚至可以认为 useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。 两者的核心差异仅在于存储的内容不同:
useMemo |
useCallback |
|
|---|---|---|
memoizedState[0] |
计算函数的返回值 | 回调函数本身(函数引用) |
| 缓存目的 | 避免重复计算 | 保持引用稳定 |
3.2.1 挂载阶段:mountCallback
js
function mountCallback<T>(callback: T, deps): T {
// 1. 创建新的 Hook 节点
const hook = mountWorkInProgressHook();
// 2. 标准化依赖数组
const nextDeps = deps === undefined ? null : deps;
// 3. 将 [函数引用, 依赖数组] 存入 memoizedState
// 注意:这里存的是函数本身,不是执行结果
hook.memoizedState = [callback, nextDeps];
// 4. 返回原始函数
return callback;
}
与 mountMemo 的关键区别:mountMemo 会执行 nextCreate() 获取返回值再存储,而 mountCallback 不执行函数,直接存储函数引用。
3.2.2 更新阶段:updateCallback
js
function updateCallback<T>(callback: T, deps): T {
// 1. 从 Hook 链表中按顺序取出对应的 Hook 节点
const hook = updateWorkInProgressHook();
// 2. 标准化新依赖数组
const nextDeps = deps === undefined ? null : deps;
// 3. 读取上次缓存的 [函数引用, 依赖数组]
const prevState = hook.memoizedState;
// 4. 依赖比较:使用与 useMemo 完全相同的算法
if (nextDeps !== null) {
const prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0]; // 🎯 返回缓存的函数引用
}
}
// 5. 依赖变化,更新缓存为新函数
hook.memoizedState = [callback, nextDeps];
return callback;
}
3.2.3 为什么 useCallback 是 useMemo 的特例
从源码层面看,两者的核心逻辑完全一致,唯一的区别在于:
js
// useMemo:存储的是执行结果
hook.memoizedState = [nextCreate(), nextDeps]; // 先执行,再存结果
// useCallback:存储的是函数本身
hook.memoizedState = [callback, nextDeps]; // 不执行,直接存引用
这也是为什么官方提供两个独立 API 的原因:
- 语义化 :
useMemo明确表示"缓存一个值",useCallback明确表示"缓存一个函数" - 减少误用 :如果用
useMemo缓存函数,需要写成useMemo(() => fn, deps),容易忘记包裹导致直接缓存执行结果 - 类型约束 :TypeScript 中
useCallback的返回值被强制约束为函数类型
3.2.4 只缓存引用,不缓存执行
这是 useCallback 最容易被误解的一点:
js
const handleClick = useCallback(() => {
console.log('clicked', count); // count 是闭包捕获的值
}, [count]);
useCallback不会执行handleClick,它只是返回一个稳定的函数引用- 只有当你手动调用
handleClick()时,函数体才会执行 - 如果依赖数组写漏了
count,函数引用虽然稳定了,但内部捕获的count永远是旧值(闭包陷阱)
3.2.5 为什么必须配合 React.memo
useCallback 单独使用没有任何性能收益 ,它只是让函数引用不变,但函数引用不变本身不会减少任何计算。只有当这个稳定的引用传递给用 React.memo 包裹的子组件时,才能避免子组件因 props 引用变化而无效重渲染。
markdown
父组件渲染
↓
useCallback 返回同一个函数引用
↓
React.memo 对子组件 props 做浅比较
↓
引用没变 → 跳过子组件渲染 ✅
引用变了 → 子组件重渲染 ❌
💡 关键区别 :useCallback 只缓存函数引用,不缓存执行结果。函数体内部逻辑每次调用时仍然会执行,只是引用地址不变。
3.3 典型使用场景
✅ 场景一:传递给 React.memo 子组件的回调
jsx
// 父组件
function Parent() {
const [count, setCount] = useState(0);
// 不用 useCallback → 每次渲染创建新函数 → Child 重渲染
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // 空依赖,整个生命周期引用不变
return (
<>
<Child onClick={handleClick} />
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
</>
);
}
// 子组件
const Child = React.memo(({ onClick }) => {
console.log('Child rendered'); // 只有 onClick 引用变化时才打印
return <button onClick={onClick}>Click</button>;
});
✅ 场景二:函数作为其他 Hook 的依赖
jsx
const fetchUser = useCallback((id) => {
return api.get(`/users/${id}`);
}, []);
useEffect(() => {
fetchUser(userId); // fetchUser 引用稳定,Effect 只依赖 userId
}, [fetchUser, userId]);
✅ 场景三:高频事件处理
jsx
const handleScroll = useCallback((e) => {
setScrollY(e.target.scrollTop);
}, []);
// 配合节流/防抖时,稳定的引用很重要
useEffect(() => {
const throttled = throttle(handleScroll, 100);
window.addEventListener('scroll', throttled);
return () => window.removeEventListener('scroll', throttled);
}, [handleScroll]);
3.4 ❌ 不该用 useCallback 的场景
jsx
// ❌ 仅组件内部使用、不传递给子组件的函数
const handleClick = useCallback(() => { setCount(c => c + 1); }, []);
// ✅ 直接写即可,没有优化收益
const handleClick = () => { setCount(c => c + 1); };
// ❌ 子组件没有用 React.memo 包裹
// useCallback 单独使用毫无意义,必须配合 memo 才能拦截重渲染
四、useMemo vs useCallback 核心对比
| 维度 | useMemo |
useCallback |
|---|---|---|
| 缓存对象 | 计算结果(值) | 函数引用(方法) |
| 执行时机 | 依赖变化时自动执行计算 | 仅缓存引用,手动调用才执行 |
| 解决的问题 | 避免重复 CPU 计算 | 避免函数引用变更导致子组件无效重渲染 |
| 能否单独使用 | ✅ 可以,直接减少计算量 | ❌ 单独使用无意义,需配合 React.memo |
| 等价关系 | useMemo(() => fn, deps) ≡ useCallback(fn, deps) |
--- |
五、黄金搭档:React.memo + useMemo + useCallback
三者形成完整的缓存链路
plain
React.memo → 拦截组件重渲染(组件级缓存)
useMemo → 稳定数据引用(值级缓存)
useCallback → 稳定函数引用(函数级缓存)
jsx
// 完整示例:带搜索和选择的商品列表
const ProductItem = React.memo(({ product, onSelect }) => {
return (
<li onClick={() => onSelect(product.id)}>
{product.name} - ¥{product.price}
</li>
);
});
function ProductPage({ products, keyword }) {
const [selected, setSelected] = useState(null);
// 1. useMemo 缓存筛选结果,避免每次渲染都遍历
const filtered = useMemo(() => {
const lower = keyword.toLowerCase();
return products.filter(p => p.name.toLowerCase().includes(lower));
}, [products, keyword]);
// 2. useCallback 稳定回调引用,配合 React.memo 生效
const handleSelect = useCallback((id) => {
setSelected(id);
}, []);
return (
<ul>
{filtered.map(p => (
<ProductItem key={p.id} product={p} onSelect={handleSelect} />
))}
</ul>
);
}
六、避坑指南
坑 1:依赖数组写漏 → 拿到过期数据
jsx
// ❌ 忘记把 filter 加入依赖
const filtered = useMemo(() =>
items.filter(i => i.category === filter), [items]);
// ✅ 所有响应式变量都要加入依赖
const filtered = useMemo(() =>
items.filter(i => i.category === filter), [items, filter]);
坑 2:依赖数组写死 [] → 缓存永不更新
jsx
// ❌ 依赖写空数组,total 永远是初始值
const total = useMemo(() => items.reduce((s, i) => s + i.price, 0), []);
坑 3:闭包陷阱 → 函数捕获过期状态
jsx
// ❌ 依赖为空,count 永远是初始值 0
const increment = useCallback(() => {
setCount(count + 1);
}, []);
// ✅ 使用函数式更新
const increment = useCallback(() => {
setCount(prev => prev + 1);
}, []);
坑 4:过度使用 → 反而更慢
useMemo / useCallback 本身有成本:维护缓存占用内存 + 每次渲染做依赖比较。如果计算本身很快(1ms 内),缓存开销反而大于计算开销。
📌 黄金法则:先用 React DevTools Profiler 定位瓶颈,确认存在性能问题后再加记忆化,不要"以防万一"到处套。
七、前瞻:React 19 编译器自动记忆化
React 19 引入了React Compiler (原 React Forget),能够自动对组件中的值和函数进行记忆化处理,大幅减少手动调用 useMemo / useCallback 的需求。
jsx
// React 19 + Compiler 开启后,以下代码会被自动优化
function Component({ items }) {
const total = items.reduce((s, i) => s + i.price, 0); // 自动 memo
const handleClick = () => { console.log('click'); }; // 自动 callback
return <div>{total}</div>;
}
但理解 useMemo 和 useCallback 的底层原理仍然非常重要------编译器并非万能,在复杂场景下你仍需手动干预,且面试和代码审查中这些概念依然是高频考点。
八、总结速查表
| 场景 | 用哪个 |
|---|---|
| 大数据筛选/排序/聚合 | useMemo |
| 复杂数学计算、递归 | useMemo |
| 稳定对象/数组引用传给子组件 | useMemo |
函数传给 React.memo 子组件 |
useCallback |
函数作为 useEffect 等 Hook 的依赖 |
useCallback |
| 简单运算、组件内部自用函数 | 都不用 |
| 不确定是否有性能问题 | 先 Profiler,再决定 |
如果你觉得这篇文章有帮助,欢迎点赞收藏,也欢迎在评论区分享你的使用心得~