Appearance
第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);
}参数说明
stream:服务端返回的ReadableStream
- 包含Flight格式的数据
- 支持流式传输,边接收边解析
options:配置选项
- serverConsumerManifest:客户端模块映射表
- moduleMap:Client Component模块映射
- serverModuleMap:Server Action模块映射
- moduleLoading:模块加载配置
- callServer:调用Server Action的回调
- encodeFormAction:表单action编码回调
- nonce:CSP nonce
- temporaryReferences:临时引用集合
- serverConsumerManifest:客户端模块映射表
返回值
返回一个Thenable(类Promise对象),resolve为根组件的渲染结果。
工作流程
- 创建Response对象(状态容器)
- 启动流式读取(startReadingFromStream)
- 返回根Chunk的Promise(getRoot)
- 客户端可以立即使用这个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对象的关键属性
配置属性
- _bundlerConfig:模块映射配置
- _serverReferenceConfig:Server Action配置
- _moduleLoading:模块加载配置
- _callServer:Server Action调用函数
- _encodeFormAction:表单action编码函数
- _nonce:CSP nonce
Chunk管理
- _chunks:Map<number, Chunk>,存储所有解析的Chunk
- 每个Chunk有唯一的ID
- Chunk可以相互引用
解析状态
- _rowState:当前行的解析状态
- _rowID:当前行的ID
- _rowTag:当前行的类型标签
- _rowLength:当前行的剩余长度
- _buffer:当前行的数据缓冲区
辅助属性
- _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);
}流式解析的优势
- 更快的首屏渲染:不需要等待完整数据,接收到第一个Chunk就可以开始渲染
- 更好的用户体验:渐进式显示内容,而不是长时间白屏
- 更低的内存占用:不需要在内存中保存完整的数据
- 支持大型响应:可以处理任意大小的数据流
流式解析流程图
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 | 字符 | 类型 | 处理函数 |
|---|---|---|---|
| 73 | I | Import Chunk | resolveModule |
| 72 | H | Hint Chunk | resolveHint |
| 69 | E | Error Chunk | resolveError |
| 84 | T | Text Chunk | resolveText |
| 68 | D | Debug Chunk | resolveDebugInfo |
| 87 | W | Console Chunk | resolveConsoleEntry |
| 0 | 无 | Model Chunk | resolveModel |
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;
}引用格式说明
$@id:引用其他Chunk- 示例:
"$@5"引用ID为5的Chunk - 客户端会查找并返回对应的Chunk
- 示例:
$Ssymbol:Symbol引用- 示例:
"$Sreact.element"引用react.element Symbol - 使用Symbol.for创建
- 示例:
$Fid:Server Action引用- 示例:
"$F3"引用ID为3的Server Action - 创建可调用的函数引用
- 示例:
$$:转义的$- 示例:
"$$price"表示字符串"$price"
- 示例:
特殊值:
"$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"客户端加载过程
- 解析Chunk 1,创建AddToCart的模块引用
- 预加载
client-addtocart.js(异步) - 解析Chunk 2,创建React Element树
- 渲染时,遇到AddToCart引用
- 等待模块加载完成(如果还未加载)
- 使用加载的模块渲染AddToCart组件
代码分割的优势
- 按需加载:只有用到的Client Component才会加载
- 并行加载:多个Client Component可以并行加载
- 缓存友好:每个组件独立打包,更新时只需重新加载变更的组件
- 更小的初始包:首屏不需要的组件不会包含在初始包中
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>用户体验对比
| 方案 | 首屏时间 | 完整内容时间 | 用户体验 |
|---|---|---|---|
| 传统SSR | 2000ms | 2000ms | 长时间白屏 |
| 流式SSR | 0ms | 2000ms | 立即显示骨架,渐进加载 |
| CSR | 0ms | 2000ms + 网络延迟 | 需要额外的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);
}缓存策略
基于URL的缓存
- 相同URL的请求复用缓存
- 适用于静态内容
基于时间的失效
- 设置缓存过期时间
- 过期后重新请求
手动失效
- 提供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,
);
}
}复用条件
- type相同:组件类型没有变化
- key相同:React key没有变化
- 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=210.5.3 导航时的优化
在客户端导航时,React会智能地复用和更新组件。
javascript
// Next.js App Router示例
// 从 /products 导航到 /products/123
// 布局组件会被复用
<Layout>
{/* 只有这部分会重新获取和渲染 */}
<ProductDetail id={123} />
</Layout>导航优化策略
部分预取
- 预取可见链接的Flight数据
- 鼠标悬停时预取
乐观更新
- 立即更新UI
- 后台获取实际数据
- 数据到达后替换
共享布局
- 复用不变的布局组件
- 只更新变化的部分
预取示例
jsx
// Link组件会自动预取
<Link href="/products/123" prefetch={true}>
View Product
</Link>
// 当用户悬停在链接上时:
// 1. 发起Flight请求:GET /rsc/products/123
// 2. 解析并缓存响应
// 3. 用户点击时立即显示(从缓存)缓存层次图
10.6 综合案例:实现一个简易Flight客户端
让我们通过实现一个简化版的Flight客户端,深入理解客户端解析的完整流程。
目标
实现一个简单的Flight客户端,支持:
- 解析Flight行格式数据
- 处理Model Chunk和Import Chunk
- 重建React Element树
- 支持引用解析
实现代码
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();扩展练习
添加Error处理
- 实现完整的Error Chunk处理
- 支持错误边界
添加Symbol支持
- 处理React内置Symbol
- 支持自定义Symbol
优化性能
- 使用WeakMap缓存对象
- 实现Chunk优先级
添加调试功能
- 记录解析日志
- 可视化Chunk依赖关系
本章小结
本章深入讲解了React客户端如何解析Flight协议数据:
ReactFlightClient源码分析
- createFromReadableStream:客户端入口函数
- Response对象:核心状态容器,管理所有Chunk
- 流式解析机制:边接收边解析,无需等待完整数据
Chunk解析流程
- 行解析器:将字节流分割成行,使用状态机解析
- 类型分发:根据类型标签分发到不同处理函数
- 引用解析:处理
$@id、$S、$F等特殊标记
组件树重建
- React Element重建:从Flight数组格式创建React Element
- Client Component加载:动态import模块
- 懒加载与代码分割:每个Client Component自动成为分割点
Suspense与流式更新
- 等待服务端数据:pending Chunk抛出Promise触发Suspense
- 增量更新UI:新Chunk到达时触发重新渲染
- 渐进式加载:快速组件先显示,慢速组件后显示
缓存与复用
- Flight响应缓存:避免重复请求
- 组件实例复用:React Fiber reconciliation
- 导航优化:预取、乐观更新、共享布局
综合案例
- 手写Flight客户端
- 理解解析流程
- 掌握Flight协议细节
思考题
- 为什么Flight客户端使用流式解析而不是等待完整数据?
- 当访问一个pending Chunk时,为什么要抛出Promise?
- Client Component的代码分割是如何实现的?
- 如何优化多次导航到同一页面的性能?
- 如果服务端发送了错误的Chunk ID引用,客户端会如何处理?
下一章预告
第11章将讲解Server Actions与Hydration,包括Server Actions的实现原理、表单处理、Hydration水合机制、Selective Hydration等内容,完整呈现React全栈应用的数据流。