useMemo 在 Fiber 上的数据结构与更新机制
这张图展示 useMemo 在首次挂载和后续更新时,如何存储和比较依赖项,决定是否重新执行计算函数。
📝 源码级深度解析(useMemo) :
- 数据结构存储 :
useMemo的 Hook 节点将[value, deps]以数组形式存入memoizedState。这种设计使得在更新阶段可以同时取出值和依赖,进行精确比对。 - 依赖比较算法 :React 使用
Object.is进行浅层逐项比较 (areHookInputsEqual函数)。对于数组或对象,即使内容相同但引用地址变了,依然判定为"变化",触发重新计算。 - 执行时机 :计算函数在 Render 阶段 执行(同步)。如果计算开销极大,会导致组件渲染变慢。因此,
useMemo优化的是"避免重复计算",而非"加快首次渲染"。 - 与
useEffect的区别 :useMemo在 Render 阶段同步执行(不能有副作用),而useEffect在 Commit 阶段异步执行(适合副作用)。
useCallback 在 Fiber 上的数据结构与更新机制
这张图展示 useCallback 如何缓存函数对象,并对比其与 useMemo 在存储内容上的差异。
📝 源码级深度解析(useCallback) :
- 存储内容是函数本身 :
useCallback的memoizedState存储的是[fn, deps],其中fn是开发者传入的函数对象。这与useMemo存储"计算返回值"形成鲜明对比。 useCallback(fn, deps)等价于useMemo(() => fn, deps):在 React 源码中,useCallback确实是useMemo的一种特例实现。updateCallback与updateMemo的核心逻辑完全一致,只是返回值不同(返回函数 vs 返回执行结果)。- 依赖变化时的行为 :当依赖项变化时,
useCallback会创建新的函数对象 (新的内存地址)。这会导致依赖该函数作为props的子组件(即使被React.memo包裹)发生重渲染,因为props的浅层比较认为prevProps.fn !== nextProps.fn。 - 何时使用 :
useCallback的核心价值在于稳定函数引用 ,使得子组件可以安全地依赖props进行性能优化(如React.memo+ 浅比较)。如果函数内部引用了state或props,必须将这些变量加入依赖数组,否则闭包会捕获陈旧值。
useCallback 的闭包陷阱(闭包捕获陈旧值)
这张图剖析为什么 useCallback 中的函数如果依赖项缺失,会长期持有"过期的"状态值。
📝 源码级深度解析(闭包陷阱) :
- 闭包的本质 :JavaScript 函数会捕获其定义时所在作用域的所有变量。
useCallback在首次挂载时创建函数对象,该对象通过闭包持有了当时的count变量(基本类型值,按值捕获)。 - 依赖项的作用 :如果依赖数组中没有包含
count,React 在更新时不会重新创建函数,旧的闭包依然指向旧的count值(0)。这会导致异步调用、事件监听等场景中出现"读取到过期状态"的 Bug。 - 正确做法 :所有在函数内部被引用的
state、props以及派生值,必须 全部加入依赖数组。如果依赖项过于频繁变化导致函数频繁重建,可考虑使用useReducer或useRef来存储不变的回调,或使用 React 19 的useEffectEvent(专门解决此问题)。 - ESLint 规则 :React 官方提供的
exhaustive-depsESLint 规则会检测依赖项是否完整,强烈建议启用,可有效避免上述陷阱。
useMemo 与 useCallback 的对比总结(源码视角)
这张图从返回值、存储内容、典型场景三个维度,直观对比两者的差异。
📝 源码级对比总结:
| 对比维度 | useMemo | useCallback |
|---|---|---|
| 存储内容 | memoizedState 存储 [value, deps],value 是计算函数的返回值 |
memoizedState 存储 [fn, deps],fn 是开发者传入的函数对象 |
| 执行时机 | 首次挂载时立即执行计算函数;依赖变化时重新执行 | 首次挂载时存储函数;依赖变化时重新创建新函数 |
| 返回值 | 返回计算结果(任意类型:数字、对象、数组等) | 返回函数本身(可调用) |
| 等价关系 | 不可相互转换 | useCallback(fn, deps) 等价于 useMemo(() => fn, deps) |
| 主要用途 | 避免重复执行昂贵计算,缓存派生数据 | 稳定函数引用,避免子组件因函数地址变化而重渲染 |
| 依赖项检查 | 使用 Object.is 逐项浅比较 |
使用 Object.is 逐项浅比较 |
| 性能开销 | 需要存储值和依赖,每次渲染都要比较依赖数组 | 同样需要存储函数和依赖,比较开销一致 |
💡 实际开发中的决策指南
- 优先使用
useMemo:当计算成本高(如大数据过滤、复杂的数学运算)且依赖项不常变化时。 - 优先使用
useCallback:当函数作为props传递给React.memo子组件时,或者作为useEffect的依赖项时。 - 不要过度使用 :如果计算非常简单(如
a + b)或函数只在父组件内部使用,使用这些 Hook 反而可能降低性能(因为比较依赖项也需要开销)。 - React 19 的新变化 :React 编译器(React Compiler)将自动记忆化 许多场景,未来可能无需手动使用
useMemo和useCallback。但目前(React 18),仍需按上述规则谨慎使用。