Skip to content

第10章 客户端Flight解析

本章将深入React客户端如何解析Flight协议数据,理解ReactFlightClient的实现、Chunk解析流程、组件树重建、Suspense与流式更新、缓存与复用机制。这是理解RSC完整数据流的关键章节。

在第9章中,我们学习了服务端如何通过Flight协议序列化React组件树。但数据发送到客户端后,客户端如何解析这些数据?如何将Flight格式的文本重建为React Element树?如何处理Client Component的动态加载?如何支持流式更新?

本章将逐一解答这些问题,带你深入理解React客户端Flight解析的完整流程。


10.1 ReactFlightClient源码分析

ReactFlightClient是客户端Flight协议的实现,负责解析服务端发送的Flight数据并重建React组件树。

10.1.1 createFromReadableStream入口

createFromReadableStream是Flight客户端的主要入口函数,接收服务端的ReadableStream并返回一个Promise。

javascript
// 文件:packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js
// 行号:182-220(React 19.3.0,简化版)

function createFromReadableStream<T>(
  stream: ReadableStream,
  options?: Options,
): Thenable<T> {
  // 1. 创建Response对象
  const response: Response = createResponse(
    options && options.serverConsumerManifest
      ? options.serverConsumerManifest.moduleMap
      : null,
    options && options.serverConsumerManifest
      ? options.serverConsumerManifest.serverModuleMap
      : null,
    options && options.moduleLoading ? options.moduleLoading : null,
    options && options.callServer ? options.callServer : undefined,
    options && options.encodeFormAction ? options.encodeFormAction : undefined,
    typeof options === 'object' && options !== null ? options.nonce : undefined,
    options && options.temporaryReferences
      ? options.temporaryReferences
      : undefined,
    __DEV__ && options && options.findSourceMapURL
      ? options.findSourceMapURL
      : undefined,
    __DEV__ && options ? options.replayConsole !== false : true,
    __DEV__ && options && options.environmentName
      ? options.environmentName
      : undefined,
  );
  
  // 2. 开始流式读取
  startReadingFromStream(response, stream);
  
  // 3. 返回根Chunk的Promise
  return getRoot(response);
}

参数说明

  1. stream:服务端返回的ReadableStream

    • 包含Flight格式的数据
    • 支持流式传输,边接收边解析
  2. options:配置选项

    • serverConsumerManifest:客户端模块映射表
      • moduleMap:Client Component模块映射
      • serverModuleMap:Server Action模块映射
    • moduleLoading:模块加载配置
    • callServer:调用Server Action的回调
    • encodeFormAction:表单action编码回调
    • nonce:CSP nonce
    • temporaryReferences:临时引用集合

返回值

返回一个Thenable(类Promise对象),resolve为根组件的渲染结果。

工作流程

  1. 创建Response对象(状态容器)
  2. 启动流式读取(startReadingFromStream)
  3. 返回根Chunk的Promise(getRoot)
  4. 客户端可以立即使用这个Promise进行渲染(Suspense会等待)

10.1.2 Response对象创建

Response对象是Flight客户端的核心状态容器,管理所有Chunk和解析状态。

javascript
// 文件:packages/react-client/src/ReactFlightClient.js
// 行号:2757-2800(React 19.3.0,简化版)

export function createResponse(
  bundlerConfig: ServerConsumerModuleMap,
  serverReferenceConfig: null | ServerManifest,
  moduleLoading: ModuleLoading,
  callServer: void | CallServerCallback,
  encodeFormAction: void | EncodeFormActionCallback,
  nonce: void | string,
  temporaryReferences: void | TemporaryReferenceSet,
): Response {
  // 创建Response实例
  const response: Response = {
    _bundlerConfig: bundlerConfig,
    _serverReferenceConfig: serverReferenceConfig,
    _moduleLoading: moduleLoading,
    _callServer: callServer !== undefined ? callServer : missingCall,
    _encodeFormAction: encodeFormAction,
    _nonce: nonce,
    _chunks: new Map(),
    _fromJSON: null,
    _rowState: 0,
    _rowID: 0,
    _rowTag: 0,
    _rowLength: 0,
    _buffer: [],
    _tempRefs: temporaryReferences,
  };
  
  // 设置JSON解析器
  response._fromJSON = createFromJSONCallback(response);
  
  return response;
}

Response对象的关键属性

  1. 配置属性

    • _bundlerConfig:模块映射配置
    • _serverReferenceConfig:Server Action配置
    • _moduleLoading:模块加载配置
    • _callServer:Server Action调用函数
    • _encodeFormAction:表单action编码函数
    • _nonce:CSP nonce
  2. Chunk管理

    • _chunks:Map<number, Chunk>,存储所有解析的Chunk
    • 每个Chunk有唯一的ID
    • Chunk可以相互引用
  3. 解析状态

    • _rowState:当前行的解析状态
    • _rowID:当前行的ID
    • _rowTag:当前行的类型标签
    • _rowLength:当前行的剩余长度
    • _buffer:当前行的数据缓冲区
  4. 辅助属性

    • _fromJSON:JSON解析回调函数
    • _tempRefs:临时引用集合

10.1.3 流式解析机制

Flight客户端支持流式解析,边接收数据边解析,无需等待完整数据。

javascript
// 文件:packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js
// 简化版

function startReadingFromStream(
  response: Response,
  stream: ReadableStream,
): void {
  // 获取Reader
  const reader = stream.getReader();
  
  // 递归读取函数
  function progress({ done, value }: ReadableStreamReadResult): void {
    if (done) {
      // 流结束
      close(response);
      return;
    }
    
    // 处理接收到的数据块
    const buffer: Uint8Array = value;
    processBinaryChunk(response, buffer);
    
    // 继续读取下一块
    reader.read().then(progress).catch(error);
  }
  
  function error(e: mixed) {
    reportGlobalError(response, e);
  }
  
  // 开始读取
  reader.read().then(progress).catch(error);
}

流式解析的优势

  1. 更快的首屏渲染:不需要等待完整数据,接收到第一个Chunk就可以开始渲染
  2. 更好的用户体验:渐进式显示内容,而不是长时间白屏
  3. 更低的内存占用:不需要在内存中保存完整的数据
  4. 支持大型响应:可以处理任意大小的数据流

流式解析流程图


10.2 Chunk解析流程

Flight数据以行为单位传输,每行是一个Chunk。客户端需要逐行解析这些Chunk。

10.2.1 行解析器

行解析器负责将字节流分割成一行一行的Chunk数据。

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

function processBinaryChunk(
  response: Response,
  chunk: Uint8Array,
): void {
  let i = 0;
  let rowState = response._rowState;
  let rowID = response._rowID;
  let rowTag = response._rowTag;
  let rowLength = response._rowLength;
  const buffer = response._buffer;
  const chunkLength = chunk.length;
  
  while (i < chunkLength) {
    let lastIdx = -1;
    
    switch (rowState) {
      case ROW_ID: {
        // 解析行ID
        const byte = chunk[i++];
        if (byte === 58 /* ":" */) {
          // ID解析完成,开始解析tag
          rowState = ROW_TAG;
        } else {
          // 继续解析ID(十六进制)
          rowID = (rowID << 4) | (byte > 96 ? byte - 87 : byte - 48);
        }
        continue;
      }
      
      case ROW_TAG: {
        // 解析类型标签
        const resolvedRowTag = chunk[i];
        
        if (
          resolvedRowTag === 84 /* "T" */ ||
          resolvedRowTag === 65 /* "A" */ ||
          resolvedRowTag === 79 /* "O" */ ||
          resolvedRowTag === 111 /* "o" */ ||
          resolvedRowTag === 85 /* "U" */ ||
          resolvedRowTag === 83 /* "S" */ ||
          resolvedRowTag === 115 /* "s" */ ||
          resolvedRowTag === 76 /* "L" */ ||
          resolvedRowTag === 108 /* "l" */ ||
          resolvedRowTag === 71 /* "G" */ ||
          resolvedRowTag === 103 /* "g" */ ||
          resolvedRowTag === 77 /* "M" */ ||
          resolvedRowTag === 109 /* "m" */ ||
          resolvedRowTag === 86 /* "V" */ ||
          resolvedRowTag === 118 /* "v" */
        ) {
          rowTag = resolvedRowTag;
          rowState = ROW_LENGTH;
          i++;
        } else if (
          resolvedRowTag > 64 && resolvedRowTag < 91 /* "A"-"Z" */
        ) {
          rowTag = resolvedRowTag;
          rowState = ROW_CHUNK_BY_NEWLINE;
          i++;
        } else {
          rowTag = 0;
          rowState = ROW_CHUNK_BY_NEWLINE;
        }
        continue;
      }

      case ROW_LENGTH: {
        // 解析长度(用于二进制数据)
        const byte = chunk[i++];
        if (byte === 44 /* "," */) {
          // 长度解析完成
          rowState = ROW_CHUNK_BY_LENGTH;
        } else {
          rowLength = (rowLength << 4) | (byte > 96 ? byte - 87 : byte - 48);
        }
        continue;
      }
      
      case ROW_CHUNK_BY_NEWLINE: {
        // 按换行符分割的Chunk
        lastIdx = chunk.indexOf(10 /* "\n" */, i);
        break;
      }
      
      case ROW_CHUNK_BY_LENGTH: {
        // 按长度分割的Chunk(二进制数据)
        lastIdx = i + rowLength;
        if (lastIdx > chunkLength) {
          lastIdx = -1;
        }
        break;
      }
    }
    
    const offset = chunk.byteOffset + i;
    
    if (lastIdx > -1) {
      // 找到完整的行
      const length = lastIdx - i;
      const lastChunk = new Uint8Array(chunk.buffer, offset, length);
      processFullRow(response, rowID, rowTag, buffer, lastChunk);
      
      // 重置状态,准备解析下一行
      i = lastIdx;
      if (rowState === ROW_CHUNK_BY_NEWLINE) {
        i++; // 跳过换行符
      }
      rowState = ROW_ID;
      rowTag = 0;
      rowID = 0;
      rowLength = 0;
      buffer.length = 0;
    } else {
      // 行未完成,保存到buffer
      const length = chunkLength - i;
      const remainingSlice = new Uint8Array(chunk.buffer, offset, length);
      buffer.push(remainingSlice);
      rowLength -= length;
      break;
    }
  }
  
  // 保存解析状态
  response._rowState = rowState;
  response._rowID = rowID;
  response._rowTag = rowTag;
  response._rowLength = rowLength;
}

行解析状态机

10.2.2 类型分发

当解析完一行后,根据rowTag(类型标签)分发到不同的处理函数。

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

function processFullRow(
  response: Response,
  id: number,
  tag: number,
  buffer: Array<Uint8Array>,
  chunk: Uint8Array,
): void {
  // 合并buffer和chunk
  const stringDecoder = createStringDecoder();
  let row = '';
  for (let i = 0; i < buffer.length; i++) {
    row += readPartialStringChunk(stringDecoder, buffer[i]);
  }
  row += readFinalStringChunk(stringDecoder, chunk);
  
  // 根据tag分发
  switch (tag) {
    case 73 /* "I" */: {
      // Import Chunk - 模块引用
      resolveModule(response, id, row);
      return;
    }
    case 72 /* "H" */: {
      // Hint Chunk - 资源提示
      const code = row[0];
      resolveHint(response, code, row.slice(1));
      return;
    }
    case 69 /* "E" */: {
      // Error Chunk - 错误
      const errorInfo = JSON.parse(row);
      resolveError(response, id, errorInfo);
      return;
    }
    case 84 /* "T" */: {
      // Text Chunk - 文本
      resolveText(response, id, row);
      return;
    }
    case 68 /* "D" */: {
      // Debug Chunk - 调试信息
      if (__DEV__) {
        const debugInfo = JSON.parse(row);
        resolveDebugInfo(response, id, debugInfo);
      }
      return;
    }
    case 87 /* "W" */: {
      // Console Replay Chunk
      if (__DEV__) {
        resolveConsoleEntry(response, row);
      }
      return;
    }
    default: {
      // Model Chunk - 默认类型
      resolveModel(response, id, row);
      return;
    }
  }
}

Chunk类型对照表

Tag字符类型处理函数
73IImport ChunkresolveModule
72HHint ChunkresolveHint
69EError ChunkresolveError
84TText ChunkresolveText
68DDebug ChunkresolveDebugInfo
87WConsole ChunkresolveConsoleEntry
0Model ChunkresolveModel

10.2.3 引用解析

Flight协议使用@id格式表示引用,客户端需要解析这些引用并替换为实际的Chunk。

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

function parseModelString(
  response: Response,
  parentObject: Object,
  key: string,
  value: string,
): any {
  // 检查是否是引用
  if (value[0] === '$') {
    if (value === '$') {
      // React Element标记
      return REACT_ELEMENT_TYPE;
    }
    
    switch (value[1]) {
      case '$': {
        // "$$" -> "$" (转义)
        return value.slice(1);
      }
      case '@': {
        // "$@123" -> 引用ID为123的Chunk
        const id = parseInt(value.slice(2), 16);
        const chunk = getChunk(response, id);
        return chunk;
      }
      case 'S': {
        // "$S1" -> 引用Symbol
        return Symbol.for(value.slice(2));
      }
      case 'F': {
        // "$F123" -> Server Action引用
        const id = parseInt(value.slice(2), 16);
        return createServerReference(response, id);
      }
      case 'Q': {
        // "$Q123" -> Map引用
        const id = parseInt(value.slice(2), 16);
        const chunk = getChunk(response, id);
        return new Map(chunk);
      }
      case 'W': {
        // "$W123" -> Set引用
        const id = parseInt(value.slice(2), 16);
        const chunk = getChunk(response, id);
        return new Set(chunk);
      }
      case 'K': {
        // "$K123" -> FormData引用
        const id = parseInt(value.slice(2), 16);
        const chunk = getChunk(response, id);
        return chunk;
      }
      case 'undefined': {
        // "$undefined" -> undefined
        return undefined;
      }
      case 'Infinity': {
        return Infinity;
      }
      case '-Infinity': {
        return -Infinity;
      }
      case 'NaN': {
        return NaN;
      }
    }
  }
  
  // 普通字符串
  return value;
}

引用格式说明

  1. $@id:引用其他Chunk

    • 示例:"$@5" 引用ID为5的Chunk
    • 客户端会查找并返回对应的Chunk
  2. $Ssymbol:Symbol引用

    • 示例:"$Sreact.element" 引用react.element Symbol
    • 使用Symbol.for创建
  3. $Fid:Server Action引用

    • 示例:"$F3" 引用ID为3的Server Action
    • 创建可调用的函数引用
  4. $$:转义的$

    • 示例:"$$price" 表示字符串"$price"
  5. 特殊值

    • "$undefined" → undefined
    • "$Infinity" → Infinity
    • "$-Infinity" → -Infinity
    • "$NaN" → NaN

引用解析示例

javascript
// Flight数据
1:I{"id":"./Button.js","chunks":["client1"],"name":"default"}
2:["$","div",null,{"children":"$@3"}]
3:"Hello World"
0:["$","$@1",null,{"onClick":"$@2"}]

// 解析过程
// 1. 解析Chunk 1 -> Import Chunk,创建模块引用
// 2. 解析Chunk 3 -> "Hello World"
// 3. 解析Chunk 2 -> React Element,children引用Chunk 3
//    结果:<div>Hello World</div>
// 4. 解析Chunk 0 -> React Element,type引用Chunk 1,onClick引用Chunk 2
//    结果:<Button onClick={<div>Hello World</div>} />

10.3 组件树重建

客户端解析Flight数据后,需要将其重建为React Element树。

10.3.1 从Flight数据到React Element

Flight数据使用数组格式表示React Element:["$", type, key, props]

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

function createReactElement(
  type: any,
  key: null | string,
  props: any,
): ReactElement {
  // 创建React Element对象
  const element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: null,
    props: props,
    _owner: null,
  };
  
  if (__DEV__) {
    // 开发环境添加额外信息
    element._store = {};
    Object.defineProperty(element._store, 'validated', {
      configurable: false,
      enumerable: false,
      writable: true,
      value: false,
    });
    Object.defineProperty(element, '_self', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: null,
    });
    Object.defineProperty(element, '_source', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: null,
    });
  }
  
  return element;
}

function resolveModel(
  response: Response,
  id: number,
  model: string,
): void {
  // 解析JSON
  const json = JSON.parse(model, response._fromJSON);
  
  // 获取或创建Chunk
  const chunk = getChunk(response, id);
  
  // 检查是否是React Element
  if (isArray(json) && json[0] === '$') {
    // ["$", type, key, props]
    const type = json[1];
    const key = json[2];
    const props = json[3];
    
    // 创建React Element
    const element = createReactElement(type, key, props);
    
    // 解析完成,标记Chunk为resolved
    resolveModelChunk(chunk, element);
  } else {
    // 普通数据
    resolveModelChunk(chunk, json);
  }
}

React Element重建示例

javascript
// Flight数据
0:["$","div",null,{"className":"container","children":[["$","h1",null,{"children":"Title"}],["$","p",null,{"children":"Content"}]]}]

// 解析后的React Element
{
  $$typeof: Symbol(react.element),
  type: 'div',
  key: null,
  props: {
    className: 'container',
    children: [
      {
        $$typeof: Symbol(react.element),
        type: 'h1',
        key: null,
        props: { children: 'Title' }
      },
      {
        $$typeof: Symbol(react.element),
        type: 'p',
        key: null,
        props: { children: 'Content' }
      }
    ]
  }
}

// 等价于JSX
<div className="container">
  <h1>Title</h1>
  <p>Content</p>
</div>

10.3.2 Client Component的加载

当遇到Client Component引用时,客户端需要动态加载对应的模块。

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

function resolveModule(
  response: Response,
  id: number,
  model: string,
): void {
  // 解析模块信息
  const moduleData = JSON.parse(model);
  // moduleData = {
  //   id: "./Button.js",
  //   chunks: ["client1"],
  //   name: "default"
  // }
  
  // 获取Chunk
  const chunk = getChunk(response, id);
  
  // 创建模块引用
  const moduleReference = resolveClientReference(
    response._bundlerConfig,
    moduleData,
  );
  
  // 预加载模块
  preloadModule(moduleReference);
  
  // 标记Chunk为resolved
  resolveModuleChunk(chunk, moduleReference);
}

function resolveClientReference(
  bundlerConfig: ServerConsumerModuleMap,
  moduleData: ModuleMetaData,
): ClientReference {
  // 从bundlerConfig查找模块
  const moduleExports = bundlerConfig[moduleData.id];
  
  if (!moduleExports) {
    throw new Error(`Module not found: ${moduleData.id}`);
  }
  
  // 返回模块引用
  if (moduleData.name === 'default') {
    return moduleExports;
  } else if (moduleData.name === '*') {
    return moduleExports;
  } else {
    return moduleExports[moduleData.name];
  }
}

Client Component加载流程

10.3.3 懒加载与代码分割

Client Component天然支持代码分割,每个Client Component都是一个独立的代码分割点。

javascript
// 服务端
// ProductPage.js - Server Component
async function ProductPage() {
  const products = await db.query('SELECT * FROM products');
  
  return (
    <div>
      <ProductList products={products} />
      {/* AddToCart是Client Component,会被代码分割 */}
      <AddToCart />
    </div>
  );
}

// AddToCart.js - Client Component
'use client';
import { useState } from 'react';

export default function AddToCart() {
  const [count, setCount] = useState(1);
  return <button onClick={() => setCount(count + 1)}>Add {count}</button>;
}

生成的Flight数据

1:I{"id":"./AddToCart.js","chunks":["client-addtocart"],"name":"default"}
2:["$","div",null,{"children":[["$","div",null,{"children":"Product List"}],["$","@1",null,{}]]}]
0:"$@2"

客户端加载过程

  1. 解析Chunk 1,创建AddToCart的模块引用
  2. 预加载client-addtocart.js(异步)
  3. 解析Chunk 2,创建React Element树
  4. 渲染时,遇到AddToCart引用
  5. 等待模块加载完成(如果还未加载)
  6. 使用加载的模块渲染AddToCart组件

代码分割的优势

  1. 按需加载:只有用到的Client Component才会加载
  2. 并行加载:多个Client Component可以并行加载
  3. 缓存友好:每个组件独立打包,更新时只需重新加载变更的组件
  4. 更小的初始包:首屏不需要的组件不会包含在初始包中

10.4 Suspense与流式更新

Flight协议支持流式传输,配合Suspense可以实现渐进式渲染。

10.4.1 等待服务端数据

当客户端尝试访问一个尚未接收的Chunk时,会抛出Promise,触发Suspense。

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

function getChunk(response: Response, id: number): Chunk {
  // 从chunks Map中查找
  const chunks = response._chunks;
  let chunk = chunks.get(id);
  
  if (!chunk) {
    // Chunk不存在,创建pending状态的Chunk
    chunk = createPendingChunk(response);
    chunks.set(id, chunk);
  }
  
  return chunk;
}

function createPendingChunk(response: Response): Chunk {
  // 创建pending状态的Promise
  const thenable = new Promise((resolve, reject) => {
    // 保存resolve和reject,等待数据到达时调用
  });
  
  const chunk: Chunk = {
    status: PENDING,
    value: thenable,
    reason: null,
    _response: response,
    then(resolve, reject) {
      // 实现thenable接口
      if (chunk.status === PENDING) {
        if (chunk.value === null) {
          chunk.value = [];
        }
        chunk.value.push(resolve);
        if (chunk.reason === null) {
          chunk.reason = [];
        }
        chunk.reason.push(reject);
      } else if (chunk.status === RESOLVED) {
        resolve(chunk.value);
      } else {
        reject(chunk.reason);
      }
    },
  };
  
  return chunk;
}

Chunk的状态转换

10.4.2 增量更新UI

当新的Chunk到达时,客户端会触发React重新渲染,更新UI。

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

function resolveModelChunk(chunk: Chunk, value: any): void {
  if (chunk.status !== PENDING) {
    // Chunk已经resolved,忽略
    return;
  }
  
  // 更新Chunk状态
  chunk.status = RESOLVED;
  chunk.value = value;
  chunk.reason = null;
  
  // 触发所有等待的Promise
  const callbacks = chunk.value; // pending时保存的resolve回调
  if (callbacks !== null) {
    for (let i = 0; i < callbacks.length; i++) {
      const callback = callbacks[i];
      callback(value);
    }
  }
}

function wakeChunk(listeners: Array<(value: any) => void>, value: any): void {
  // 唤醒所有等待的组件
  for (let i = 0; i < listeners.length; i++) {
    const listener = listeners[i];
    listener(value);
  }
}

流式更新示例

jsx
// 服务端
async function ProductPage() {
  return (
    <div>
      <h1>Products</h1>
      <Suspense fallback={<div>Loading products...</div>}>
        <ProductList />
      </Suspense>
      <Suspense fallback={<div>Loading reviews...</div>}>
        <ReviewList />
      </Suspense>
    </div>
  );
}

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

async function ReviewList() {
  await delay(1000); // 模拟慢查询
  const reviews = await db.query('SELECT * FROM reviews');
  return <div>{reviews.map(r => <div key={r.id}>{r.text}</div>)}</div>;
}

Flight数据流

// 第一批数据(立即发送)
0:["$","div",null,{"children":[["$","h1",null,{"children":"Products"}],["$","$L1",null,{"fallback":["$","div",null,{"children":"Loading products..."}],"children":"$@2"}],["$","$L3",null,{"fallback":["$","div",null,{"children":"Loading reviews..."}],"children":"$@4"}]]}]

// 第二批数据(ProductList完成)
2:["$","div",null,{"children":[["$","div","p1",{"children":"Product 1"}],["$","div","p2",{"children":"Product 2"}]]}]

// 第三批数据(ReviewList完成,1秒后)
4:["$","div",null,{"children":[["$","div","r1",{"children":"Great!"}],["$","div","r2",{"children":"Nice!"}]]}]

渲染时间线

10.4.3 示例:RSC的渐进式加载

让我们通过一个完整的例子理解RSC的渐进式加载。

服务端代码

jsx
// app/dashboard/page.js
export default async function Dashboard() {
  return (
    <div className="dashboard">
      <Header />
      <div className="content">
        <Suspense fallback={<Skeleton />}>
          <UserProfile />
        </Suspense>
        <Suspense fallback={<Skeleton />}>
          <RecentActivity />
        </Suspense>
        <Suspense fallback={<Skeleton />}>
          <Analytics />
        </Suspense>
      </div>
    </div>
  );
}

// Header是同步的,立即渲染
function Header() {
  return <header><h1>Dashboard</h1></header>;
}

// UserProfile - 快速查询(100ms)
async function UserProfile() {
  const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
  return (
    <div className="profile">
      <img src={user.avatar} />
      <h2>{user.name}</h2>
    </div>
  );
}

// RecentActivity - 中等查询(500ms)
async function RecentActivity() {
  const activities = await db.query(
    'SELECT * FROM activities WHERE user_id = ? ORDER BY created_at DESC LIMIT 10',
    [userId]
  );
  return (
    <div className="activity">
      {activities.map(a => (
        <div key={a.id}>{a.description}</div>
      ))}
    </div>
  );
}

// Analytics - 慢查询(2000ms)
async function Analytics() {
  const stats = await db.query(
    'SELECT * FROM analytics WHERE user_id = ? AND date > ?',
    [userId, lastMonth]
  );
  return (
    <div className="analytics">
      <Chart data={stats} />
    </div>
  );
}

客户端渲染过程

javascript
// 1. 初始渲染(0ms)
<div className="dashboard">
  <header><h1>Dashboard</h1></header>
  <div className="content">
    <Skeleton /> {/* UserProfile pending */}
    <Skeleton /> {/* RecentActivity pending */}
    <Skeleton /> {/* Analytics pending */}
  </div>
</div>

// 2. UserProfile完成(100ms)
<div className="dashboard">
  <header><h1>Dashboard</h1></header>
  <div className="content">
    <div className="profile">
      <img src="avatar.jpg" />
      <h2>John Doe</h2>
    </div>
    <Skeleton /> {/* RecentActivity pending */}
    <Skeleton /> {/* Analytics pending */}
  </div>
</div>

// 3. RecentActivity完成(500ms)
<div className="dashboard">
  <header><h1>Dashboard</h1></header>
  <div className="content">
    <div className="profile">...</div>
    <div className="activity">
      <div>Logged in</div>
      <div>Updated profile</div>
      ...
    </div>
    <Skeleton /> {/* Analytics pending */}
  </div>
</div>

// 4. Analytics完成(2000ms)
<div className="dashboard">
  <header><h1>Dashboard</h1></header>
  <div className="content">
    <div className="profile">...</div>
    <div className="activity">...</div>
    <div className="analytics">
      <Chart data={...} />
    </div>
  </div>
</div>

用户体验对比

方案首屏时间完整内容时间用户体验
传统SSR2000ms2000ms长时间白屏
流式SSR0ms2000ms立即显示骨架,渐进加载
CSR0ms2000ms + 网络延迟需要额外的API请求

10.5 缓存与复用

Flight客户端实现了多层缓存机制,提高性能和用户体验。

10.5.1 Flight响应缓存

客户端会缓存Flight响应,避免重复请求。

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

// 全局缓存Map
const flightCache = new Map<string, Response>();

function createFromFetch<T>(
  promiseForResponse: Promise<Response>,
  options?: Options,
): Thenable<T> {
  // 生成缓存key
  const cacheKey = generateCacheKey(promiseForResponse);
  
  // 检查缓存
  const cachedResponse = flightCache.get(cacheKey);
  if (cachedResponse) {
    return getRoot(cachedResponse);
  }
  
  // 创建新的Response
  const response = createResponse(
    options && options.serverConsumerManifest
      ? options.serverConsumerManifest.moduleMap
      : null,
    // ... 其他参数
  );
  
  // 保存到缓存
  flightCache.set(cacheKey, response);
  
  // 开始读取
  promiseForResponse.then(
    (r) => startReadingFromStream(response, r.body),
    (e) => reportGlobalError(response, e),
  );
  
  return getRoot(response);
}

缓存策略

  1. 基于URL的缓存

    • 相同URL的请求复用缓存
    • 适用于静态内容
  2. 基于时间的失效

    • 设置缓存过期时间
    • 过期后重新请求
  3. 手动失效

    • 提供API清除缓存
    • 用于数据更新后刷新

缓存示例

javascript
// 第一次访问 /products
const response1 = createFromFetch(fetch('/rsc/products'));
// 发起网络请求,创建Response对象

// 第二次访问 /products(在缓存有效期内)
const response2 = createFromFetch(fetch('/rsc/products'));
// 直接返回缓存的Response对象,无需网络请求

// 清除缓存
flightCache.delete('/rsc/products');

10.5.2 组件实例复用

React会复用已经渲染的组件实例,避免不必要的重新渲染。

javascript
// Fiber reconciliation会比较新旧树
function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
): void {
  if (current === null) {
    // 首次渲染,创建新的Fiber
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 更新渲染,复用现有Fiber
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

复用条件

  1. type相同:组件类型没有变化
  2. key相同:React key没有变化
  3. props浅比较:props没有变化(对于memo组件)

复用示例

jsx
// 初始渲染
<ProductList products={[{id: 1, name: 'A'}, {id: 2, name: 'B'}]} />

// 更新渲染(添加一个产品)
<ProductList products={[{id: 1, name: 'A'}, {id: 2, name: 'B'}, {id: 3, name: 'C'}]} />

// React会:
// 1. 复用id=1和id=2的组件实例
// 2. 只创建id=3的新实例
// 3. 避免重新渲染id=1和id=2

10.5.3 导航时的优化

在客户端导航时,React会智能地复用和更新组件。

javascript
// Next.js App Router示例
// 从 /products 导航到 /products/123

// 布局组件会被复用
<Layout>
  {/* 只有这部分会重新获取和渲染 */}
  <ProductDetail id={123} />
</Layout>

导航优化策略

  1. 部分预取

    • 预取可见链接的Flight数据
    • 鼠标悬停时预取
  2. 乐观更新

    • 立即更新UI
    • 后台获取实际数据
    • 数据到达后替换
  3. 共享布局

    • 复用不变的布局组件
    • 只更新变化的部分

预取示例

jsx
// Link组件会自动预取
<Link href="/products/123" prefetch={true}>
  View Product
</Link>

// 当用户悬停在链接上时:
// 1. 发起Flight请求:GET /rsc/products/123
// 2. 解析并缓存响应
// 3. 用户点击时立即显示(从缓存)

缓存层次图


10.6 综合案例:实现一个简易Flight客户端

让我们通过实现一个简化版的Flight客户端,深入理解客户端解析的完整流程。

目标

实现一个简单的Flight客户端,支持:

  1. 解析Flight行格式数据
  2. 处理Model Chunk和Import Chunk
  3. 重建React Element树
  4. 支持引用解析

实现代码

javascript
// mini-flight-client.js

class FlightClient {
  constructor(moduleMap = {}) {
    this.moduleMap = moduleMap;
    this.chunks = new Map();
    this.pendingChunks = new Map();
    this.buffer = '';
  }
  
  // 处理接收到的数据
  processData(data) {
    this.buffer += data;
    
    // 按行分割
    const lines = this.buffer.split('\n');
    
    // 保留最后一行(可能不完整)
    this.buffer = lines.pop();
    
    // 处理每一行
    for (const line of lines) {
      if (line) {
        this.processLine(line);
      }
    }
  }
  
  // 处理一行数据
  processLine(line) {
    // 解析格式:<id>:<type><data>
    const colonIndex = line.indexOf(':');
    if (colonIndex === -1) return;
    
    const id = parseInt(line.slice(0, colonIndex), 16);
    const rest = line.slice(colonIndex + 1);
    
    // 检查类型标识符
    const type = rest[0];
    
    if (type === 'I') {
      // Import Chunk
      const data = rest.slice(1);
      this.resolveModule(id, data);
    } else if (type === 'E') {
      // Error Chunk
      const data = rest.slice(1);
      this.resolveError(id, data);
    } else {
      // Model Chunk(默认)
      this.resolveModel(id, rest);
    }
  }
  
  // 解析Model Chunk
  resolveModel(id, data) {
    try {
      // 使用自定义JSON解析器
      const value = JSON.parse(data, (key, value) => {
        return this.parseValue(value);
      });
      
      // 保存Chunk
      this.chunks.set(id, value);
      
      // 触发等待的Promise
      this.wakeChunk(id, value);
    } catch (error) {
      console.error('Failed to parse model:', error);
      this.resolveError(id, { message: error.message });
    }
  }
  
  // 解析值(处理引用)
  parseValue(value) {
    if (typeof value !== 'string') {
      return value;
    }
    
    // 检查特殊标记
    if (value[0] === '$') {
      if (value === '$') {
        // React Element标记
        return Symbol.for('react.element');
      }
      
      if (value[1] === '@') {
        // 引用:$@123
        const refId = parseInt(value.slice(2), 16);
        return this.getChunk(refId);
      }
      
      if (value[1] === 'S') {
        // Symbol:$Sreact.element
        return Symbol.for(value.slice(2));
      }
      
      if (value === '$undefined') {
        return undefined;
      }
    }
    
    return value;
  }
  
  // 获取Chunk(如果不存在则创建pending)
  getChunk(id) {
    if (this.chunks.has(id)) {
      return this.chunks.get(id);
    }
    
    // 创建pending Promise
    const pending = new Promise((resolve, reject) => {
      if (!this.pendingChunks.has(id)) {
        this.pendingChunks.set(id, []);
      }
      this.pendingChunks.get(id).push({ resolve, reject });
    });
    
    // 抛出Promise,触发Suspense
    throw pending;
  }
  
  // 唤醒等待的Chunk
  wakeChunk(id, value) {
    const pending = this.pendingChunks.get(id);
    if (pending) {
      for (const { resolve } of pending) {
        resolve(value);
      }
      this.pendingChunks.delete(id);
    }
  }
  
  // 解析Import Chunk
  resolveModule(id, data) {
    const moduleInfo = JSON.parse(data);
    // moduleInfo = { id: "./Button.js", chunks: ["client1"], name: "default" }
    
    // 从moduleMap获取模块
    const moduleExports = this.moduleMap[moduleInfo.id];
    
    if (!moduleExports) {
      console.error(`Module not found: ${moduleInfo.id}`);
      return;
    }
    
    // 获取导出
    const moduleRef = moduleInfo.name === 'default'
      ? moduleExports
      : moduleExports[moduleInfo.name];
    
    // 保存模块引用
    this.chunks.set(id, moduleRef);
    this.wakeChunk(id, moduleRef);
  }
  
  // 解析Error Chunk
  resolveError(id, data) {
    const errorInfo = typeof data === 'string' ? JSON.parse(data) : data;
    const error = new Error(errorInfo.message || 'An error occurred');
    error.digest = errorInfo.digest;
    
    // 触发等待的Promise(reject)
    const pending = this.pendingChunks.get(id);
    if (pending) {
      for (const { reject } of pending) {
        reject(error);
      }
      this.pendingChunks.delete(id);
    }
  }
  
  // 获取根Chunk
  getRoot() {
    return this.getChunk(0);
  }
}

// 导出
module.exports = { FlightClient };

使用示例

javascript
// example.js
const { FlightClient } = require('./mini-flight-client');

// 1. 定义模块映射
const moduleMap = {
  './Button.js': function Button({ children, onClick }) {
    return { type: 'button', props: { children, onClick } };
  },
};

// 2. 创建客户端
const client = new FlightClient(moduleMap);

// 3. 模拟接收Flight数据
const flightData = `1:I{"id":"./Button.js","chunks":["client1"],"name":"default"}
2:["$","h1",null,{"children":"Hello"}]
3:["$","@1",null,{"children":"Click me","onClick":"handleClick"}]
0:["$","div",null,{"children":["@2","@3"]}]
`;

// 4. 处理数据
client.processData(flightData);

// 5. 获取根元素
try {
  const root = client.getRoot();
  console.log('Root element:', JSON.stringify(root, null, 2));
} catch (promise) {
  // 如果抛出Promise,说明数据还未完全到达
  promise.then(() => {
    const root = client.getRoot();
    console.log('Root element:', JSON.stringify(root, null, 2));
  });
}

输出结果

json
{
  "type": "div",
  "props": {
    "children": [
      {
        "type": "h1",
        "props": {
          "children": "Hello"
        }
      },
      {
        "type": "button",
        "props": {
          "children": "Click me",
          "onClick": "handleClick"
        }
      }
    ]
  }
}

流式接收示例

javascript
// stream-example.js
const { FlightClient } = require('./mini-flight-client');

const client = new FlightClient(moduleMap);

// 模拟流式接收数据
const chunks = [
  '1:I{"id":"./Button.js","chunks":["client1"],"name":"default"}\n',
  '2:["$","h1",null,{"children":"Hello"}]\n',
  '3:["$","@1",null,{"children":"Click me"}]\n',
  '0:["$","div",null,{"children":["@2","@3"]}]\n',
];

// 逐个处理chunk
chunks.forEach((chunk, index) => {
  console.log(`\n--- Processing chunk ${index} ---`);
  client.processData(chunk);
  
  try {
    const root = client.getRoot();
    console.log('Root resolved:', root);
  } catch (promise) {
    console.log('Root still pending...');
  }
});

测试用例

javascript
// test.js
const { FlightClient } = require('./mini-flight-client');

// 测试1:简单元素
function test1() {
  const client = new FlightClient();
  client.processData('0:["$","div",null,{"children":"Hello"}]\n');
  
  const root = client.getRoot();
  console.assert(root.type === 'div');
  console.assert(root.props.children === 'Hello');
  console.log('✓ Test 1 passed');
}

// 测试2:引用
function test2() {
  const client = new FlightClient();
  client.processData('1:"World"\n');
  client.processData('0:["$","div",null,{"children":"$@1"}]\n');
  
  const root = client.getRoot();
  console.assert(root.props.children === 'World');
  console.log('✓ Test 2 passed');
}

// 测试3:Client Component
function test3() {
  const Button = () => ({ type: 'button' });
  const client = new FlightClient({ './Button.js': Button });
  
  client.processData('1:I{"id":"./Button.js","chunks":[],"name":"default"}\n');
  client.processData('0:["$","@1",null,{}]\n');
  
  const root = client.getRoot();
  console.assert(root.type === 'button');
  console.log('✓ Test 3 passed');
}

// 测试4:流式接收
async function test4() {
  const client = new FlightClient();
  
  // 第一批数据
  client.processData('1:"Loading..."\n');
  client.processData('0:["$","div",null,{"children":"$@1"}]\n');
  
  let root = client.getRoot();
  console.assert(root.props.children === 'Loading...');
  
  // 第二批数据(更新)
  client.processData('1:"Loaded!"\n');
  
  root = client.getRoot();
  console.assert(root.props.children === 'Loaded!');
  console.log('✓ Test 4 passed');
}

// 运行测试
test1();
test2();
test3();
test4();

扩展练习

  1. 添加Error处理

    • 实现完整的Error Chunk处理
    • 支持错误边界
  2. 添加Symbol支持

    • 处理React内置Symbol
    • 支持自定义Symbol
  3. 优化性能

    • 使用WeakMap缓存对象
    • 实现Chunk优先级
  4. 添加调试功能

    • 记录解析日志
    • 可视化Chunk依赖关系

本章小结

本章深入讲解了React客户端如何解析Flight协议数据:

  1. ReactFlightClient源码分析

    • createFromReadableStream:客户端入口函数
    • Response对象:核心状态容器,管理所有Chunk
    • 流式解析机制:边接收边解析,无需等待完整数据
  2. Chunk解析流程

    • 行解析器:将字节流分割成行,使用状态机解析
    • 类型分发:根据类型标签分发到不同处理函数
    • 引用解析:处理$@id$S$F等特殊标记
  3. 组件树重建

    • React Element重建:从Flight数组格式创建React Element
    • Client Component加载:动态import模块
    • 懒加载与代码分割:每个Client Component自动成为分割点
  4. Suspense与流式更新

    • 等待服务端数据:pending Chunk抛出Promise触发Suspense
    • 增量更新UI:新Chunk到达时触发重新渲染
    • 渐进式加载:快速组件先显示,慢速组件后显示
  5. 缓存与复用

    • Flight响应缓存:避免重复请求
    • 组件实例复用:React Fiber reconciliation
    • 导航优化:预取、乐观更新、共享布局
  6. 综合案例

    • 手写Flight客户端
    • 理解解析流程
    • 掌握Flight协议细节

思考题

  1. 为什么Flight客户端使用流式解析而不是等待完整数据?
  2. 当访问一个pending Chunk时,为什么要抛出Promise?
  3. Client Component的代码分割是如何实现的?
  4. 如何优化多次导航到同一页面的性能?
  5. 如果服务端发送了错误的Chunk ID引用,客户端会如何处理?

下一章预告

第11章将讲解Server Actions与Hydration,包括Server Actions的实现原理、表单处理、Hydration水合机制、Selective Hydration等内容,完整呈现React全栈应用的数据流。