Skip to content

第2章 React整体架构

本章将深入剖析React的设计理念和核心模块,帮助你建立对React全栈架构的整体认知。

在上一章中,我们了解了React 19的重大变革和源码仓库结构。现在,让我们更深入地探索React的整体架构。React是如何组织其庞大的代码库的?各个模块之间是如何协作的?客户端和服务端是如何配合完成渲染的?

本章将回答这些问题,为后续深入各个模块的源码分析奠定基础。


2.1 React的设计理念

在深入源码之前,我们需要先理解React的设计理念。这些理念贯穿于React的每一行代码,理解它们能帮助我们更好地把握源码的设计意图。

2.1.1 声明式编程

React最核心的设计理念是声明式编程(Declarative Programming)。

传统的命令式编程关注"如何做"——开发者需要一步步告诉计算机执行什么操作:

javascript
// 命令式:手动操作DOM
const container = document.getElementById('root');
const button = document.createElement('button');
button.innerText = '点击次数: 0';
button.onclick = function() {
  const count = parseInt(button.innerText.split(': ')[1]) + 1;
  button.innerText = '点击次数: ' + count;
};
container.appendChild(button);

而声明式编程关注"是什么"——开发者只需描述UI应该是什么样子,React负责将其变为现实:

jsx
// 声明式:描述UI状态
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(c => c + 1)}>
      点击次数: {count}
    </button>
  );
}

声明式编程的优势在于:

  1. 可预测性:给定相同的状态,总是渲染相同的UI
  2. 可维护性:代码更易读、更易理解
  3. 可优化性:React可以在底层自动优化DOM操作

这种理念在源码中的体现是:React将UI视为状态的函数,即UI = f(state)。当状态变化时,React重新计算UI应该是什么样子,然后高效地更新DOM。

2.1.2 组件化思想

React的第二个核心理念是组件化(Component-Based)。

组件是React应用的基本构建单元。每个组件封装了自己的结构(JSX)、样式和行为(事件处理、状态管理),可以独立开发、测试和复用。

jsx
// 组件封装了完整的UI单元
function ProductCard({ product }) {
  const [isLiked, setIsLiked] = useState(false);
  
  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>{product.price}</p>
      <button onClick={() => setIsLiked(!isLiked)}>
        {isLiked ? '❤️' : '🤍'}
      </button>
    </div>
  );
}

组件化思想在源码中的体现是Fiber架构。每个组件对应一个Fiber节点,Fiber树反映了组件的层级结构。React通过遍历Fiber树来协调组件的更新。

2.1.3 服务端优先

React 19引入了一个新的设计理念:服务端优先(Server-First)。

传统的React应用是"客户端优先"的——所有组件代码都发送到浏览器执行。这导致了JavaScript包体积大、首屏加载慢等问题。

React 19通过React Server Components(RSC)实现了服务端优先:

jsx
// 服务端组件:只在服务端执行
async function ProductList() {
  // 直接访问数据库,无需API
  const products = await db.query('SELECT * FROM products');
  
  return (
    <ul>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </ul>
  );
}

服务端优先的优势:

  1. 零客户端JavaScript:服务端组件的代码不会发送到浏览器
  2. 直接数据访问:可以直接访问数据库、文件系统等服务端资源
  3. 更快的首屏:HTML在服务端生成,用户更快看到内容
  4. 更好的SEO:搜索引擎可以直接抓取渲染后的HTML

这种理念在源码中的体现是react-server包和Flight协议。服务端组件在服务端渲染后,通过Flight协议将结果序列化传输到客户端。


2.2 React的核心模块

React的代码库采用模块化设计,每个模块有明确的职责边界。理解这些模块的职责和关系,是深入源码的关键。

2.2.1 react包:组件定义API

react包是React的核心包,定义了组件开发所需的所有API,但不包含任何渲染逻辑。

packages/react/
├── src/
│   ├── ReactClient.js        # 客户端API导出
│   ├── ReactServer.js        # 服务端API导出
│   ├── ReactHooks.js         # Hooks API定义
│   ├── ReactBaseClasses.js   # Component/PureComponent
│   ├── ReactContext.js       # createContext
│   ├── ReactMemo.js          # memo
│   ├── ReactLazy.js          # lazy
│   └── jsx/
│       └── ReactJSXElement.js # createElement/jsx
└── package.json

react包导出的主要API包括:

组件相关

  • ComponentPureComponent:类组件基类
  • createElement:创建React元素
  • memo:函数组件记忆化
  • lazy:组件懒加载
  • forwardRef:转发ref

Hooks相关

  • useStateuseReducer:状态管理
  • useEffectuseLayoutEffect:副作用
  • useContext:上下文消费
  • useMemouseCallback:记忆化
  • useRef:引用
  • useTransitionuseDeferredValue:并发特性
  • useActionState:Server Actions状态

上下文相关

  • createContext:创建上下文
  • useContext:消费上下文

让我们看看react包的客户端入口文件:

javascript
// 文件:packages/react/src/ReactClient.js
// 简化版 - 展示主要导出

export {
  // 组件基类
  Component,
  PureComponent,
  
  // 创建元素
  createElement,
  cloneElement,
  isValidElement,
  
  // 组件工具
  createContext,
  forwardRef,
  lazy,
  memo,
  
  // Hooks
  useState,
  useReducer,
  useEffect,
  useLayoutEffect,
  useContext,
  useMemo,
  useCallback,
  useRef,
  useId,
  
  // 并发特性
  useTransition,
  startTransition,
  useDeferredValue,
  
  // React 19新增
  useOptimistic,
  useActionState,
  use,
  
  // 内置组件
  Fragment,
  Suspense,
  StrictMode,
  Profiler,
};

react包的设计原则是与渲染目标无关。无论是DOM、Native还是其他渲染目标,都使用相同的react包。这种设计使得React可以支持多种平台。

2.2.2 react-reconciler包:客户端协调器

react-reconciler包是React的核心引擎,实现了Fiber架构和协调算法。它负责:

  1. 构建Fiber树:将组件树转换为Fiber树
  2. 协调更新:计算新旧Fiber树的差异
  3. 调度任务:管理更新任务的优先级
  4. 收集副作用:标记需要执行的DOM操作
packages/react-reconciler/
├── src/
│   ├── ReactFiber.js              # Fiber节点创建
│   ├── ReactFiberBeginWork.js     # 协调开始阶段
│   ├── ReactFiberCompleteWork.js  # 协调完成阶段
│   ├── ReactFiberCommitWork.js    # 提交阶段
│   ├── ReactFiberHooks.js         # Hooks实现
│   ├── ReactFiberWorkLoop.js      # 工作循环
│   ├── ReactFiberLane.js          # Lane优先级
│   ├── ReactFiberReconciler.js    # 协调器入口
│   └── ReactChildFiber.js         # 子节点Diff
└── package.json

协调器的核心流程可以概括为三个阶段:

react-reconciler的设计原则是与宿主环境无关。它通过HostConfig接口与具体的渲染目标交互,这使得同一套协调逻辑可以用于DOM、Native等不同平台。

2.2.3 react-dom包:DOM渲染器

react-dom包是React的DOM渲染器,负责将React组件渲染到浏览器DOM中。

packages/react-dom/
├── src/
│   ├── client/
│   │   ├── ReactDOMRoot.js        # createRoot/hydrateRoot
│   │   └── ReactDOMClient.js      # 客户端入口
│   ├── server/
│   │   ├── ReactDOMFizzServerNode.js    # Node.js SSR
│   │   ├── ReactDOMFizzServerBrowser.js # 浏览器SSR
│   │   └── ReactDOMFizzStaticNode.js    # 静态生成
│   └── shared/
│       └── ReactDOMSharedInternals.js
├── client.js                      # 客户端入口
├── server.js                      # 服务端入口
├── server.node.js                 # Node.js服务端入口
└── package.json

react-dom提供的主要API:

客户端APIreact-dom/client):

  • createRoot:创建React根节点
  • hydrateRoot:水合服务端渲染的HTML

服务端APIreact-dom/server):

  • renderToPipeableStream:流式SSR(Node.js)
  • renderToReadableStream:流式SSR(Web Streams)
  • renderToString:同步渲染为字符串(不推荐)
  • renderToStaticMarkup:渲染静态HTML
jsx
// 客户端渲染
import { createRoot } from 'react-dom/client';

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

// 服务端渲染
import { renderToPipeableStream } from 'react-dom/server';

const { pipe } = renderToPipeableStream(<App />, {
  onShellReady() {
    response.statusCode = 200;
    pipe(response);
  }
});

react-dom内部使用react-reconciler进行协调,并通过react-dom-bindings执行实际的DOM操作。

2.2.4 react-server包:服务端渲染核心

react-server包是React服务端渲染的核心实现,包含两个主要引擎:

  1. Fizz:流式SSR引擎,将组件渲染为HTML流
  2. Flight:RSC序列化引擎,将服务端组件序列化为特殊格式
packages/react-server/
├── src/
│   ├── ReactFizzServer.js         # Fizz引擎核心
│   ├── ReactFizzHooks.js          # 服务端Hooks
│   ├── ReactFlightServer.js       # Flight引擎核心
│   ├── ReactFlightHooks.js        # RSC Hooks
│   └── ReactFlightActionServer.js # Server Actions
├── index.js                       # Fizz入口
└── flight.js                      # Flight入口

Fizz引擎负责流式SSR:

javascript
// Fizz将组件树渲染为HTML流
// 文件:packages/react-server/src/ReactFizzServer.js

// 核心数据结构
type Task = {
  node: ReactNodeList,           // 要渲染的节点
  segment: Segment,              // 输出段
  blockedBoundary: SuspenseBoundary, // Suspense边界
  // ...
};

type Segment = {
  status: 'pending' | 'completed', // 段状态
  chunks: Array<Chunk>,           // HTML片段
  // ...
};

Flight引擎负责RSC序列化:

javascript
// Flight将服务端组件序列化为特殊格式
// 文件:packages/react-server/src/ReactFlightServer.js

// Flight协议输出示例:
// 0:["$","div",null,{"children":"Hello"}]
// 1:I["./ClientComponent.js","default"]
// 2:["$","$L1",null,{"name":"World"}]

2.2.5 react-client包:客户端Flight解析

react-client包负责解析服务端传来的Flight数据,将其重建为React元素树。

packages/react-client/
├── src/
│   ├── ReactFlightClient.js       # Flight解析核心
│   ├── ReactFlightReplyClient.js  # Server Actions响应
│   └── ReactFlightTemporaryReferences.js
├── flight.js                      # 入口
└── package.json

Flight客户端的工作流程:

javascript
// Flight客户端解析示例
// 文件:packages/react-client/src/ReactFlightClient.js

// 解析Flight数据行
// "0:["$","div",null,{"children":"Hello"}]"
// 解析为:{ id: 0, type: 'model', value: ReactElement }

2.3 客户端与服务端的协作

React 19的全栈能力依赖于客户端和服务端的紧密协作。本节将详细分析不同渲染模式的工作原理。

2.3.1 CSR、SSR、RSC的区别

React支持三种主要的渲染模式,它们各有特点:

客户端渲染(CSR)

特点:

  • 所有渲染在浏览器进行
  • 首屏加载慢(需要下载JS、执行、请求数据)
  • SEO不友好
  • 适合后台管理系统等对SEO要求不高的场景

服务端渲染(SSR)

特点:

  • 服务端渲染HTML,客户端水合
  • 首屏快(HTML直接可见)
  • SEO友好
  • 组件代码仍需发送到客户端
  • 适合内容型网站

React Server Components(RSC)

特点:

  • Server Components只在服务端执行
  • 零客户端JavaScript(对于Server Components)
  • 可直接访问服务端资源
  • 自动代码分割
  • 适合全栈应用

2.3.2 数据流向图解

让我们通过一个完整的数据流图来理解React全栈架构:

数据流说明

  1. 服务端处理

    • Server Components在服务端执行,可以直接访问数据库
    • Flight Serializer将组件树序列化为Flight格式
    • Fizz将HTML流式输出(用于SSR)
  2. 网络传输

    • Flight数据通过HTTP响应流式传输
    • HTML也可以流式传输(Streaming SSR)
  3. 客户端处理

    • Flight Client解析Flight数据
    • 重建React元素树
    • Client Components在客户端执行
    • Reconciler协调更新
    • react-dom执行DOM操作

2.3.3 示例:一个请求的完整生命周期

让我们通过一个具体的例子来理解完整的请求生命周期。假设我们有一个商品详情页:

jsx
// app/product/[id]/page.jsx - Server Component
import { ProductInfo } from './ProductInfo';
import { AddToCartButton } from './AddToCartButton';

export default async function ProductPage({ params }) {
  // 1. 服务端:直接查询数据库
  const product = await db.query(
    'SELECT * FROM products WHERE id = ?',
    [params.id]
  );
  
  return (
    <div className="product-page">
      {/* 2. Server Component渲染 */}
      <ProductInfo product={product} />
      
      {/* 3. Client Component引用 */}
      <AddToCartButton productId={product.id} />
    </div>
  );
}
jsx
// app/product/[id]/ProductInfo.jsx - Server Component
export function ProductInfo({ product }) {
  return (
    <div className="product-info">
      <h1>{product.name}</h1>
      <p className="price">${product.price}</p>
      <p className="description">{product.description}</p>
    </div>
  );
}
jsx
// app/product/[id]/AddToCartButton.jsx - Client Component
"use client";

import { useState } from 'react';

export function AddToCartButton({ productId }) {
  const [isAdding, setIsAdding] = useState(false);
  
  async function handleClick() {
    setIsAdding(true);
    await addToCart(productId);
    setIsAdding(false);
  }
  
  return (
    <button onClick={handleClick} disabled={isAdding}>
      {isAdding ? '添加中...' : '加入购物车'}
    </button>
  );
}

请求生命周期

Flight数据示例

0:["$","div",null,{"className":"product-page","children":[...]}]
1:["$","div",null,{"className":"product-info","children":[...]}]
2:I["./AddToCartButton.js","AddToCartButton"]
3:["$","$L2",null,{"productId":123}]

解释:

  • 0::根div元素
  • 1::ProductInfo渲染结果(内联)
  • 2::客户端组件模块引用(I表示Import)
  • 3::AddToCartButton的props($L2引用第2行的模块)

2.4 渲染模式演进

React的渲染模式经历了多次演进,从最初的客户端渲染到如今的React Server Components。理解这个演进过程,有助于我们理解React设计决策的背景。

2.4.1 客户端渲染(CSR)

客户端渲染是React最初的渲染模式,也是最简单的模式。

jsx
// index.html
<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
</head>
<body>
  <div id="root"></div>
  <script src="/bundle.js"></script>
</body>
</html>

// index.jsx
import { createRoot } from 'react-dom/client';
import App from './App';

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

CSR的工作流程

CSR的问题

  1. 首屏白屏时间长:需要下载、解析、执行JS后才能渲染
  2. SEO不友好:搜索引擎看到的是空HTML
  3. 性能瓶颈:所有代码都在客户端执行

2.4.2 服务端渲染(SSR)

为了解决CSR的问题,React引入了服务端渲染。

jsx
// server.js
import { renderToString } from 'react-dom/server';
import App from './App';

app.get('/', (req, res) => {
  const html = renderToString(<App />);
  res.send(`
    <!DOCTYPE html>
    <html>
    <body>
      <div id="root">${html}</div>
      <script src="/bundle.js"></script>
    </body>
    </html>
  `);
});

SSR的工作流程

SSR的改进

  • 首屏更快(HTML直接可见)
  • SEO友好(搜索引擎可以抓取内容)

SSR的问题

  • 服务器压力大(每个请求都要渲染)
  • TTFB(Time To First Byte)可能较长
  • 组件代码仍需发送到客户端
  • Hydration阻塞交互

2.4.3 流式服务端渲染(Streaming SSR)

React 18引入了流式SSR,解决了传统SSR的阻塞问题。

jsx
// server.js
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';

app.get('/', (req, res) => {
  const { pipe } = renderToPipeableStream(<App />, {
    onShellReady() {
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/html');
      pipe(res);
    }
  });
});

流式SSR的工作流程

流式SSR的改进

  • TTFB更短(Shell先返回)
  • 用户更快看到内容
  • Suspense边界可以独立加载
  • 选择性Hydration(优先水合用户交互的部分)

2.4.4 React Server Components(RSC)

React 19将RSC作为稳定特性,这是渲染模式的又一次重大演进。

jsx
// Server Component
async function ProductList() {
  const products = await db.query('SELECT * FROM products');
  return (
    <ul>
      {products.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}

// Client Component
"use client";
function AddToCartButton({ productId }) {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>Add ({count})</button>;
}

RSC的工作流程

RSC的革命性改进

  1. 零客户端JavaScript:Server Components的代码不发送到客户端
  2. 直接数据访问:可以直接访问数据库、文件系统
  3. 自动代码分割:Client Components自动按需加载
  4. 更小的Bundle:只有交互组件的代码发送到客户端

渲染模式对比

特性CSRSSRStreaming SSRRSC
首屏速度更快最快
SEO
服务器压力
客户端JS全部全部全部部分
数据获取客户端服务端服务端服务端
交互性完整完整渐进按需

2.5 示例:一个请求的完整生命周期

为了更好地理解React全栈架构,让我们通过一个完整的示例来追踪一个请求从发起到渲染完成的全过程。

场景设定

假设我们有一个博客应用,用户访问文章详情页/posts/123。页面包含:

  • 文章内容(Server Component)
  • 评论列表(Server Component,使用Suspense)
  • 点赞按钮(Client Component)
jsx
// app/posts/[id]/page.jsx
import { Suspense } from 'react';
import { PostContent } from './PostContent';
import { Comments } from './Comments';
import { LikeButton } from './LikeButton';

export default async function PostPage({ params }) {
  return (
    <article>
      <PostContent postId={params.id} />
      <LikeButton postId={params.id} />
      <Suspense fallback={<p>加载评论中...</p>}>
        <Comments postId={params.id} />
      </Suspense>
    </article>
  );
}

阶段1:服务端接收请求

当用户访问/posts/123时,服务端开始处理请求:

javascript
// 伪代码:服务端处理流程
async function handleRequest(request) {
  // 1. 解析路由,确定要渲染的组件
  const { component, params } = matchRoute(request.url);
  
  // 2. 创建Flight请求上下文
  const flightRequest = createFlightRequest();
  
  // 3. 开始渲染Server Components
  const flightStream = renderToReadableStream(
    <component params={params} />,
    clientManifest
  );
  
  // 4. 返回流式响应
  return new Response(flightStream);
}

阶段2:Server Components执行

服务端开始执行Server Components:

阶段3:Flight序列化

React将渲染结果序列化为Flight格式:

// Flight数据流示例
0:["$","article",null,{"children":[["$","$L1",null,{}],["$","$L2",null,{"postId":"123"}],["$","$Sreact.suspense",null,{"fallback":["$","p",null,{"children":"加载评论中..."}],"children":"$L3"}]]}]
1:["$","div",null,{"className":"post-content","children":[["$","h1",null,{"children":"文章标题"}],["$","p",null,{"children":"文章内容..."}]]}]
2:I["./LikeButton.js","LikeButton"]
3:["$","div",null,{"className":"comments","children":[...]}]

Flight数据解读

行号类型说明
0:Model根组件article的结构
1:ModelPostContent的渲染结果(内联)
2:ImportLikeButton的模块引用
3:ModelComments的渲染结果(延迟到达)

注意第3行是在Comments数据获取完成后才发送的,这就是流式传输的体现。

阶段4:客户端接收与解析

客户端的Flight Client开始解析接收到的数据:

javascript
// 伪代码:客户端解析流程
// 文件:packages/react-client/src/ReactFlightClient.js

function processFlightData(chunk) {
  // 1. 解析行数据
  const [id, type, value] = parseRow(chunk);
  
  // 2. 根据类型处理
  switch (type) {
    case 'I': // Import - 客户端组件引用
      // 记录模块引用,稍后动态加载
      moduleCache.set(id, {
        specifier: value[0],
        name: value[1]
      });
      break;
      
    case '$': // Model - React元素
      // 重建React元素树
      const element = reconstructElement(value);
      chunks.set(id, element);
      break;
  }
  
  // 3. 触发等待该chunk的组件更新
  resolveWaitingComponents(id);
}

阶段5:组件树重建与渲染

Flight Client将解析后的数据重建为React元素树:

重建过程

  1. 解析Model数据:将Flight格式转换为React元素
  2. 解析Import引用:动态加载Client Component代码
  3. 构建元素树:组合Server Component结果和Client Component
  4. 协调渲染:通过Reconciler将元素树转换为Fiber树
  5. 提交DOM:将变更应用到真实DOM

阶段6:Hydration水合

对于需要交互的Client Components,React执行水合过程:

javascript
// 水合过程
// 文件:packages/react-dom-bindings/src/client/ReactDOMHydration.js

function hydrateInstance(instance, props) {
  // 1. 复用服务端渲染的DOM节点
  // 2. 绑定事件处理器
  // 3. 恢复组件状态
  
  // 对于LikeButton:
  // - 找到服务端渲染的<button>元素
  // - 绑定onClick事件
  // - 初始化useState状态
}

阶段7:页面可交互

水合完成后,页面完全可交互:

完整时间线

让我们用一个时间线图来总结整个过程:

关键指标

  • TTFB(Time To First Byte):~50ms - 服务端开始响应
  • FCP(First Contentful Paint):~100ms - 用户看到内容
  • TTI(Time To Interactive):~300ms - 页面可交互

源码追踪指南

如果你想在源码中追踪这个流程,以下是关键文件:

阶段源码文件
服务端渲染入口packages/react-server/src/ReactFlightServer.js
Flight序列化packages/react-server/src/ReactFlightServer.js
客户端解析packages/react-client/src/ReactFlightClient.js
Hydrationpackages/react-dom-bindings/src/client/ReactDOMHydration.js
协调器packages/react-reconciler/src/ReactFiberWorkLoop.js

本章小结

本章我们深入探讨了React的整体架构,主要内容包括:

  1. 设计理念:React的三大核心理念——声明式编程、组件化思想、服务端优先。这些理念贯穿于React的每一行代码。

  2. 核心模块:React的五大核心模块各司其职:

    • react:组件定义API,与渲染目标无关
    • react-reconciler:协调引擎,实现Fiber架构
    • react-dom:DOM渲染器,负责浏览器渲染
    • react-server:服务端渲染核心,包含Fizz和Flight
    • react-client:客户端Flight解析
  3. 渲染模式:从CSR到SSR,再到Streaming SSR和RSC,React的渲染模式不断演进,每一次演进都是为了解决前一代方案的问题。

  4. 全栈协作:通过一个完整的请求生命周期示例,我们看到了服务端和客户端是如何通过Flight协议紧密协作的。

理解这些架构知识,是深入源码的基础。在接下来的章节中,我们将逐一深入各个模块的实现细节。


思考题

  1. 为什么React要将react包和react-dom包分开?这种设计有什么好处?

  2. Flight协议相比直接传输JSON有什么优势?为什么React要设计这种特殊的序列化格式?

  3. 在RSC模式下,如果一个Server Component需要使用useState,会发生什么?React是如何处理这种情况的?

  4. Streaming SSR和RSC都支持流式传输,它们的区别是什么?各自适用于什么场景?

  5. 假设你要为React添加一个新的渲染目标(比如渲染到Canvas),你需要修改哪些模块?