【图】React源码解析-从源码数据结构、执行机制对比、闭包陷阱与最佳实践四个维度,深挖useMemo和useCallback的底层原理

useMemo 在 Fiber 上的数据结构与更新机制

这张图展示 useMemo 在首次挂载和后续更新时,如何存储和比较依赖项,决定是否重新执行计算函数。

📝 源码级深度解析(useMemo)

  1. 数据结构存储useMemo 的 Hook 节点将 [value, deps] 以数组形式存入 memoizedState。这种设计使得在更新阶段可以同时取出值和依赖,进行精确比对。
  2. 依赖比较算法 :React 使用 Object.is 进行浅层逐项比较areHookInputsEqual 函数)。对于数组或对象,即使内容相同但引用地址变了,依然判定为"变化",触发重新计算。
  3. 执行时机 :计算函数在 Render 阶段 执行(同步)。如果计算开销极大,会导致组件渲染变慢。因此,useMemo 优化的是"避免重复计算",而非"加快首次渲染"。
  4. useEffect 的区别useMemo 在 Render 阶段同步执行(不能有副作用),而 useEffect 在 Commit 阶段异步执行(适合副作用)。

useCallback 在 Fiber 上的数据结构与更新机制

这张图展示 useCallback 如何缓存函数对象,并对比其与 useMemo 在存储内容上的差异。

📝 源码级深度解析(useCallback)

  1. 存储内容是函数本身useCallbackmemoizedState 存储的是 [fn, deps],其中 fn 是开发者传入的函数对象。这与 useMemo 存储"计算返回值"形成鲜明对比。
  2. useCallback(fn, deps) 等价于 useMemo(() => fn, deps) :在 React 源码中,useCallback 确实是 useMemo 的一种特例实现。updateCallbackupdateMemo 的核心逻辑完全一致,只是返回值不同(返回函数 vs 返回执行结果)。
  3. 依赖变化时的行为 :当依赖项变化时,useCallback创建新的函数对象 (新的内存地址)。这会导致依赖该函数作为 props 的子组件(即使被 React.memo 包裹)发生重渲染,因为 props 的浅层比较认为 prevProps.fn !== nextProps.fn
  4. 何时使用useCallback 的核心价值在于稳定函数引用 ,使得子组件可以安全地依赖 props 进行性能优化(如 React.memo + 浅比较)。如果函数内部引用了 stateprops,必须将这些变量加入依赖数组,否则闭包会捕获陈旧值。

useCallback 的闭包陷阱(闭包捕获陈旧值)

这张图剖析为什么 useCallback 中的函数如果依赖项缺失,会长期持有"过期的"状态值。

📝 源码级深度解析(闭包陷阱)

  1. 闭包的本质 :JavaScript 函数会捕获其定义时所在作用域的所有变量。useCallback 在首次挂载时创建函数对象,该对象通过闭包持有了当时的 count 变量(基本类型值,按值捕获)。
  2. 依赖项的作用 :如果依赖数组中没有包含 count,React 在更新时不会重新创建函数,旧的闭包依然指向旧的 count 值(0)。这会导致异步调用、事件监听等场景中出现"读取到过期状态"的 Bug。
  3. 正确做法 :所有在函数内部被引用的 stateprops 以及派生值,必须 全部加入依赖数组。如果依赖项过于频繁变化导致函数频繁重建,可考虑使用 useReduceruseRef 来存储不变的回调,或使用 React 19 的 useEffectEvent(专门解决此问题)。
  4. ESLint 规则 :React 官方提供的 exhaustive-deps ESLint 规则会检测依赖项是否完整,强烈建议启用,可有效避免上述陷阱。

useMemouseCallback 的对比总结(源码视角)

这张图从返回值、存储内容、典型场景三个维度,直观对比两者的差异。

📝 源码级对比总结

对比维度 useMemo useCallback
存储内容 memoizedState 存储 [value, deps]value 是计算函数的返回值 memoizedState 存储 [fn, deps]fn 是开发者传入的函数对象
执行时机 首次挂载时立即执行计算函数;依赖变化时重新执行 首次挂载时存储函数;依赖变化时重新创建新函数
返回值 返回计算结果(任意类型:数字、对象、数组等) 返回函数本身(可调用)
等价关系 不可相互转换 useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
主要用途 避免重复执行昂贵计算,缓存派生数据 稳定函数引用,避免子组件因函数地址变化而重渲染
依赖项检查 使用 Object.is 逐项浅比较 使用 Object.is 逐项浅比较
性能开销 需要存储值和依赖,每次渲染都要比较依赖数组 同样需要存储函数和依赖,比较开销一致

💡 实际开发中的决策指南

  1. 优先使用 useMemo:当计算成本高(如大数据过滤、复杂的数学运算)且依赖项不常变化时。
  2. 优先使用 useCallback :当函数作为 props 传递给 React.memo 子组件时,或者作为 useEffect 的依赖项时。
  3. 不要过度使用 :如果计算非常简单(如 a + b)或函数只在父组件内部使用,使用这些 Hook 反而可能降低性能(因为比较依赖项也需要开销)。
  4. React 19 的新变化 :React 编译器(React Compiler)将自动记忆化 许多场景,未来可能无需手动使用 useMemouseCallback。但目前(React 18),仍需按上述规则谨慎使用。
相关推荐
不一样的少年_18 小时前
不用 LangChain,手搓 AI Agent:给大模型装上“手”,让它自己读项目文件
前端·人工智能·agent
索西引擎18 小时前
【React】useState 函数式更新机制:闭包陷阱规避与状态一致性保障分析
前端·react.js·前端框架
anyup18 小时前
从文本到具身:我给 AI Agent 搭了套实时 3D 交互身体
前端·人工智能·aigc
lichenyang45319 小时前
给弱模型一本说明书:我把文档规范做成单文件 Skill,也终于分清了 Skill 和 MCP
前端
用户2986985301419 小时前
前端实战:在 React 中使用 JavaScript 一键导出 Excel 图表与形状为图片
javascript·react.js·excel
咖啡无伴侣19 小时前
Vue3 DOM 异步更新与性能优化全套学习笔记
前端·vue.js
为了摸鱼而战19 小时前
OpenSpec + Superpowers + gstack 三器合一:你的 AI 编程终于可以从"拍脑袋"进化到"流水线"了
前端
程序猿乐锅19 小时前
【苍穹外卖 day11|统计报表接口与 Apache ECharts 图表展示】
前端·apache·echarts
yy403319 小时前
【HarmonyOS学习笔记】2026-07-19 | 布局性能实验:百分比vs固定值vs预计算
前端·harmonyos