【图】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),仍需按上述规则谨慎使用。
相关推荐
Hive_MOM4 小时前
制造企业库存精益化管理数字化:从“库存压资金、找货靠人翻“到“库存水位实时可见、资金周转提速“
前端·制造
光影少年4 小时前
react navite调试方案:Flipper、远程调试
前端·javascript·react native·react.js·前端框架
计算机魔术师4 小时前
智谱开源 GLM-5.3 模型权重,主打智能体编程与网络防御
前端
百变梦仔4 小时前
多个 Skill 同时出现时,Codex 前端任务我会先定优先级
前端
IT_陈寒4 小时前
SpringBoot自动配置失效?这个隐式依赖坑了我三天
前端·人工智能·后端
码视野4 小时前
基于 Spring Boot + Vue3 的【城市雨污水管网液位淤积溯源与立交桥下穿隧洞防汛排涝智控中台】设计与实现(含PRD/三端高保真源码/大屏)
java·前端·人工智能·spring boot·后端
雪芽蓝域zzs4 小时前
第五节:Vue‑Router4 路由配置,布局嵌套路由
前端·javascript·vue.js
雪芽蓝域zzs4 小时前
第十二节:完整用户管理 CRUD 页面(表格分页、新增 / 编辑弹窗、删除、Mock 接口)
前端·javascript·vue.js
深念Y5 小时前
NativeScript 移动端开发踩坑记录
前端·ui·vue·安卓·移动端·native·原生
YHHLAI5 小时前
[特殊字符] 从 JSX 到 TypeScript:React 开发的进化之路
javascript·react.js·typescript