1. 为什么 React 中需要缓存?
原因只有一个:
函数组件每次渲染都会重新执行:所有对象、数组、函数、计算全部重新生成。
示例:
typescript
const arr = [1, 2, 3]; // 每次渲染都是新引用
const fn = () => {}; // 新函数引用
const v = heavyCompute(data); // 昂贵计算重新执行
这些导致:
- 子组件不必要渲染
- useEffect 频繁触发
- 重计算导致卡顿
React 提供了 引用级缓存机制 :useMemo 和 useCallback。
2. useMemo 与 useCallback 的本质区别
✔ useMemo ------ 缓存"值"
typescript
const memoValue = useMemo(() => compute(x), [x]);
缓存的不是函数,而是计算结果。
✔ useCallback ------ 缓存"函数引用"
typescript
const memoFn = useCallback(() => doSomething(x), [x]);
本质等价于:
typescript
useMemo(() => () => doSomething(x), [x]);
useCallback 就是 useMemo 的语法糖。
3. 它们真正解决的问题是什么?
🧩 问题一:引用不稳定导致子组件重复渲染
typescript
<Child config={{ a: 1 }} />
config 每次都是新对象 → 子组件必渲染。
🧩 问题二:useEffect 的依赖不断变化 → 无限循环
typescript
useEffect(() => {
api.fetch(filters);
}, [filters]);
filters 是 {} 字面量 → 无限触发。
🧩 问题三:昂贵计算反复执行 → 性能卡顿
比如:
- 列表排序、过滤
- 矩阵运算
- 大规模数据 transform
- Markdown/图表解析
4. useMemo vs useCallback 场景划分(最重要的表)
| 目的 | 应使用 |
|---|---|
| 缓存昂贵计算结果 | useMemo |
| 缓存对象/数组引用 | useMemo |
| 稳定 useEffect 的依赖数组 | useMemo |
| 子组件需要稳定的回调函数 | useCallback |
| 避免回调因重新创建导致子组件渲染 | useCallback |
| 避免事件监听 remove 失败 | useCallback |
| 缓存函数闭包环境 | useCallback |
5. useMemo 的底层运行机制(深度解析)
5.1 React 的依赖比较算法
typescript
function areDepsEqual(nextDeps, prevDeps) {
if (prevDeps === null) return false;
for (let i = 0; i < prevDeps.length; i++) {
if (!Object.is(nextDeps[i], prevDeps[i])) {
return false;
}
}
return true;
}
关键点:
- 使用
Object.is(比===更严格) - 依赖项必须是稳定引用
- 依赖项数组的顺序不能变化
5.2 useMemo 内部生命周期(更新阶段)
伪代码:
typescript
if (areDepsEqual(nextDeps, prevDeps)) {
return prevValue; // 返回缓存
} else {
const nextValue = create();
saveToHook(nextValue, nextDeps);
return nextValue;
}
缓存命中 → 不执行计算
缓存 miss → 执行工厂函数并更新缓存
6. useMemo 的性能特性与基准分析
判断是否需要 useMemo,一个非常实用的标准:
计算耗时 > 1ms → 适合使用 useMemo
大量测试表明:
- useMemo 的缓存开销约为 0.03ms ~ 0.1ms
- 当你的函数执行时间 < 0.1ms 时,useMemo 反而会更慢
6.1 性能测量示例
typescript
const processedData = useMemo(() => {
const start = performance.now();
const result = heavyCompute(data);
const end = performance.now();
console.log(`Compute took ${end - start}ms`);
return result;
}, [data]);
7. 高级模式:工程级 useMemo 组合策略
7.1 分层缓存(Layered Memoization)
typescript
// 1. 过滤
const filtered = useMemo(() => {
return users.filter(u => filters.every(f => f(u)));
}, [users, filters]);
// 2. 排序
const sorted = useMemo(() => {
return [...filtered].sort(sortFn);
}, [filtered, sortFn]);
// 3. 衍生数据
const stats = useMemo(() => {
return { total: sorted.length, avg: avg(sorted) };
}, [sorted]);
这样可以大幅减少重复计算。
7.2 智能缓存(Smart Memo)
typescript
const useSmartMemo = (factory, deps, maxAge = 1000) => {
const cacheRef = useRef(null);
return useMemo(() => {
const now = Date.now();
if (cacheRef.current &&
now - cacheRef.current.timestamp < maxAge &&
areDepsEqual(cacheRef.current.deps, deps)) {
return cacheRef.current.value;
}
const value = factory();
cacheRef.current = { value, deps, timestamp: now };
return value;
}, deps);
};
8. useMemo 依赖管理:常见陷阱与最佳实践
❌ 8.1 依赖项是对象字面量 → 永远变化
示例:
typescript
useMemo(() => compute(v), [{ x: 1 }]);
每次渲染 { x: 1 } 都是新对象 → useMemo 永远失效。
🟢 正确写法:
typescript
const config = useMemo(() => ({ x: 1 }), []);
useMemo(() => compute(v, config), [v, config]);
❌ 8.2 闭包陷阱
typescript
const fn = useMemo(() => () => setCount(count + 1), [count]);
问题:count 是旧值。
🟢 正确:
typescript
const fn = useCallback(() => setCount(c => c + 1), []);
9. useCallback 深度理解与闭包陷阱
闭包会保存旧的变量环境,所以 useCallback 通常用于:
- 保持函数引用稳定
- 保持闭包行为正确(例如函数式更新)
- 配合 React.memo 避免子组件重复渲染
10. 并发模式下 useMemo 的行为
React 在 concurrent mode 下:
- useMemo 可能被打断、多次执行
- 可能在渲染中被"丢弃"
因此 useMemo 不能用于副作用,只能用于纯计算。
高级用法:useDeferredValue + useMemo
typescript
const deferred = useDeferredValue(query);
const results = useMemo(() => search(deferred), [deferred]);
11. 什么时候不该用 useMemo?
❌ 计算量极小
❌ 依赖项频繁变化(缓存命中率低)
❌ 组件每次都一定会重新渲染
❌ 只是为了"看起来优雅"
🟢 应使用 useMemo 的情况:
- 大规模数组处理
- 大量 derived data
- 传递对象给 React.memo 子组件
- 使用 useEffect 依赖且需要稳定引用
12. useMemo/useCallback 使用决策树(工程标准版)
是 否 是 否 是 否 是 否 该计算昂贵吗? 使用 useMemo 该值是否作为 props 传递? 子组件是否 memo 化 该函数是否作为依赖传给 useEffect? 使用 useMemo / useCallback 不需要 memo 使用 useCallback 不需要
13. 总结:一句话记住 useMemo 和 useCallback
1. useMemo 缓存值,为了解决昂贵计算和引用稳定性问题。
2. useCallback 缓存函数引用,为了解决函数传递导致的重复渲染问题。
3. 不要为了"看起来高级"滥用它们------衡量成本和收益才是工程思维。