Skip to content

附录A 源码调试技巧

本附录提供React源码调试的实用技巧,帮助你更高效地理解和分析React的内部实现。

在阅读React源码的过程中,静态分析代码只能让我们理解"是什么",而动态调试能帮助我们理解"为什么"和"何时"。本附录将介绍客户端代码调试、服务端代码调试以及Flight协议调试的具体方法和技巧。


A.1 客户端代码调试

客户端代码调试主要针对在浏览器中运行的React代码,包括组件渲染、状态更新、事件处理等。

A.1.1 使用浏览器开发者工具

基础调试流程

  1. 打开开发者工具:在浏览器中按F12或右键选择"检查"
  2. 定位源码文件:在Sources面板中找到React源码文件
  3. 设置断点:点击行号设置断点
  4. 触发代码执行:与页面交互触发断点
  5. 单步调试:使用F10(单步跳过)、F11(单步进入)、Shift+F11(单步跳出)

Source Map配置

为了调试未压缩的源码,需要确保Source Map正确配置。在开发环境中,React会自动生成Source Map。如果使用自定义构建,需要在webpack配置中启用:

javascript
// webpack.config.js
module.exports = {
  mode: 'development',
  devtool: 'source-map', // 或 'inline-source-map'
  // ...
};

A.1.2 VSCode调试配置

VSCode提供了强大的JavaScript调试功能,可以直接在编辑器中设置断点和查看变量。

配置launch.json

在项目根目录创建.vscode/launch.json

json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "调试React应用",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}/src",
      "sourceMaps": true,
      "sourceMapPathOverrides": {
        "webpack:///src/*": "${webRoot}/*",
        "webpack:///./src/*": "${webRoot}/*"
      }
    },
    {
      "type": "chrome",
      "request": "attach",
      "name": "附加到Chrome",
      "port": 9222,
      "webRoot": "${workspaceFolder}/src"
    }
  ]
}

调试React源码

如果要调试React库本身的源码,需要配置source map路径映射:

json
{
  "type": "chrome",
  "request": "launch",
  "name": "调试React源码",
  "url": "http://localhost:3000",
  "webRoot": "${workspaceFolder}",
  "sourceMaps": true,
  "sourceMapPathOverrides": {
    "webpack:///../../node_modules/react/*": "${workspaceFolder}/node_modules/react/*",
    "webpack:///../../node_modules/react-dom/*": "${workspaceFolder}/node_modules/react-dom/*"
  }
}

A.1.3 条件断点和日志断点

条件断点

条件断点只在满足特定条件时才会中断执行,这在循环或频繁调用的函数中非常有用。

在Chrome DevTools中:

  1. 右键点击断点
  2. 选择"Edit breakpoint"
  3. 输入条件表达式

常用条件示例:

javascript
// 只在处理特定组件时中断
workInProgress.type.name === 'MyComponent'

// 只在特定状态值时中断
count > 10

// 只在特定更新优先级时中断
(lane & SyncLane) !== NoLane

日志断点(Logpoints)

日志断点不会中断执行,只会在控制台输出信息。在Chrome DevTools中:

  1. 右键点击行号
  2. 选择"Add logpoint"
  3. 输入要输出的表达式

示例:

javascript
// 输出当前Fiber节点信息
'Processing Fiber:', workInProgress.tag, workInProgress.type

// 输出更新队列
'Update queue:', fiber.updateQueue

// 输出Hooks链表
'Hooks:', fiber.memoizedState

A.1.4 调试Hooks执行

Hooks的执行流程比较复杂,涉及mount和update两个阶段。以下是调试Hooks的关键位置:

useState调试

javascript
// 文件:packages/react-reconciler/src/ReactFiberHooks.js

// 在mountState设置断点,观察初始化过程
function mountState(initialState) {
  const hook = mountWorkInProgressHook();
  // 断点:查看hook对象的创建
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  // 断点:查看queue的创建
  const queue = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
  };
  hook.queue = queue;
  return [hook.memoizedState, dispatch];
}

// 在updateState设置断点,观察更新过程
function updateState(initialState) {
  return updateReducer(basicStateReducer, initialState);
}

// 在dispatchSetState设置断点,观察状态更新触发
function dispatchSetState(fiber, queue, action) {
  // 断点:查看更新对象的创建
  const update = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: null,
  };
  // 断点:查看更新如何加入队列
}

useEffect调试

javascript
// 在mountEffect设置断点
function mountEffect(create, deps) {
  return mountEffectImpl(
    PassiveEffect | PassiveStaticEffect,
    HookPassive,
    create,
    deps,
  );
}

// 在commitPassiveEffects设置断点,观察effect执行
function commitPassiveEffects(root, finishedWork) {
  // 断点:查看effect链表的遍历
}

A.1.5 调试Fiber树结构

Fiber树是React的核心数据结构,理解其结构对于调试至关重要。

查看Fiber树

在控制台中,可以通过DOM节点访问Fiber:

javascript
// 获取DOM节点对应的Fiber
const domNode = document.getElementById('root');
const fiberNode = domNode._reactRootContainer._internalRoot.current;

// 遍历Fiber树
function printFiberTree(fiber, indent = 0) {
  if (!fiber) return;
  
  console.log(
    ' '.repeat(indent),
    fiber.tag,
    fiber.type?.name || fiber.type,
    fiber.key
  );
  
  // 遍历子节点
  let child = fiber.child;
  while (child) {
    printFiberTree(child, indent + 2);
    child = child.sibling;
  }
}

printFiberTree(fiberNode);

使用React DevTools

React DevTools提供了可视化的Fiber树查看功能:

  1. 安装React DevTools浏览器扩展
  2. 打开DevTools的"Components"面板
  3. 选择组件查看其props、state、hooks
  4. 在"Profiler"面板中分析渲染性能

关键Fiber属性

调试时需要关注的Fiber属性:

javascript
{
  tag: 0,                    // WorkTag类型
  type: FunctionComponent,   // 组件类型
  key: null,                 // React key
  stateNode: null,           // 对应的DOM节点或实例
  
  return: parentFiber,       // 父Fiber
  child: firstChildFiber,    // 第一个子Fiber
  sibling: nextSiblingFiber, // 下一个兄弟Fiber
  
  memoizedState: null,       // 当前状态(Hooks链表)
  memoizedProps: {},         // 当前props
  pendingProps: {},          // 新的props
  
  updateQueue: null,         // 更新队列
  
  flags: 0,                  // 副作用标记
  subtreeFlags: 0,           // 子树副作用标记
  
  alternate: null,           // 对应的另一棵树的Fiber
  lanes: 0,                  // 优先级
}

A.1.6 调试更新流程

React的更新流程从触发更新到最终渲染经历多个阶段,以下是关键调试点:

1. 触发更新

javascript
// 在dispatchSetState设置断点
// 文件:packages/react-reconciler/src/ReactFiberHooks.js
function dispatchSetState(fiber, queue, action) {
  // 断点:查看是哪个组件触发了更新
  const lane = requestUpdateLane(fiber);
  const update = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: null,
  };
  // 断点:查看更新对象
  enqueueUpdate(fiber, queue, update, lane);
  scheduleUpdateOnFiber(fiber, lane);
}

2. 调度更新

javascript
// 在scheduleUpdateOnFiber设置断点
// 文件:packages/react-reconciler/src/ReactFiberWorkLoop.js
function scheduleUpdateOnFiber(fiber, lane) {
  // 断点:查看更新如何被调度
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  ensureRootIsScheduled(root);
}

3. 渲染阶段

javascript
// 在workLoopConcurrent设置断点
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    // 断点:查看每个Fiber的处理
    performUnitOfWork(workInProgress);
  }
}

// 在beginWork设置断点
function beginWork(current, workInProgress, renderLanes) {
  // 断点:查看组件如何被处理
  switch (workInProgress.tag) {
    case FunctionComponent:
      return updateFunctionComponent(current, workInProgress, renderLanes);
    // ...
  }
}

4. 提交阶段

javascript
// 在commitRoot设置断点
function commitRoot(root) {
  // 断点:查看DOM更新的提交
  commitBeforeMutationEffects(root, finishedWork);
  commitMutationEffects(root, finishedWork);
  commitLayoutEffects(finishedWork, root);
}

A.1.7 性能分析

使用React DevTools Profiler

  1. 打开React DevTools的"Profiler"面板
  2. 点击录制按钮
  3. 与应用交互
  4. 停止录制
  5. 查看火焰图和排序图

关键指标

  • Render duration:组件渲染耗时
  • Commit duration:提交阶段耗时
  • Interactions:用户交互追踪

使用Chrome Performance面板

  1. 打开Chrome DevTools的"Performance"面板
  2. 点击录制按钮
  3. 与应用交互
  4. 停止录制
  5. 分析Main线程的活动

关键信息

  • Long tasks:超过50ms的任务
  • Layout shifts:布局偏移
  • Paint events:绘制事件

A.1.8 常见调试场景

场景1:组件为什么重新渲染?

在组件函数开头添加日志:

javascript
function MyComponent(props) {
  console.log('MyComponent render', props);
  // 或使用useEffect追踪
  useEffect(() => {
    console.log('Props changed:', props);
  });
  
  return <div>...</div>;
}

使用React DevTools的"Highlight updates"功能可以可视化组件更新。

场景2:状态更新为什么没有生效?

检查以下几点:

  1. 是否直接修改了状态对象(应该使用不可变更新)
  2. 是否在异步回调中使用了过时的状态(使用函数式更新)
  3. 是否在渲染期间调用了setState(会被忽略)

dispatchSetState设置断点,查看更新是否被正确触发。

场景3:为什么出现无限循环?

常见原因:

  1. useEffect依赖项包含每次都变化的对象
  2. 在渲染期间触发状态更新
  3. useEffect中触发状态更新,而状态又在依赖项中

scheduleUpdateOnFiber设置条件断点,统计调用次数:

javascript
// 条件断点
(window.updateCount = (window.updateCount || 0) + 1) > 100

A.2 服务端代码调试

服务端代码调试主要针对在Node.js或Edge Runtime中运行的React代码,包括SSR、RSC渲染等。

A.2.1 Node.js调试配置

使用VSCode调试Node.js

.vscode/launch.json中添加Node.js调试配置:

json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "调试SSR服务器",
      "program": "${workspaceFolder}/server.js",
      "skipFiles": ["<node_internals>/**"],
      "sourceMaps": true,
      "outFiles": ["${workspaceFolder}/dist/**/*.js"],
      "env": {
        "NODE_ENV": "development"
      }
    },
    {
      "type": "node",
      "request": "attach",
      "name": "附加到Node进程",
      "port": 9229,
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}

使用命令行调试

bash
# 启动调试模式
node --inspect server.js

# 或指定调试端口
node --inspect=9229 server.js

# 在第一行代码处暂停
node --inspect-brk server.js

然后在Chrome中访问chrome://inspect,点击"inspect"连接到Node进程。

A.2.2 调试流式SSR(Fizz)

流式SSR的核心实现在react-server包中,以下是关键调试点:

renderToPipeableStream入口

javascript
// 文件:packages/react-server/src/ReactFizzServer.js

export function renderToPipeableStream(
  children,
  options
) {
  // 断点:查看渲染选项
  const request = createRequest(
    children,
    createResponseState(options),
    createRootFormatContext(options),
    options
  );
  
  // 断点:查看Request对象的创建
  startWork(request);
  
  return {
    pipe(destination) {
      // 断点:查看流的输出
      startFlowing(request, destination);
    },
    abort(reason) {
      abort(request, reason);
    }
  };
}

Task执行流程

javascript
// 在performWork设置断点
function performWork(request) {
  // 断点:查看任务队列
  const pingedTasks = request.pingedTasks;
  let i;
  for (i = 0; i < pingedTasks.length; i++) {
    const task = pingedTasks[i];
    // 断点:查看每个Task的处理
    retryTask(request, task);
  }
  pingedTasks.splice(0, i);
  
  if (request.destination !== null) {
    // 断点:查看输出流
    flushCompletedQueues(request, request.destination);
  }
}

Segment渲染

javascript
// 在renderElement设置断点
function renderElement(
  request,
  task,
  type,
  props,
  ref
) {
  // 断点:查看元素类型
  if (typeof type === 'function') {
    if (shouldConstruct(type)) {
      // 类组件
      renderClassComponent(request, task, type, props);
    } else {
      // 函数组件
      renderFunctionComponent(request, task, type, props);
    }
  } else if (typeof type === 'string') {
    // 断点:查看HTML标签的渲染
    renderHostElement(request, task, type, props);
  }
}

Suspense处理

javascript
// 在renderSuspenseBoundary设置断点
function renderSuspenseBoundary(
  request,
  task,
  props
) {
  // 断点:查看Suspense边界的创建
  const fallback = props.fallback;
  const children = props.children;
  
  const newSegment = createSegment(request);
  // 断点:查看Segment的创建
  
  const newTask = createTask(
    request,
    children,
    task.blockedBoundary,
    newSegment,
    task.abortSet,
    task.legacyContext,
    task.context,
    task.treeContext
  );
  
  // 断点:查看Task的调度
  request.pingedTasks.push(newTask);
}

A.2.3 调试React Server Components

RSC的调试涉及服务端序列化和客户端解析两个部分。

服务端序列化调试

javascript
// 文件:packages/react-server/src/ReactFlightServer.js

// 在renderToReadableStream设置断点
export function renderToReadableStream(
  model,
  webpackMap,
  options
) {
  // 断点:查看初始model
  const request = createRequest(
    model,
    webpackMap,
    options ? options.onError : undefined,
    options ? options.context : undefined,
    options ? options.identifierPrefix : undefined
  );
  
  // 断点:查看Request对象
  startWork(request);
  return request.destination;
}

// 在resolveModelToJSON设置断点
function resolveModelToJSON(
  request,
  parent,
  key,
  value
) {
  // 断点:查看值的类型判断
  if (typeof value === 'object' && value !== null) {
    if (isClientReference(value)) {
      // 断点:客户端组件引用
      return serializeClientReference(request, parent, key, value);
    } else if (isServerReference(value)) {
      // 断点:Server Action引用
      return serializeServerReference(request, parent, key, value);
    }
  }
  
  if (typeof value === 'function') {
    // 断点:函数序列化
    if (isClientReference(value)) {
      return serializeClientReference(request, parent, key, value);
    }
    throw new Error('Functions cannot be passed to Client Components');
  }
  
  return value;
}

组件渲染追踪

javascript
// 在renderElement设置断点
function renderElement(
  request,
  task,
  type,
  props,
  ref
) {
  // 断点:查看组件类型
  if (typeof type === 'function') {
    if (isClientReference(type)) {
      // 断点:客户端组件
      return renderClientComponent(request, task, type, props);
    } else {
      // 断点:服务端组件
      return renderServerComponent(request, task, type, props);
    }
  }
}

// 在renderServerComponent设置断点
function renderServerComponent(
  request,
  task,
  Component,
  props
) {
  // 断点:查看服务端组件的执行
  const result = Component(props);
  
  // 断点:查看async组件的处理
  if (typeof result === 'object' && result !== null && typeof result.then === 'function') {
    // Promise处理
    result.then(
      resolvedResult => {
        // 断点:查看resolved值
        renderModelDestructive(request, task, emptyRoot, '', resolvedResult);
      },
      error => {
        // 断点:查看错误处理
        logRecoverableError(request, error);
      }
    );
  } else {
    // 断点:查看同步结果
    renderModelDestructive(request, task, emptyRoot, '', result);
  }
}

A.2.4 调试Server Actions

Server Actions的调试需要追踪从客户端调用到服务端执行的完整流程。

服务端Action处理

javascript
// 在decodeAction设置断点
async function decodeAction(body, serverManifest) {
  // 断点:查看请求体解析
  const formData = await body.formData();
  
  const action = formData.get('action');
  // 断点:查看action ID
  
  const actionId = JSON.parse(action);
  const actionModule = serverManifest[actionId.id];
  
  // 断点:查看action模块的加载
  const actionFunction = actionModule[actionId.name];
  
  // 断点:查看参数解析
  const args = await decodeFormData(formData, serverManifest);
  
  // 断点:查看action执行
  return actionFunction.apply(null, args);
}

Action调用追踪

在服务端action函数中添加日志:

javascript
// actions.js
"use server";

export async function createPost(formData) {
  console.log('Server Action called:', {
    title: formData.get('title'),
    content: formData.get('content')
  });
  
  // 设置断点,查看执行上下文
  const result = await db.insert('posts', {
    title: formData.get('title'),
    content: formData.get('content')
  });
  
  console.log('Action result:', result);
  return result;
}

A.2.5 调试Hydration

Hydration是服务端渲染和客户端接管的关键环节。

hydrateRoot调试

javascript
// 文件:packages/react-dom/src/client/ReactDOMRoot.js

export function hydrateRoot(
  container,
  initialChildren,
  options
) {
  // 断点:查看hydration配置
  const root = createHydrationContainer(
    initialChildren,
    null,
    container,
    ConcurrentRoot,
    options
  );
  
  // 断点:查看root创建
  const rootFiber = root.current;
  
  // 断点:查看hydration开始
  updateContainer(initialChildren, root, null, null);
  
  return new ReactDOMHydrationRoot(root);
}

Hydration不匹配检测

javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMHostConfig.js

function hydrateTextInstance(
  textInstance,
  text,
  internalInstanceHandle
) {
  // 断点:查看文本节点hydration
  const textNode = textInstance;
  const textContent = textNode.textContent;
  
  if (textContent !== text) {
    // 断点:检测到不匹配
    if (__DEV__) {
      warnForTextDifference(textContent, text);
    }
    return true; // 需要更新
  }
  
  return false;
}

function hydrateInstance(
  instance,
  type,
  props,
  rootContainerInstance,
  hostContext,
  internalInstanceHandle
) {
  // 断点:查看元素节点hydration
  const domElement = instance;
  
  // 断点:查看属性对比
  updateFiberProps(domElement, props);
  
  return diffHydratedProperties(
    domElement,
    type,
    props,
    hostContext
  );
}

A.2.6 日志和追踪

添加自定义日志

在React源码中添加日志可以帮助理解执行流程:

javascript
// 在关键位置添加日志
function performWork(request) {
  console.log('[Fizz] performWork', {
    pingedTasks: request.pingedTasks.length,
    destination: !!request.destination
  });
  
  // 原有代码...
}

使用环境变量控制日志

javascript
const DEBUG = process.env.DEBUG_REACT === 'true';

function debugLog(...args) {
  if (DEBUG) {
    console.log('[React Debug]', ...args);
  }
}

// 使用
debugLog('Rendering component:', type.name);

追踪异步操作

使用async_hooks追踪异步操作:

javascript
const async_hooks = require('async_hooks');
const fs = require('fs');

const fd = fs.openSync('async-trace.log', 'w');

const hook = async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    fs.writeSync(fd, `Init: ${type}(${asyncId}) triggered by ${triggerAsyncId}\n`);
  },
  before(asyncId) {
    fs.writeSync(fd, `Before: ${asyncId}\n`);
  },
  after(asyncId) {
    fs.writeSync(fd, `After: ${asyncId}\n`);
  },
  destroy(asyncId) {
    fs.writeSync(fd, `Destroy: ${asyncId}\n`);
  }
});

hook.enable();

A.2.7 常见服务端调试场景

场景1:SSR渲染卡住

检查以下几点:

  1. 是否有未resolve的Promise
  2. Suspense边界是否正确配置
  3. 是否有死循环

performWork设置断点,查看任务队列是否一直增长。

场景2:Hydration不匹配

常见原因:

  1. 服务端和客户端渲染逻辑不一致
  2. 使用了浏览器特定API(如windowlocalStorage
  3. 随机数或时间戳导致内容不同

hydrateInstance设置断点,查看具体哪个节点不匹配。

场景3:Server Action调用失败

检查:

  1. action函数是否正确标记"use server"
  2. 参数序列化是否正确
  3. 服务端是否正确处理POST请求

decodeAction设置断点,查看请求解析过程。


A.3 Flight协议调试

Flight协议是React Server Components的通信协议,调试Flight协议可以帮助理解RSC的数据传输机制。

A.3.1 Flight数据格式解析

Flight协议使用基于行的文本格式,每行代表一个chunk。

基本格式

<id>:<type> <data>
  • id:chunk的唯一标识符
  • type:chunk类型(J=JSON, M=Module, S=Symbol, E=Error等)
  • data:chunk的数据内容

示例Flight数据

0:["$","div",null,{"children":["$","h1",null,{"children":"Hello"}]}]
M1:{"id":"./src/Button.js","chunks":["client1"],"name":"Button"}
J2:{"title":"My Post","content":"..."}
S3:"Symbol(react.element)"
E4:{"message":"Error occurred","stack":"..."}

A.3.2 拦截和查看Flight数据

在浏览器中拦截

使用Chrome DevTools的Network面板:

  1. 打开Network面板
  2. 筛选XHR/Fetch请求
  3. 查找RSC请求(通常是?_rsc=xxx
  4. 在Response标签中查看Flight数据

使用代理工具

使用Charles或Fiddler等代理工具可以拦截和修改Flight数据:

javascript
// Charles Rewrite规则示例
// 将Flight响应保存到文件
Match: URL contains "_rsc"
Action: Save Response Body to File

在Node.js中拦截

在服务端添加中间件记录Flight数据:

javascript
// middleware.js
export function flightLogger(req, res, next) {
  if (req.url.includes('_rsc')) {
    const originalWrite = res.write;
    const originalEnd = res.end;
    const chunks = [];
    
    res.write = function(chunk) {
      chunks.push(Buffer.from(chunk));
      return originalWrite.apply(res, arguments);
    };
    
    res.end = function(chunk) {
      if (chunk) {
        chunks.push(Buffer.from(chunk));
      }
      const body = Buffer.concat(chunks).toString('utf8');
      console.log('[Flight Response]', body);
      fs.writeFileSync('flight-response.txt', body);
      return originalEnd.apply(res, arguments);
    };
  }
  next();
}

A.3.3 调试Flight序列化

服务端序列化调试

javascript
// 文件:packages/react-server/src/ReactFlightServer.js

// 在emitChunk设置断点
function emitChunk(request, task, value) {
  // 断点:查看chunk的生成
  const id = task.id;
  const json = stringify(value);
  
  // 断点:查看序列化结果
  const row = id.toString(16) + ':' + json + '\n';
  
  // 断点:查看输出
  request.destination.enqueue(textEncoder.encode(row));
}

// 在serializeClientReference设置断点
function serializeClientReference(
  request,
  parent,
  key,
  clientReference
) {
  // 断点:查看客户端引用的序列化
  const moduleId = getModuleId(clientReference);
  const exportName = getExportName(clientReference);
  
  // 断点:查看Module chunk的创建
  return serializeByValueID(
    request,
    {
      $$typeof: CLIENT_REFERENCE_TAG,
      id: moduleId,
      name: exportName
    }
  );
}

// 在serializeServerReference设置断点
function serializeServerReference(
  request,
  parent,
  key,
  serverReference
) {
  // 断点:查看Server Action的序列化
  const id = getServerReferenceId(serverReference);
  const bound = getServerReferenceBoundArguments(serverReference);
  
  // 断点:查看绑定参数
  return serializeByValueID(
    request,
    {
      $$typeof: SERVER_REFERENCE_TAG,
      id: id,
      bound: bound
    }
  );
}

追踪特定组件的序列化

javascript
// 在renderElement中添加条件日志
function renderElement(request, task, type, props, ref) {
  if (typeof type === 'function' && type.name === 'MyComponent') {
    console.log('[Flight] Serializing MyComponent', {
      props,
      isClientReference: isClientReference(type)
    });
  }
  // 原有代码...
}

A.3.4 调试Flight解析

客户端解析调试

javascript
// 文件:packages/react-client/src/ReactFlightClient.js

// 在processFullRow设置断点
function processFullRow(response, row) {
  // 断点:查看行解析
  const colon = row.indexOf(':', 0);
  const id = parseInt(row.substring(0, colon), 16);
  const tag = row[colon + 1];
  
  // 断点:查看chunk类型
  switch (tag) {
    case 'J': {
      // JSON chunk
      const json = row.substring(colon + 2);
      // 断点:查看JSON解析
      resolveModel(response, id, json);
      return;
    }
    case 'M': {
      // Module chunk
      const json = row.substring(colon + 2);
      // 断点:查看模块引用解析
      resolveModule(response, id, json);
      return;
    }
    case 'S': {
      // Symbol chunk
      const name = row.substring(colon + 2);
      // 断点:查看Symbol解析
      resolveSymbol(response, id, name);
      return;
    }
    case 'E': {
      // Error chunk
      const json = row.substring(colon + 2);
      // 断点:查看错误处理
      resolveError(response, id, json);
      return;
    }
  }
}

// 在resolveModel设置断点
function resolveModel(response, id, json) {
  // 断点:查看JSON解析
  const model = JSON.parse(json, response._fromJSON);
  
  // 断点:查看model类型
  const chunk = getChunk(response, id);
  
  // 断点:查看chunk状态更新
  resolveModelChunk(chunk, model);
}

// 在createElement设置断点
function createElement(type, key, props) {
  // 断点:查看React元素的创建
  if (typeof type === 'object' && type !== null && type.$$typeof === CLIENT_REFERENCE_TAG) {
    // 断点:客户端组件的加载
    return createLazyClientComponent(type, props);
  }
  
  // 断点:普通元素创建
  return {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    props: props
  };
}

追踪特定chunk的解析

javascript
// 添加条件日志
function resolveModel(response, id, json) {
  if (id === 5) { // 追踪特定ID的chunk
    console.log('[Flight] Resolving chunk 5:', json);
  }
  // 原有代码...
}

A.3.5 Flight数据可视化

创建Flight查看器

javascript
// flight-viewer.js
function parseFlight(flightData) {
  const lines = flightData.trim().split('\n');
  const chunks = [];
  
  for (const line of lines) {
    const colonIndex = line.indexOf(':');
    const id = parseInt(line.substring(0, colonIndex), 16);
    const tag = line[colonIndex + 1];
    const data = line.substring(colonIndex + 2);
    
    chunks.push({
      id,
      tag,
      type: getChunkTypeName(tag),
      data: tag === 'J' || tag === 'M' || tag === 'E' ? JSON.parse(data) : data
    });
  }
  
  return chunks;
}

function getChunkTypeName(tag) {
  const types = {
    'J': 'JSON',
    'M': 'Module Reference',
    'S': 'Symbol',
    'E': 'Error',
    'P': 'Promise',
    'I': 'Iterable'
  };
  return types[tag] || 'Unknown';
}

function visualizeFlight(flightData) {
  const chunks = parseFlight(flightData);
  
  console.group('Flight Data Visualization');
  chunks.forEach(chunk => {
    console.group(`Chunk ${chunk.id} (${chunk.type})`);
    console.log('Raw:', chunk.data);
    if (chunk.type === 'JSON') {
      console.log('Parsed:', chunk.data);
    }
    console.groupEnd();
  });
  console.groupEnd();
}

// 使用
const flightResponse = await fetch('/?_rsc=xxx').then(r => r.text());
visualizeFlight(flightResponse);

生成Flight流程图

javascript
function generateFlightDiagram(flightData) {
  const chunks = parseFlight(flightData);
  const graph = {
    nodes: [],
    edges: []
  };
  
  chunks.forEach(chunk => {
    graph.nodes.push({
      id: chunk.id,
      label: `${chunk.id}: ${chunk.type}`,
      data: chunk.data
    });
    
    // 分析引用关系
    if (chunk.type === 'JSON' && typeof chunk.data === 'object') {
      findReferences(chunk.data).forEach(refId => {
        graph.edges.push({
          from: chunk.id,
          to: refId
        });
      });
    }
  });
  
  return graph;
}

function findReferences(obj, refs = []) {
  if (typeof obj === 'string' && obj.startsWith('$')) {
    const id = parseInt(obj.substring(1));
    if (!isNaN(id)) {
      refs.push(id);
    }
  } else if (Array.isArray(obj)) {
    obj.forEach(item => findReferences(item, refs));
  } else if (typeof obj === 'object' && obj !== null) {
    Object.values(obj).forEach(value => findReferences(value, refs));
  }
  return refs;
}

A.3.6 常见Flight调试场景

场景1:客户端组件未加载

检查:

  1. Module chunk是否正确生成
  2. 模块ID是否正确
  3. Webpack配置是否正确

resolveModule设置断点,查看模块解析过程。

场景2:数据丢失或错误

检查:

  1. 序列化是否正确处理所有数据类型
  2. 是否有循环引用
  3. 是否有不可序列化的值(如函数、Symbol)

resolveModelToJSON设置断点,查看序列化过程。

场景3:Server Action调用失败

检查:

  1. Server Reference是否正确序列化
  2. 绑定参数是否正确传递
  3. 客户端是否正确发送请求

serializeServerReference和客户端的action调用处设置断点。

A.3.7 Flight性能分析

测量序列化性能

javascript
function measureSerialization(component, props) {
  const start = performance.now();
  
  const stream = renderToReadableStream(
    React.createElement(component, props)
  );
  
  const chunks = [];
  const reader = stream.getReader();
  
  return reader.read().then(function processChunk({ done, value }) {
    if (done) {
      const end = performance.now();
      const totalSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
      
      console.log('Serialization metrics:', {
        duration: end - start,
        size: totalSize,
        chunksCount: chunks.length
      });
      
      return;
    }
    
    chunks.push(value);
    return reader.read().then(processChunk);
  });
}

测量解析性能

javascript
function measureParsing(flightData) {
  const start = performance.now();
  
  const response = createFromReadableStream(
    new ReadableStream({
      start(controller) {
        controller.enqueue(new TextEncoder().encode(flightData));
        controller.close();
      }
    })
  );
  
  response.then(() => {
    const end = performance.now();
    console.log('Parsing duration:', end - start);
  });
}

分析Flight数据大小

javascript
function analyzeFlight(flightData) {
  const chunks = parseFlight(flightData);
  const stats = {
    totalSize: flightData.length,
    chunkCount: chunks.length,
    byType: {}
  };
  
  chunks.forEach(chunk => {
    const type = chunk.type;
    if (!stats.byType[type]) {
      stats.byType[type] = { count: 0, size: 0 };
    }
    stats.byType[type].count++;
    stats.byType[type].size += JSON.stringify(chunk.data).length;
  });
  
  console.table(stats.byType);
  return stats;
}

本附录小结

本附录介绍了React源码调试的三个主要方面:

  1. 客户端代码调试:使用浏览器DevTools和VSCode调试客户端渲染、Hooks、Fiber树等
  2. 服务端代码调试:使用Node.js调试器调试SSR、RSC、Server Actions等
  3. Flight协议调试:拦截、解析和可视化Flight数据,理解RSC通信机制

掌握这些调试技巧,可以帮助你更深入地理解React的内部实现,快速定位和解决问题。


调试工具速查

工具用途适用场景
Chrome DevTools浏览器调试客户端代码、网络请求
VSCode Debugger编辑器调试客户端和服务端代码
React DevToolsReact专用工具组件树、性能分析
Node.js InspectorNode调试服务端代码
Network面板网络分析Flight数据、API请求
Performance面板性能分析渲染性能、长任务
Console API日志输出快速调试、追踪

推荐阅读