Skip to main content

第 35 章:源码调试方法论——断点、Trace、DevTools(选讲)

选讲。前面 34 章的"动手实验"都在让你打断点。本章把这套能力系统化:怎么拿到能调试的 React、在哪打点、怎么用 DevTools 找到瓶颈。这不是讲 React 本身,是讲你怎么验证这本书讲的一切。

先记住这一句:调试 React 源码三件套:拿到 dev 构建(可断点)、在全书高频函数打点(可观察)、用 DevTools 辅助定位(可量化)。三件套合起来,任何"React 为什么这样"的问题都能落地验证。

一图流:调试工作流

第一步:拿到能调试的 React

前言讲过两条路,这里是实操版:

  1. node_modules 里的 dev 包(最快):react/cjs/react.development.js 这类文件带完整变量名和注释,能打断点。缺点是 reconciler 的核心(react-reconciler)不在 npm 上,看不了 Fiber 内部。
  2. monorepo 源码构建(完整):clone facebook/reactgit checkout v19.2.7,按贡献指南构建 dev 版。产物在 build/node_modules/,替换进 demo 项目,全部 reconciler 源码都能断点。本书所有 <Src> 行号都对着这份源码。

第二步:在哪些函数打点

全书出现频率最高的几个函数,就是你的"断点地图"(都在对应章节验证过行号):

想观察什么断在哪
一次更新从哪开始dispatchSetState(ReactFiberHooks.js:3599)
调度scheduleUpdateOnFiber(ReactFiberWorkLoop.js:916)
每个 fiber 的处理beginWork(ReactFiberBeginWork.js:4091)
算新 stateupdateReducer(ReactFiberHooks.js:1294)
落 DOMcommitMutationEffects(ReactFiberCommitWork.js:1944)
跑副作用commitLayoutEffects(ReactFiberCommitWork.js:2876)

技巧:条件断点beginWork 会被调用几千次,全打太吵。右键断点设条件,比如 workInProgress.tag === 5(只看 DOM 元素),或 workInProgress.type?.name === 'Counter'(只看某个组件)。这样几千次命中里只停下来你要的那几次。

第三步:观察什么

命中断点后,重点看三样:

  1. 调用栈:从栈底往上,能看清"这次 beginWork 是哪个调度链过来的"(第 21 章那条 setState 接力线,栈就是它的活地图)。
  2. 关键变量workInProgress.tag(第 3 章的工种)、currentworkInProgressalternate(双缓冲)、lanes(优先级)。
  3. 跳过 DEV 分支:React 源码大量 __DEV__ 代码,很多和核心逻辑无关。要看清主流程,可以直接在 if (__DEV__) { ... } 之外的代码里打点,或者构建时用 --type=PROD(生产版省掉 DEV 噪音,但变量名会被压缩,调试信息少)。通常"dev 构建 + 跳过 DEV 块"是最佳组合。

第四步:用 DevTools 量化

断点适合"看某一次",DevTools 适合"看整体":

  • Components 面板:选一个组件,右侧就是它的 memoizedProps / memoizedState / Hooks 链表(第 12 章那个链表直接可视化)。看 fiber 长什么样,这里最直观。
  • Profiler 火焰图:每帧谁在渲染、渲染多久。定位"哪个组件是热点",再用断点去看它的 beginWork 为什么没 bailout(第 24 章的三条件,对着火焰图找违反者)。

常见坑

  • 生产包没调试信息reactproduction.js 是压缩+混淆的,别在那上面断点。
  • 多份 React 共存:项目里装了两份 react(版本/实例不同),DevTools 会报"doesn't match",也会让 <Src> 行号对不上。用 npm ls react 排查。
  • 把 dev 构建当生产:dev 版有 StrictMode 双调用、大量警告,性能数字不能当基准。基准用生产,调试用 dev。

动手实验

  1. 搭调试环境:按前言 clone react@v19.2.7 构建 dev 版,替换进一个最小 demo,确认能对 beginWork 断点。
  2. 条件断点:在 beginWork 设条件 workInProgress.type?.name === 'Counter',点按钮,确认只对 Counter 停下。
  3. 火焰图定位热点:Profiler 记录一次慢渲染,找最粗的柱,回去用第 24 章的三条件检查它为什么没 bailout。

小结

  • 三件套:dev 构建 + 高频函数断点 + DevTools 量化
  • 断点地图:dispatchSetState / scheduleUpdateOnFiber / beginWork / updateReducer / commitMutationEffects
  • 条件断点把几千次命中筛成几次,只看你关心的。
  • 调用栈是第 21 章那条接力线的活地图。
  • DevTools 看整体(火焰图定位热点),断点看局部(为什么没 bailout)。
  • 下一步(选讲):DevTools 自己是怎么实现的,第 36 章。