useMemo 和 useCallback 是 React 面试中非常常见的一对“兄弟 Hook”, 很多面试官会问:
“useMemo 和 useCallback 有什么区别?” “为什么要用它们?底层实现原理是什么?” “它们在 Fiber 里是怎么保存和判断依赖变化的?”
下面我帮你分成三个层次来讲解,从面试高分回答到源码级理解 👇
⸻
🧩 一、面试简答版(2 分钟高分回答)
useMemo 和 useCallback 都是 React 提供的性能优化 Hook, 用来 缓存函数组件每次渲染时的计算结果或函数引用,防止不必要的重新渲染。
Hook 作用 返回值 使用场景 useMemo(fn, deps) 缓存计算结果 返回计算结果 计算开销大(如复杂计算、过滤、排序) useCallback(fn, deps) 缓存函数引用 返回函数本身 传递给子组件的回调函数,避免子组件重复渲染
一句话区分:
✅ useMemo 返回值是一个计算结果, ✅ useCallback 返回值是一个函数。
它们内部原理几乎一样,只是 useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。
⸻
💡 二、原理理解(中高级面试)
1️⃣ 内部实现原理概述
在源码层面(ReactFiberHooks.js): • 每次组件渲染时,都会执行函数组件,从而执行到 useMemo 或 useCallback • React 内部会通过 updateMemo 或 updateCallback 来判断依赖是否变化 • 比较依赖时使用 areHookInputsEqual(nextDeps, prevDeps) • 如果依赖未变,直接复用上次缓存的值 • 如果依赖变了,就重新计算 fn(),并缓存新的结果和依赖
⸻
2️⃣ 源码级伪实现(简化理解)
🔹 useMemo
function useMemo(create, deps) { const hook = updateWorkInProgressHook(); // 获取当前 hook 节点 const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState;
// 比较依赖 if (prevState !== null && nextDeps !== null) { const [prevValue, prevDeps] = prevState; if (areHookInputsEqual(nextDeps, prevDeps)) { // ✅ 依赖未变,直接复用旧值 return prevValue; } }
// 依赖变了或初次渲染 → 重新计算 const nextValue = create(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; }
🔹 useCallback
其实就是简化版的 useMemo:
function useCallback(callback, deps) { return useMemo(() => callback, deps); }
👉 所以 useCallback(fn, deps) 只是让 useMemo 返回函数本身而已。
⸻
3️⃣ React 内部怎么存储这些值?
在 Fiber 架构中,每个函数组件都有一个 hook 链表(通过 memoizedState 串联)。
结构大致如下:
fiber.memoizedState (Hook链表) ├─ useState → { memoizedState: currentState } ├─ useMemo → { memoizedState: [cachedValue, deps] } └─ useCallback → { memoizedState: [callbackFn, deps] }
React 每次渲染时会: • 从旧 fiber 的 hook 链表里取出上一次的 hook(prevHook) • 进行依赖比较 • 决定是否复用 memoizedState
这就是为什么 Hook 调用顺序不能变(否则链表错位)。
⸻
⚙️ 三、使用示例与区别
🔸 useMemo 示例
const expensiveValue = useMemo(() => { return heavyCalculation(a, b); }, [a, b]);
👉 当 a 或 b 不变时,不会重新计算,返回上次缓存的结果。 适用于: • 计算量大 • 子组件依赖计算结果作为 props
⸻
🔸 useCallback 示例
const handleClick = useCallback(() => { setCount(c => c + 1); }, []);
// 传给子组件 return ;
👉 确保每次渲染时 handleClick 的函数引用相同, 防止子组件因为 props 变化而重新渲染。
⸻
🔍 四、依赖比较机制
React 使用一个函数 areHookInputsEqual 来比较依赖数组:
function areHookInputsEqual(nextDeps, prevDeps) { if (prevDeps === null || nextDeps.length !== prevDeps.length) return false; for (let i = 0; i < nextDeps.length; i++) { if (!Object.is(nextDeps[i], prevDeps[i])) return false; } return true; }
• 使用 Object.is(比 === 更精确)
• 一旦某个依赖项不相等,就认为依赖变了
⸻
🧠 五、面试加分点
你可以补充一句(很多人不会说这点):
在 React 18 之后,useMemo 和 useCallback 的缓存是基于 Fiber 的 hook 链表存储的, 它们不会跨渲染持久化到内存之外; 一旦组件卸载或重新挂载,缓存全部失效。
所以 useMemo / useCallback 并非通用缓存工具,而是用于避免渲染期重复计算和引用变化的优化手段。
⸻
✅ 面试总结回答模板(背这段即可):
useMemo 和 useCallback 都是 React 的性能优化 Hook, 前者缓存计算结果,后者缓存函数引用。 它们内部通过 Fiber 的 hook 链表存储上次的值与依赖数组, 在下一次 render 阶段时比较新旧依赖(areHookInputsEqual)。 如果依赖未变化,直接复用上一次的结果;否则重新计算并缓存。
useCallback(fn, deps) 本质等价于 useMemo(() => fn, deps)。
⸻