Skip to content

第1章 React 19全栈架构导读

本章将带你了解React 19的重大变革,熟悉源码仓库结构,并搭建调试环境。这是深入源码之旅的起点。

在开始阅读React源码之前,我们需要先回答几个问题:React 19相比之前的版本有什么本质变化?为什么说React已经从前端框架演变为全栈框架?源码仓库是如何组织的?如何搭建一个高效的调试环境?

本章将逐一解答这些问题,为后续的源码分析打下坚实基础。


1.1 React 19的重大变革

React 19是React发展历程中的一个里程碑版本。如果说React 16引入Fiber架构是一次内部重构,React 18引入并发特性是一次能力升级,那么React 19则标志着React从纯前端UI库向全栈框架的战略转型。

1.1.1 从前端框架到全栈框架

长期以来,React被定位为"用于构建用户界面的JavaScript库"。开发者使用React编写组件,这些组件在浏览器中运行,通过调用后端API获取数据。这种模式被称为客户端渲染(Client-Side Rendering,CSR)。

然而,CSR存在一些固有问题:

  1. 首屏加载慢:浏览器需要先下载JavaScript包,执行代码,再发起数据请求,最后才能渲染页面
  2. SEO不友好:搜索引擎爬虫看到的是空白HTML
  3. 瀑布式请求:组件渲染后才能发起数据请求,导致请求串行化

为了解决这些问题,社区发展出了服务端渲染(Server-Side Rendering,SSR)方案。Next.js、Remix等框架在服务端预渲染HTML,然后在客户端"水合"(Hydration)使页面具有交互性。

但传统SSR也有局限:

  • 服务端渲染的组件仍然需要在客户端重新执行一遍
  • 所有组件代码都要打包发送到客户端
  • 数据获取逻辑分散在getServerSideProps等特殊函数中

React 19通过引入React Server Components(RSC)和Server Actions,从根本上改变了这一局面。现在,组件可以真正地只在服务端运行,数据获取可以直接在组件内部进行,服务端函数可以像调用本地函数一样被客户端调用。

jsx
// 这是一个Server Component,只在服务端执行
async function ProductList() {
  // 直接在组件中获取数据,无需API调用
  const products = await db.query('SELECT * FROM products');
  
  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

这种模式带来了革命性的变化:

  • 零客户端JavaScript:Server Component的代码不会发送到浏览器
  • 直接数据访问:可以直接访问数据库、文件系统等服务端资源
  • 自动代码分割:客户端只加载需要交互的组件代码

1.1.2 React Server Components的诞生

React Server Components(RSC)的概念最早在2020年12月由React团队提出。经过三年多的开发和迭代,RSC在React 19中正式成为稳定特性。

RSC的核心思想是:将组件分为服务端组件和客户端组件两类,让它们各司其职

服务端组件(Server Components):

  • 在服务端执行,可以访问服务端资源
  • 支持async/await,可以直接进行数据获取
  • 渲染结果通过特殊的序列化格式(Flight协议)传输到客户端
  • 代码不会打包到客户端bundle中

客户端组件(Client Components):

  • 在客户端执行,可以使用浏览器API
  • 可以使用useState、useEffect等Hooks
  • 可以响应用户交互事件
  • 需要通过"use client"指令标记
jsx
// ServerComponent.jsx - 默认是Server Component
import ClientButton from './ClientButton';

async function ServerComponent() {
  const data = await fetchData(); // 服务端数据获取
  
  return (
    <div>
      <h1>{data.title}</h1>
      <ClientButton /> {/* 嵌入客户端组件 */}
    </div>
  );
}
jsx
// ClientButton.jsx - 客户端组件
"use client";

import { useState } from 'react';

export default function ClientButton() {
  const [count, setCount] = useState(0);
  
  return (
    <button onClick={() => setCount(c => c + 1)}>
      点击次数: {count}
    </button>
  );
}

1.1.3 Server Actions与表单处理

除了RSC,React 19还引入了Server Actions,这是一种在客户端直接调用服务端函数的机制。

传统的表单处理流程是:

  1. 用户填写表单
  2. 客户端JavaScript拦截提交事件
  3. 发送AJAX请求到API端点
  4. 服务端处理请求并返回结果
  5. 客户端更新UI

Server Actions简化了这一流程:

jsx
// actions.js
"use server";

export async function createPost(formData) {
  const title = formData.get('title');
  const content = formData.get('content');
  
  await db.insert('posts', { title, content });
  
  // 自动触发页面重新验证
  revalidatePath('/posts');
}
jsx
// PostForm.jsx
import { createPost } from './actions';

export function PostForm() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="标题" />
      <textarea name="content" placeholder="内容" />
      <button type="submit">发布</button>
    </form>
  );
}

Server Actions的优势:

  • 渐进增强:即使JavaScript禁用,表单仍然可以工作
  • 类型安全:服务端函数可以直接使用TypeScript类型
  • 自动序列化:React自动处理参数的序列化和反序列化
  • 乐观更新:配合useOptimistic Hook实现即时反馈

1.1.4 新的渲染模式

React 19支持多种渲染模式,开发者可以根据场景选择最合适的方案:

渲染模式执行位置首屏性能交互性适用场景
CSR客户端较慢完整后台管理系统
SSR服务端+客户端较快完整内容型网站
Streaming SSR服务端+客户端渐进式大型应用
RSC服务端为主最快按需全栈应用

这些模式并非互斥,而是可以在同一个应用中混合使用。例如,一个电商网站可以:

  • 使用RSC渲染商品列表(无需交互)
  • 使用客户端组件实现购物车(需要状态管理)
  • 使用Streaming SSR实现商品详情页(需要快速首屏)

1.2 React源码仓库结构

理解React源码的第一步是熟悉其仓库结构。React采用Monorepo(单一代码仓库)的组织方式,所有相关包都在同一个Git仓库中管理。

1.2.1 Monorepo项目组织

React源码托管在GitHub上:https://github.com/facebook/react

克隆仓库后,你会看到以下顶层目录结构:

react/
├── packages/          # 所有npm包的源码
├── scripts/           # 构建、测试、发布脚本
├── fixtures/          # 测试用的示例项目
├── compiler/          # React Compiler(独立子项目)
├── .github/           # GitHub Actions配置
├── package.json       # 根配置文件
├── yarn.lock          # 依赖锁定文件
└── ReactVersions.js   # 版本号定义

React使用Yarn Workspaces管理多包依赖。根目录的package.json中定义了工作空间:

json
{
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}

这意味着packages/目录下的每个子目录都是一个独立的npm包,它们可以相互引用,但在发布时是独立的。

1.2.2 核心包职责划分

React的核心功能分布在多个包中,每个包有明确的职责边界:

react包

  • 定义组件API(ComponentcreateElementuseState等)
  • 不包含任何渲染逻辑
  • 是所有React应用的基础依赖

react-reconciler包

  • 实现Fiber架构和协调算法
  • 处理组件的挂载、更新、卸载
  • 与具体渲染目标无关(可用于DOM、Native等)

react-dom包

  • 实现DOM渲染器
  • 提供createRoothydrateRoot等API
  • 包含事件系统实现

scheduler包

  • 实现任务调度器
  • 管理任务优先级
  • 实现时间切片

这种分层设计使得React可以支持多种渲染目标。例如,React Native使用react-native-renderer替代react-dom,但共享reactreact-reconciler的代码。

1.2.3 服务端相关包概览

React 19的全栈能力主要由以下包提供:

react-server包

  • 服务端渲染的核心实现
  • 包含Fizz(流式SSR引擎)
  • 包含Flight(RSC序列化协议)服务端部分

react-client包

  • Flight协议的客户端解析器
  • 负责将服务端传来的数据重建为React元素

react-server-dom-webpack包

  • RSC与Webpack的集成
  • 提供客户端和服务端的入口
  • 包含Webpack插件

react-dom-bindings包

  • DOM操作的底层实现
  • 事件系统
  • Hydration逻辑

这些包的关系可以用下图表示:

1.2.4 packages目录详解

packages/目录包含了React的所有源码包。以下是React 19.3.0版本中的主要包:

包名说明发布状态
react核心API定义公开
react-domDOM渲染器公开
react-dom-bindingsDOM操作底层实现内部
react-reconciler协调器公开
scheduler调度器公开
react-server服务端渲染核心内部
react-clientFlight客户端内部
react-server-dom-webpackRSC Webpack集成公开
react-server-dom-turbopackRSC Turbopack集成公开
react-server-dom-parcelRSC Parcel集成公开
shared共享工具函数内部
react-refresh热更新支持公开
react-devtools开发者工具公开

其中,标记为"内部"的包不会单独发布到npm,而是被其他包引用或打包进最终产物。

让我们深入看看几个核心包的目录结构:

react包结构

packages/react/
├── src/
│   ├── React.js              # 主入口,导出所有API
│   ├── ReactHooks.js         # Hooks API定义
│   ├── ReactElement.js       # createElement实现
│   ├── ReactChildren.js      # Children工具函数
│   └── jsx/
│       └── ReactJSXElement.js # JSX运行时
├── index.js                  # npm包入口
├── jsx-runtime.js            # JSX自动运行时
└── package.json

react-server包结构

packages/react-server/
├── src/
│   ├── ReactFizzServer.js    # 流式SSR引擎
│   ├── ReactFlightServer.js  # RSC序列化
│   ├── ReactServerStreamConfig*.js # 流配置
│   └── forks/                # 平台特定实现
├── index.js                  # Fizz入口
├── flight.js                 # Flight入口
└── package.json

1.3 源码调试环境搭建

阅读源码最有效的方式是边读边调试。本节将指导你搭建一个可以断点调试React源码的开发环境。

1.3.1 克隆React仓库

首先,克隆React官方仓库:

bash
git clone https://github.com/facebook/react.git
cd react

如果你想基于特定版本进行学习,可以切换到对应的tag:

bash
# 查看所有版本标签
git tag -l "v19.*"

# 切换到19.3.0版本
git checkout v19.3.0

1.3.2 安装依赖与构建

React使用Yarn作为包管理器。确保你安装了Node.js 18+和Yarn 1.x:

bash
# 检查Node版本
node --version  # 应该 >= 18.0.0

# 安装Yarn(如果没有)
npm install -g yarn

# 安装依赖
yarn install

安装完成后,构建React:

bash
# 构建所有包(开发模式)
yarn build

# 或者只构建特定包
yarn build react react-dom scheduler

构建产物会输出到build/目录。

1.3.3 示例:在VSCode中调试React源码

要调试React源码,我们需要创建一个测试项目并配置source map。

步骤1:创建测试项目

fixtures/目录下创建一个简单的测试项目:

bash
mkdir -p fixtures/debug-test
cd fixtures/debug-test

创建index.html

html
<!DOCTYPE html>
<html>
<head>
  <title>React Debug Test</title>
</head>
<body>
  <div id="root"></div>
  <script type="module" src="./main.jsx"></script>
</body>
</html>

创建main.jsx

jsx
import React from 'react';
import { createRoot } from 'react-dom/client';

function App() {
  const [count, setCount] = React.useState(0);
  
  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(c => c + 1)}>
        Increment
      </button>
    </div>
  );
}

const root = createRoot(document.getElementById('root'));
root.render(<App />);

步骤2:配置VSCode调试

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

json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "Debug React",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}",
      "sourceMaps": true,
      "sourceMapPathOverrides": {
        "webpack:///packages/*": "${workspaceFolder}/packages/*"
      }
    }
  ]
}

步骤3:启动调试

  1. 使用支持source map的开发服务器启动测试项目
  2. 在VSCode中按F5启动调试
  3. packages/react-reconciler/src/ReactFiberHooks.js中设置断点
  4. 在浏览器中点击按钮触发状态更新
  5. 断点命中,可以查看调用栈和变量

1.3.4 调试服务端渲染代码

调试服务端代码需要使用Node.js调试器。

步骤1:创建SSR测试脚本

创建fixtures/ssr-debug/server.js

javascript
const React = require('react');
const { renderToPipeableStream } = require('react-dom/server');
const http = require('http');

function App() {
  return React.createElement('div', null, 
    React.createElement('h1', null, 'Hello SSR'),
    React.createElement('p', null, 'This is server-rendered.')
  );
}

const server = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'text/html');
  
  const { pipe } = renderToPipeableStream(
    React.createElement(App),
    {
      onShellReady() {
        res.statusCode = 200;
        pipe(res);
      },
      onError(error) {
        console.error(error);
        res.statusCode = 500;
        res.end('Server Error');
      }
    }
  );
});

server.listen(3000, () => {
  console.log('SSR server running at http://localhost:3000');
});

步骤2:配置Node.js调试

.vscode/launch.json中添加配置:

json
{
  "type": "node",
  "request": "launch",
  "name": "Debug SSR",
  "program": "${workspaceFolder}/fixtures/ssr-debug/server.js",
  "sourceMaps": true,
  "outFiles": ["${workspaceFolder}/build/**/*.js"]
}

步骤3:设置断点调试

packages/react-server/src/ReactFizzServer.js中设置断点,然后启动调试。访问http://localhost:3000时,断点会命中,你可以逐步跟踪SSR的执行流程。


1.4 阅读源码的方法论

React源码庞大而复杂,直接阅读很容易迷失方向。本节分享一些实用的源码阅读方法。

1.4.1 客户端与服务端的代码边界

React 19的代码可以分为三个主要区域:

  1. 纯客户端代码:只在浏览器中运行

    • react-dom/client
    • react-reconciler(大部分)
    • 事件系统
  2. 纯服务端代码:只在Node.js/Edge Runtime中运行

    • react-server
    • react-dom/server
    • Server Actions处理
  3. 共享代码:客户端和服务端都使用

    • react核心API
    • shared工具函数
    • 部分react-reconciler代码

理解这个边界很重要。当你看到一段代码时,首先要判断它运行在哪个环境。React使用条件编译来区分:

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

if (__DEV__) {
  // 开发环境代码
}

if (enableServerContext) {
  // 服务端上下文相关代码
}

这些条件在构建时会被替换为具体的布尔值,不同的构建目标会产生不同的代码。

1.4.2 抓住主线,忽略细节

React源码中有大量的边界情况处理、开发环境检查、性能优化代码。初次阅读时,建议:

  1. 忽略__DEV__:这些是开发环境的警告和检查
  2. 忽略enableXxx特性开关:这些是实验性功能
  3. 忽略错误处理:先理解正常流程
  4. 关注核心数据结构:Fiber、Hook、Update等

例如,beginWork函数的简化版本:

javascript
// 简化版 - 省略了开发环境检查和边界情况处理
// 文件:packages/react-reconciler/src/ReactFiberBeginWork.js

function beginWork(current, workInProgress, renderLanes) {
  // 根据组件类型分发处理
  switch (workInProgress.tag) {
    case FunctionComponent:
      return updateFunctionComponent(current, workInProgress, renderLanes);
    case ClassComponent:
      return updateClassComponent(current, workInProgress, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    // ... 其他类型
  }
}

实际源码中,这个函数有数百行,但核心逻辑就是这个switch语句。

1.4.3 善用断点和日志

阅读静态代码只能理解"是什么",要理解"为什么"和"何时",需要动态调试。

技巧1:条件断点

在VSCode中,右键点击断点可以设置条件:

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

技巧2:日志断点

不中断执行,只打印日志:

javascript
// 打印当前处理的Fiber节点
console.log('Processing:', workInProgress.tag, workInProgress.type)

技巧3:调用栈分析

当断点命中时,查看调用栈可以理解代码的执行路径。React的调用栈通常很深,但有规律可循:

dispatchSetState          <- 用户触发更新
  scheduleUpdateOnFiber   <- 调度更新
    ensureRootIsScheduled <- 确保根节点被调度
      performConcurrentWorkOnRoot <- 执行并发渲染
        renderRootConcurrent <- 渲染阶段
          workLoopConcurrent <- 工作循环
            performUnitOfWork <- 处理单个Fiber
              beginWork       <- 开始处理

技巧4:使用React DevTools

React DevTools可以可视化Fiber树结构,帮助理解组件层级和状态。在调试时,可以配合DevTools查看当前的Fiber树状态。


本章小结

本章介绍了React 19的重大变革和源码阅读的准备工作:

  1. React 19的定位转变:从前端UI库到全栈框架
  2. 核心新特性:React Server Components、Server Actions、Streaming SSR
  3. 仓库结构:Monorepo组织,核心包职责划分
  4. 调试环境:VSCode配置,客户端和服务端调试方法
  5. 阅读方法:抓住主线,善用调试工具

下一章,我们将深入React的整体架构,理解各个模块如何协作完成渲染工作。


思考题

  1. React Server Components和传统SSR的本质区别是什么?
  2. 为什么React要将reactreact-dom分成两个包?
  3. 在你的项目中,哪些组件适合作为Server Component,哪些适合作为Client Component?