跳到主要内容

React fizz config DOM

一、作用

二、导出类型

1. 头信息描述符

// We make every property of the descriptor optional because it is not a contract that
// the headers provided by onHeaders has any particular header types.
// 我们将描述符的每个属性都设为可选,因为 onHeaders 提供的头部并不保证具有任何特定的头部类型。
export type HeadersDescriptor = {
Link?: string;
};

2. 流媒体格式

export type StreamingFormat = 0 | 1;

3. 指令状态

export type InstructionState = number;

4. 渲染状态

// Per request, global state that is not contextual to the rendering subtree.
// This cannot be resumed and therefore should only contain things that are
// temporary working state or are never used in the prerender pass.
// 根据请求,全局状态与渲染子树无关。
// 这无法恢复,因此应仅包含临时工作状态或在预渲染阶段从未使用的内容。
export type RenderState = {
// These can be recreated from resumable state.
// 这些可以从可恢复状态重新创建。
placeholderPrefix: PrecomputedChunk;
segmentPrefix: PrecomputedChunk;
boundaryPrefix: PrecomputedChunk;

// inline script streaming format, unused if using external runtime / data
// 内联脚本流式格式,如果使用外部运行时/数据则未使用
startInlineScript: PrecomputedChunk;

startInlineStyle: PrecomputedChunk;

// the preamble must always flush before resuming, so all these chunks must
// be null or empty when resuming.
// 在恢复之前,前导部分必须始终刷新,因此在恢复时,所有这些块必须是空的或为 null。

// preamble chunks
// 前导块
preamble: PreambleState;

// external runtime script chunks
// 外部运行时脚本块
externalRuntimeScript: null | ExternalRuntimeScript;
bootstrapChunks: Array<Chunk | PrecomputedChunk>;
importMapChunks: Array<Chunk | PrecomputedChunk>;

// Hoistable chunks
// 可提升的块
charsetChunks: Array<Chunk | PrecomputedChunk>;
viewportChunks: Array<Chunk | PrecomputedChunk>;
hoistableChunks: Array<Chunk | PrecomputedChunk>;

// Headers queues for Resources that can flush early
// 可提前刷新资源的头队列
onHeaders: void | ((headers: HeadersDescriptor) => void);
headers: null | {
preconnects: string;
fontPreloads: string;
highImagePreloads: string;
remainingCapacity: number;
};
resets: {
// corresponds to ResumableState.unknownResources["font"]
// 对应于 ResumableState.unknownResources["font"]
font: {
[href: string]: Preloaded;
};
// the rest correspond to ResumableState[<...>Resources]
// 其余部分对应 ResumableState[<...>Resources]
dns: { [key: string]: Exists };
connect: {
default: { [key: string]: Exists };
anonymous: { [key: string]: Exists };
credentials: { [key: string]: Exists };
};
image: {
[key: string]: Preloaded;
};
style: {
[key: string]: Exists | Preloaded | PreloadedWithCredentials;
};
};

// Flushing queues for Resource dependencies
// 刷新资源依赖队列
preconnects: Set<Resource>;
fontPreloads: Set<Resource>;
highImagePreloads: Set<Resource>;
// usedImagePreloads: Set<PreloadResource>,
// 使用的图像预加载: Set<PreloadResource>,
styles: Map<string, StyleQueue>;
bootstrapScripts: Set<Resource>;
scripts: Set<Resource>;
bulkPreloads: Set<Resource>;

// Temporarily keeps track of key to preload resources before shell flushes.
// 临时跟踪键以在 shell 清空之前预加载资源
preloads: {
images: Map<string, Resource>;
stylesheets: Map<string, Resource>;
scripts: Map<string, Resource>;
moduleScripts: Map<string, Resource>;
};

nonce: {
script: string | void;
style: string | void;
};

// Module-global-like reference for flushing/hoisting state of style resources
// We need to track whether the current request has flushed any style resources
// without sending an instruction to hoist them. we do that here
// 模块全局般的引用,用于刷新/提升样式资源的状态。我们需要跟踪当前请求是否已经刷新了任何样式资
// 源,而没有发送提升它们的指令。我们在这里进行此操作
stylesToHoist: boolean;

// We allow the legacy renderer to extend this object.
// 我们允许旧版渲染器扩展此对象。

// 下面注释的是源码中的类型(该类型是由 `flow-typed` 支持,但是非 TS 标准,故注之)
// ...
};

5. 可恢复状态

// Per response, global state that is not contextual to the rendering subtree.
// This is resumable and therefore should be serializable.
// 根据响应,全球状态不依赖于渲染子树的上下文。这是可恢复的,因此应当是可序列化的。
export type ResumableState = {
idPrefix: string;
nextFormID: number;
streamingFormat: StreamingFormat;

// We carry the bootstrap intializers in resumable state in case we postpone in the shell
// of a prerender. On resume we will reinitialize the bootstrap scripts if necessary.
// If we end up flushing the bootstrap scripts we void these on the resumable state
// 我们将引导程序初始化器保存在可恢复状态中,以防我们在预渲染的外壳中推迟执行。在恢复时,如果有必要,我们将
// 重新初始化引导脚本。如果最终我们刷新了引导脚本,我们将使这些可恢复状态失效。
bootstrapScriptContent?: string | void;
bootstrapScripts?: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void;
bootstrapModules?: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void;

// state for script streaming format, unused if using external runtime / data
// 脚本流式格式的状态,如果使用外部运行时/数据则不使用
instructions: InstructionState;

// postamble state
// 后记状态
hasBody: boolean;
hasHtml: boolean;

// Resources - Request local cache
// 资源 - 请求本地缓存
unknownResources: {
[asType: string]: {
[href: string]: Preloaded;
};
};
dnsResources: { [key: string]: Exists };
connectResources: {
default: { [key: string]: Exists };
anonymous: { [key: string]: Exists };
credentials: { [key: string]: Exists };
};
imageResources: {
[key: string]: Preloaded;
};
styleResources: {
[key: string]: Exists | Preloaded | PreloadedWithCredentials;
};
scriptResources: {
[key: string]: Exists | Preloaded | PreloadedWithCredentials;
};
moduleUnknownResources: {
[asType: string]: {
[href: string]: Preloaded;
};
};
moduleScriptResources: {
[key: string]: Exists | Preloaded | PreloadedWithCredentials;
};
};

6. 引导脚本描述符

export type BootstrapScriptDescriptor = {
src: string;
integrity?: string;
crossOrigin?: string;
};

7. 外部运行时脚本

export type ExternalRuntimeScript = {
src: string;
chunks: Array<Chunk | PrecomputedChunk>;
};

8. 前言状态

export type PreambleState = {
htmlChunks: null | Array<Chunk | PrecomputedChunk>;
headChunks: null | Array<Chunk | PrecomputedChunk>;
bodyChunks: null | Array<Chunk | PrecomputedChunk>;
};

9. 格式上下文

// Lets us keep track of contextual state and pick it back up after suspending.
// 让我们跟踪上下文状态,并在挂起后恢复它。
export type FormatContext = {
insertionMode: InsertionMode; // root/svg/html/mathml/table
// <select> 内选中的值,或在 <select> 外为 null
selectedValue: null | string | Array<string>; // the selected value(s) inside a <select>, or null outside <select>
tagScope: number;
// 跟踪我们是否在第一个 DOM 节点之外的 ViewTransition 中
viewTransition: null | ViewTransitionContext; // tracks if we're inside a ViewTransition outside the first DOM node
};

10. 资源

export type Resource = Array<Chunk | PrecomputedChunk>;

11. 可提升状态

export type HoistableState = {
styles: Set<StyleQueue>;
stylesheets: Set<StylesheetResource>;
suspenseyImages: boolean;
};

12. 样式队列

export type StyleQueue = {
precedence: Chunk | PrecomputedChunk;
rules: Array<Chunk | PrecomputedChunk>;
hrefs: Array<Chunk | PrecomputedChunk>;
sheets: Map<string, StylesheetResource>;
};

13. 过渡状态

export type TransitionStatus = FormStatus;

三、导出常量

1. 是主渲染器

备注

源码中 116 - 118 行

// Used to distinguish these contexts from ones used in other renderers.
// 用于将这些上下文与其他渲染器中使用的上下文区分开。
// E.g. this can be used to distinguish legacy renderers from this modern one.
// 例如,这可用于将传统渲染器与这个现代渲染器区分开。
export const isPrimaryRenderer = true;

2. 支持客户端 API

备注

源码中 120 行

export const supportsClientAPIs = true;

3. 根 HTML 模式

备注

源码中 765 - 768 行

// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
// modes. We only include the variants as they matter for the sake of our purposes.
// We don't actually provide the namespace therefore we use constants instead of the string.
// 当前写入的插入模式的常量。我们并没有编码所有的 HTML5 插入模式。我们只包含对我们的目的而言重要的变体。
// 我们实际上并没有提供命名空间,因此我们使用常量而不是字符串。用于根元素标签。

// 用于根元素标签。
export const ROOT_HTML_MODE = 0; // Used for the root most element tag.

4. 文档类型块

备注

源码中 4148 - 4149 行

export const doctypeChunk: PrecomputedChunk =
stringToPrecomputedChunk('<!DOCTYPE html>');

5. 非未完成过渡

备注

源码中 7159 行

export const NotPendingTransition: TransitionStatus = NotPending;

四、创建渲染状态

备注
// Allows us to keep track of what we've already written so we can refer back to it.
// if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag
// is set, the server will send instructions via data attributes (instead of inline scripts)
// 允许我们跟踪已经写入的内容,以便我们可以回溯参考。
// 如果传入了 externalRuntimeConfig 并且 enableFizzExternalRuntime 功能标志已设置,服务器将通过数据
// 属性(而不是内联脚本)发送指令
export function createRenderState(
resumableState: ResumableState,
nonce:
| string
| {
script?: string;
style?: string;
}
| void,
externalRuntimeConfig: string | BootstrapScriptDescriptor | void,
importMap: ImportMap | void,
onHeaders: void | ((headers: HeadersDescriptor) => void),
maxHeadersLength: void | number,
): RenderState {
const nonceScript = typeof nonce === 'string' ? nonce : nonce && nonce.script;
const inlineScriptWithNonce =
nonceScript === undefined
? startInlineScript
: stringToPrecomputedChunk(
'<script nonce="' + escapeTextForBrowser(nonceScript) + '"',
);
const nonceStyle =
typeof nonce === 'string' ? undefined : nonce && nonce.style;
const inlineStyleWithNonce =
nonceStyle === undefined
? startInlineStyle
: stringToPrecomputedChunk(
'<style nonce="' + escapeTextForBrowser(nonceStyle) + '"',
);
const idPrefix = resumableState.idPrefix;

const bootstrapChunks: Array<Chunk | PrecomputedChunk> = [];
let externalRuntimeScript: null | ExternalRuntimeScript = null;
const { bootstrapScriptContent, bootstrapScripts, bootstrapModules } =
resumableState;
if (bootstrapScriptContent !== undefined) {
bootstrapChunks.push(inlineScriptWithNonce);
pushCompletedShellIdAttribute(bootstrapChunks, resumableState);
bootstrapChunks.push(
endOfStartTag,
stringToChunk(escapeEntireInlineScriptContent(bootstrapScriptContent)),
endInlineScript,
);
}
if (enableFizzExternalRuntime) {
if (externalRuntimeConfig !== undefined) {
if (typeof externalRuntimeConfig === 'string') {
externalRuntimeScript = {
src: externalRuntimeConfig,
chunks: [],
};
pushScriptImpl(externalRuntimeScript.chunks, {
src: externalRuntimeConfig,
async: true,
integrity: undefined,
nonce: nonceScript,
});
} else {
externalRuntimeScript = {
src: externalRuntimeConfig.src,
chunks: [],
};
pushScriptImpl(externalRuntimeScript.chunks, {
src: externalRuntimeConfig.src,
async: true,
integrity: externalRuntimeConfig.integrity,
nonce: nonceScript,
});
}
}
}

const importMapChunks: Array<Chunk | PrecomputedChunk> = [];
if (importMap !== undefined) {
const map = importMap;
importMapChunks.push(importMapScriptStart);
importMapChunks.push(
stringToChunk(escapeEntireInlineScriptContent(JSON.stringify(map))),
);
importMapChunks.push(importMapScriptEnd);
}
if (__DEV__) {
if (onHeaders && typeof maxHeadersLength === 'number') {
if (maxHeadersLength <= 0) {
console.error(
'React expected a positive non-zero `maxHeadersLength` option but found %s instead. When using the `onHeaders` option you may supply an optional `maxHeadersLength` option as well however, when setting this value to zero or less no headers will be captured.',
maxHeadersLength === 0 ? 'zero' : maxHeadersLength,
);
}
}
}
const headers = onHeaders
? {
preconnects: '',
fontPreloads: '',
highImagePreloads: '',
remainingCapacity:
// We seed the remainingCapacity with 2 extra bytes because when we decrement the capacity
// we always assume we are inserting an interstitial ", " however the first header does not actually
// consume these two extra bytes.
// 我们用额外的 2 个字节初始化 remainingCapacity,因为当我们减少容量时
// 我们总是假设正在插入一个中间部分,但第一个头实际上并不消耗这两个额外字节。
2 +
(typeof maxHeadersLength === 'number'
? maxHeadersLength
: DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS),
}
: null;
const renderState: RenderState = {
placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
boundaryPrefix: stringToPrecomputedChunk(idPrefix + 'B:'),
startInlineScript: inlineScriptWithNonce,
startInlineStyle: inlineStyleWithNonce,
preamble: createPreambleState(),

externalRuntimeScript: externalRuntimeScript,
bootstrapChunks: bootstrapChunks,
importMapChunks,

onHeaders,
headers,
resets: {
font: {},
dns: {},
connect: {
default: {},
anonymous: {},
credentials: {},
},
image: {},
style: {},
},

charsetChunks: [],
viewportChunks: [],
hoistableChunks: [],

// cleared on flush
// 在刷新时清除
preconnects: new Set(),
fontPreloads: new Set(),
highImagePreloads: new Set(),
// usedImagePreloads: new Set(),
styles: new Map(),
bootstrapScripts: new Set(),
scripts: new Set(),
bulkPreloads: new Set(),

preloads: {
images: new Map(),
stylesheets: new Map(),
scripts: new Map(),
moduleScripts: new Map(),
},

nonce: {
script: nonceScript,
style: nonceStyle,
},
// like a module global for currently rendering boundary
// 类似于当前渲染边界的模块全局变量
hoistableState: null,
stylesToHoist: false,
};

if (bootstrapScripts !== undefined) {
for (let i = 0; i < bootstrapScripts.length; i++) {
const scriptConfig = bootstrapScripts[i];
let src, crossOrigin, integrity;
const props: PreloadAsProps = {
rel: 'preload',
as: 'script',
fetchPriority: 'low',
nonce,
} as any;
if (typeof scriptConfig === 'string') {
props.href = src = scriptConfig;
} else {
props.href = src = scriptConfig.src;
props.integrity = integrity =
typeof scriptConfig.integrity === 'string'
? scriptConfig.integrity
: undefined;
props.crossOrigin = crossOrigin =
typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null
? undefined
: scriptConfig.crossOrigin === 'use-credentials'
? 'use-credentials'
: '';
}

preloadBootstrapScriptOrModule(resumableState, renderState, src, props);

bootstrapChunks.push(
startScriptSrc,
stringToChunk(escapeTextForBrowser(src)),
attributeEnd,
);
if (nonceScript) {
bootstrapChunks.push(
scriptNonce,
stringToChunk(escapeTextForBrowser(nonceScript)),
attributeEnd,
);
}
if (typeof integrity === 'string') {
bootstrapChunks.push(
scriptIntegirty,
stringToChunk(escapeTextForBrowser(integrity)),
attributeEnd,
);
}
if (typeof crossOrigin === 'string') {
bootstrapChunks.push(
scriptCrossOrigin,
stringToChunk(escapeTextForBrowser(crossOrigin)),
attributeEnd,
);
}
pushCompletedShellIdAttribute(bootstrapChunks, resumableState);
bootstrapChunks.push(endAsyncScript);
}
}
if (bootstrapModules !== undefined) {
for (let i = 0; i < bootstrapModules.length; i++) {
const scriptConfig = bootstrapModules[i];
let src, crossOrigin, integrity;
const props: PreloadModuleProps = {
rel: 'modulepreload',
fetchPriority: 'low',
nonce: nonceScript,
} as any;
if (typeof scriptConfig === 'string') {
props.href = src = scriptConfig;
} else {
props.href = src = scriptConfig.src;
props.integrity = integrity =
typeof scriptConfig.integrity === 'string'
? scriptConfig.integrity
: undefined;
props.crossOrigin = crossOrigin =
typeof scriptConfig === 'string' || scriptConfig.crossOrigin == null
? undefined
: scriptConfig.crossOrigin === 'use-credentials'
? 'use-credentials'
: '';
}

preloadBootstrapScriptOrModule(resumableState, renderState, src, props);

bootstrapChunks.push(
startModuleSrc,
stringToChunk(escapeTextForBrowser(src)),
attributeEnd,
);
if (nonceScript) {
bootstrapChunks.push(
scriptNonce,
stringToChunk(escapeTextForBrowser(nonceScript)),
attributeEnd,
);
}
if (typeof integrity === 'string') {
bootstrapChunks.push(
scriptIntegirty,
stringToChunk(escapeTextForBrowser(integrity)),
attributeEnd,
);
}
if (typeof crossOrigin === 'string') {
bootstrapChunks.push(
scriptCrossOrigin,
stringToChunk(escapeTextForBrowser(crossOrigin)),
attributeEnd,
);
}
pushCompletedShellIdAttribute(bootstrapChunks, resumableState);
bootstrapChunks.push(endAsyncScript);
}
}

return renderState;
}

五、恢复渲染状态

export function resumeRenderState(
resumableState: ResumableState,
nonce: NonceOption | void,
): RenderState {
return createRenderState(
resumableState,
nonce,
undefined,
undefined,
undefined,
undefined,
);
}

六、创建可恢复状态

备注
export function createResumableState(
identifierPrefix: string | void,
externalRuntimeConfig: string | BootstrapScriptDescriptor | void,
bootstrapScriptContent: string | void,
bootstrapScripts: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
bootstrapModules: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
): ResumableState {
const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;

let streamingFormat = ScriptStreamingFormat;
if (enableFizzExternalRuntime) {
if (externalRuntimeConfig !== undefined) {
streamingFormat = DataStreamingFormat;
}
}
return {
idPrefix: idPrefix,
nextFormID: 0,
streamingFormat,
bootstrapScriptContent,
bootstrapScripts,
bootstrapModules,
instructions: NothingSent,
hasBody: false,
hasHtml: false,

// @TODO add bootstrap script to implicit preloads
// @TODO 添加引导脚本到隐式预加载

// persistent
// 持久
unknownResources: {},
dnsResources: {},
connectResources: {
default: {},
anonymous: {},
credentials: {},
},
imageResources: {},
styleResources: {},
scriptResources: {},
moduleUnknownResources: {},
moduleScriptResources: {},
};
}

七、重置可恢复状态

export function resetResumableState(
resumableState: ResumableState,
renderState: RenderState,
): void {
// Resets the resumable state based on what didn't manage to fully flush in the render state.
// This currently assumes nothing was flushed.
// 根据在渲染状态中未能完全刷新内容的情况重置可恢复状态。目前假设没有任何内容被刷新。
resumableState.nextFormID = 0;
resumableState.hasBody = false;
resumableState.hasHtml = false;
resumableState.unknownResources = {
font: renderState.resets.font,
};
resumableState.dnsResources = renderState.resets.dns;
resumableState.connectResources = renderState.resets.connect;
resumableState.imageResources = renderState.resets.image;
resumableState.styleResources = renderState.resets.style;
resumableState.scriptResources = {};
resumableState.moduleUnknownResources = {};
resumableState.moduleScriptResources = {};
// Nothing was flushed so no instructions could've flushed.
// 没有任何内容被刷新,所以没有指令可能被刷新。
resumableState.instructions = NothingSent;
}

八、完成可恢复状态

export function completeResumableState(resumableState: ResumableState): void {
// This function is called when we have completed a prerender and there is a shell.
// 当我们完成预渲染并且有一个壳时,会调用此函数。
resumableState.bootstrapScriptContent = undefined;
resumableState.bootstrapScripts = undefined;
resumableState.bootstrapModules = undefined;
}

九、创建前言状态

export function createPreambleState(): PreambleState {
return {
htmlChunks: null,
headChunks: null,
bodyChunks: null,
};
}

十、可以有前言

export function canHavePreamble(formatContext: FormatContext): boolean {
return formatContext.insertionMode < HTML_MODE;
}

十一、创建根格式上下文

export function createRootFormatContext(namespaceURI?: string): FormatContext {
const insertionMode =
namespaceURI === 'http://www.w3.org/2000/svg'
? SVG_MODE
: namespaceURI === 'http://www.w3.org/1998/Math/MathML'
? MATHML_MODE
: ROOT_HTML_MODE;
return createFormatContext(insertionMode, null, NO_SCOPE, null);
}

十二、获取子格式上下文

备注
export function getChildFormatContext(
parentContext: FormatContext,
type: string,
props: Object,
): FormatContext {
const subtreeScope = parentContext.tagScope & SUBTREE_SCOPE;
switch (type) {
case 'noscript':
return createFormatContext(
HTML_MODE,
null,
subtreeScope | NOSCRIPT_SCOPE,
null,
);
case 'select':
return createFormatContext(
HTML_MODE,
props.value != null ? props.value : props.defaultValue,
subtreeScope,
null,
);
case 'svg':
return createFormatContext(SVG_MODE, null, subtreeScope, null);
case 'picture':
return createFormatContext(
HTML_MODE,
null,
subtreeScope | PICTURE_SCOPE,
null,
);
case 'math':
return createFormatContext(MATHML_MODE, null, subtreeScope, null);
case 'foreignObject':
return createFormatContext(HTML_MODE, null, subtreeScope, null);
// Table parents are special in that their children can only be created at all if they're
// wrapped in a table parent. So we need to encode that we're entering this mode.
// 表父项是特殊的,因为它们的子项只有在被包裹在表父项中时才能创建。因此我们需要编码以表示我们正在进入这种模式。
case 'table':
return createFormatContext(HTML_TABLE_MODE, null, subtreeScope, null);
case 'thead':
case 'tbody':
case 'tfoot':
return createFormatContext(
HTML_TABLE_BODY_MODE,
null,
subtreeScope,
null,
);
case 'colgroup':
return createFormatContext(HTML_COLGROUP_MODE, null, subtreeScope, null);
case 'tr':
return createFormatContext(HTML_TABLE_ROW_MODE, null, subtreeScope, null);
case 'head':
if (parentContext.insertionMode < HTML_MODE) {
// We are either at the root or inside the <html> tag and can enter
// the <head> scope
// 我们要么处于根目录,要么在 <html> 标签内,并且可以进入 <head> 范围
return createFormatContext(HTML_HEAD_MODE, null, subtreeScope, null);
}
break;
case 'html':
if (parentContext.insertionMode === ROOT_HTML_MODE) {
return createFormatContext(HTML_HTML_MODE, null, subtreeScope, null);
}
break;
}
if (parentContext.insertionMode >= HTML_TABLE_MODE) {
// Whatever tag this was, it wasn't a table parent or other special parent, so we must have
// entered plain HTML again.
// 无论这个标签是什么,它都不是表格父元素或其他特殊父元素,所以我们一定是又进入了普通 HTML。
return createFormatContext(HTML_MODE, null, subtreeScope, null);
}
if (parentContext.insertionMode < HTML_MODE) {
return createFormatContext(HTML_MODE, null, subtreeScope, null);
}
if (enableViewTransition) {
if (parentContext.viewTransition !== null) {
// If we're inside a view transition, regardless what element we were in, it consumes
// the view transition context.
// 如果我们在视图过渡中,无论我们在哪个元素中,它都会消耗视图过渡上下文。
return createFormatContext(
parentContext.insertionMode,
parentContext.selectedValue,
subtreeScope,
null,
);
}
}
if (parentContext.tagScope !== subtreeScope) {
return createFormatContext(
parentContext.insertionMode,
parentContext.selectedValue,
subtreeScope,
null,
);
}
return parentContext;
}

十三、获取挂起回退格式上下文

export function getSuspenseFallbackFormatContext(
resumableState: ResumableState,
parentContext: FormatContext,
): FormatContext {
if (parentContext.tagScope & UPDATE_SCOPE) {
// If we're rendering a Suspense in fallback mode and that is inside a ViewTransition,
// which hasn't disabled updates, then revealing it might animate the parent so we need
// the ViewTransition instructions.
// 如果我们在回退模式下渲染一个 Suspense,并且它在一个 ViewTransition 内部,该 ViewTransition 并
// 没有禁用更新,那么显示它可能会使父元素产生动画,因此我们需要 ViewTransition 的指令。
resumableState.instructions |= NeedUpgradeToViewTransitions;
}
return createFormatContext(
parentContext.insertionMode,
parentContext.selectedValue,
parentContext.tagScope | FALLBACK_SCOPE | EXIT_SCOPE,
getSuspenseViewTransition(parentContext.viewTransition),
);
}

十四、获取挂起内容格式上下文

export function getSuspenseContentFormatContext(
resumableState: ResumableState,
parentContext: FormatContext,
): FormatContext {
const viewTransition = getSuspenseViewTransition(
parentContext.viewTransition,
);
let subtreeScope = parentContext.tagScope | ENTER_SCOPE;
if (viewTransition !== null && viewTransition.share !== 'none') {
// If we have a ViewTransition wrapping Suspense then the appearing animation
// will be applied just like an "enter" below. Mark it as animating.
// 如果我们有一个 ViewTransition 包裹着 Suspense,那么出现动画将会像下面的“进入”一样被应用。将其标
// 记为正在动画中。
subtreeScope |= APPEARING_SCOPE;
}
return createFormatContext(
parentContext.insertionMode,
parentContext.selectedValue,
subtreeScope,
viewTransition,
);
}

十五、获取视图过渡格式上下文

export function getViewTransitionFormatContext(
resumableState: ResumableState,
parentContext: FormatContext,
update: ?string,
enter: ?string,
exit: ?string,
share: ?string,
name: ?string,
// 名称或自动生成的唯一名称
autoName: string, // name or an autogenerated unique name
): FormatContext {
// We're entering a <ViewTransition>. Normalize props.
// 我们正在进入一个 <ViewTransition>。规范化属性。
if (update == null) {
update = 'auto';
}
if (enter == null) {
enter = 'auto';
}
if (exit == null) {
exit = 'auto';
}
if (name == null) {
const parentViewTransition = parentContext.viewTransition;
if (parentViewTransition !== null) {
// If we have multiple nested ViewTransition and the parent has a "share"
// but the child doesn't, then the parent ViewTransition can still activate
// a share scenario so we reuse the name and share from the parent.
// 如果我们有多个嵌套的 ViewTransition,并且父元素有一个 “share”,但子元素没有,那么父元素的
// ViewTransition 仍然可以激活一个共享场景,因此我们重用父元素的名称并从父元素共享。
name = parentViewTransition.name;
share = parentViewTransition.share;
} else {
name = 'auto';
// 只有在有明确名称的情况下,分享才相关
share = 'none'; // share is only relevant if there's an explicit name
}
} else {
if (share == null) {
share = 'auto';
}
if (parentContext.tagScope & FALLBACK_SCOPE) {
// If we have an explicit name and share is not disabled, and we're inside
// a fallback, then that fallback might pair with content and so we might need
// the ViewTransition instructions to animate between them.
// 如果我们有一个显式名称并且共享未被禁用,并且我们在一个回退中,那么该回退可能会与内容配对,因此我们
// 可能需要 ViewTransition 指令在它们之间进行动画。
resumableState.instructions |= NeedUpgradeToViewTransitions;
}
}
if (!(parentContext.tagScope & EXIT_SCOPE)) {
// exit 只与回退中的第一个 ViewTransition 相关
exit = 'none'; // exit is only relevant for the first ViewTransition inside fallback
} else {
resumableState.instructions |= NeedUpgradeToViewTransitions;
}
if (!(parentContext.tagScope & ENTER_SCOPE)) {
// enter 只与 content 内的第一个 ViewTransition 相关
enter = 'none'; // enter is only relevant for the first ViewTransition inside content
} else {
resumableState.instructions |= NeedUpgradeToViewTransitions;
}
const viewTransition: ViewTransitionContext = {
update,
enter,
exit,
share,
name,
autoName,
nameIdx: 0,
};
let subtreeScope = parentContext.tagScope & SUBTREE_SCOPE;
if (update !== 'none') {
subtreeScope |= UPDATE_SCOPE;
} else {
subtreeScope &= ~UPDATE_SCOPE;
}
if (enter !== 'none') {
subtreeScope |= APPEARING_SCOPE;
}
return createFormatContext(
parentContext.insertionMode,
parentContext.selectedValue,
subtreeScope,
viewTransition,
);
}

十六、前言上下文

export function isPreambleContext(formatContext: FormatContext): boolean {
return formatContext.insertionMode === HTML_HEAD_MODE;
}

十七、标记 ID

export function makeId(
resumableState: ResumableState,
treeId: string,
localId: number,
): string {
const idPrefix = resumableState.idPrefix;

let id = '_' + idPrefix + 'R_' + treeId;

// Unless this is the first id at this level, append a number at the end
// that represents the position of this useId hook among all the useId
// hooks for this fiber.
// 除非这是此级别的第一个 id,否则在末尾附加一个数字。该数字表示此 useId hook 在此 fiber 的所有
// useId hook 中的位置。
if (localId > 0) {
id += 'H' + localId.toString(32);
}

return id + '_';
}

十八、推送文本实例

export function pushTextInstance(
target: Array<Chunk | PrecomputedChunk>,
text: string,
renderState: RenderState,
textEmbedded: boolean,
): boolean {
if (text === '') {
// Empty text doesn't have a DOM node representation and the hydration is aware of this.
// 空文本没有 DOM 节点表示,水合功能对此是有意识的。
return textEmbedded;
}
if (textEmbedded) {
target.push(textSeparator);
}
target.push(stringToChunk(encodeHTMLTextNode(text)));
return true;
}

十九、推送段落结尾

// Called when Fizz is done with a Segment. Currently the only purpose is to conditionally
// emit a text separator when we don't know for sure it is safe to omit
// 当 Fizz 完成一个片段时调用。目前唯一的目的是在我们不确定是否可以安全省略时,有条件地发出文本分隔符
export function pushSegmentFinale(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
lastPushedText: boolean,
textEmbedded: boolean,
): void {
if (lastPushedText && textEmbedded) {
target.push(textSeparator);
}
}

廿、推送表单状态标记是否匹配

export function pushFormStateMarkerIsMatching(
target: Array<Chunk | PrecomputedChunk>,
) {
target.push(formStateMarkerIsMatching);
}

廿一、推送表单状态标记不匹配

export function pushFormStateMarkerIsNotMatching(
target: Array<Chunk | PrecomputedChunk>,
) {
target.push(formStateMarkerIsNotMatching);
}

廿二、启动实例

备注
export function pushStartInstance(
target: Array<Chunk | PrecomputedChunk>,
type: string,
props: Object,
resumableState: ResumableState,
renderState: RenderState,
preambleState: null | PreambleState,
hoistableState: null | HoistableState,
formatContext: FormatContext,
textEmbedded: boolean,
): ReactNodeList {
if (__DEV__) {
validateARIAProperties(type, props);
validateInputProperties(type, props);
validateUnknownProperties(type, props, null);

if (
!props.suppressContentEditableWarning &&
props.contentEditable &&
props.children != null
) {
console.error(
'A component is `contentEditable` and contains `children` managed by ' +
'React. It is now your responsibility to guarantee that none of ' +
'those nodes are unexpectedly modified or duplicated. This is ' +
'probably not intentional.',
);
}

if (
formatContext.insertionMode !== SVG_MODE &&
formatContext.insertionMode !== MATHML_MODE
) {
if (type.indexOf('-') === -1 && type.toLowerCase() !== type) {
console.error(
'<%s /> is using incorrect casing. ' +
'Use PascalCase for React components, ' +
'or lowercase for HTML elements.',
type,
);
}
}
}

switch (type) {
case 'div':
case 'span':
case 'svg':
case 'path':
// Fast track very common tags
// 快速跟踪非常常用的标签
break;
case 'a':
return pushStartAnchor(target, props, formatContext);
case 'g':
case 'p':
case 'li':
// Fast track very common tags
// 快速跟踪非常常用的标签
break;
// Special tags
// 特殊标签
case 'select':
return pushStartSelect(target, props, formatContext);
case 'option':
return pushStartOption(target, props, formatContext);
case 'textarea':
return pushStartTextArea(target, props, formatContext);
case 'input':
return pushInput(
target,
props,
resumableState,
renderState,
formatContext,
);
case 'button':
return pushStartButton(
target,
props,
resumableState,
renderState,
formatContext,
);
case 'form':
return pushStartForm(
target,
props,
resumableState,
renderState,
formatContext,
);
case 'menuitem':
return pushStartMenuItem(target, props, formatContext);
case 'object':
return pushStartObject(target, props, formatContext);
case 'title':
return pushTitle(target, props, renderState, formatContext);
case 'link':
return pushLink(
target,
props,
resumableState,
renderState,
hoistableState,
textEmbedded,
formatContext,
);
case 'script':
return pushScript(
target,
props,
resumableState,
renderState,
textEmbedded,
formatContext,
);
case 'style':
return pushStyle(
target,
props,
resumableState,
renderState,
hoistableState,
textEmbedded,
formatContext,
);
case 'meta':
return pushMeta(target, props, renderState, textEmbedded, formatContext);
// Newline eating tags
// 换行吞噬标签
case 'listing':
case 'pre': {
return pushStartPreformattedElement(target, props, type, formatContext);
}
case 'img': {
return pushImg(
target,
props,
resumableState,
renderState,
hoistableState,
formatContext,
);
}
// Omitted close tags
// 省略关闭标签
case 'base':
case 'area':
case 'br':
case 'col':
case 'embed':
case 'hr':
case 'keygen':
case 'param':
case 'source':
case 'track':
case 'wbr': {
return pushSelfClosing(target, props, type, formatContext);
}
// These are reserved SVG and MathML elements, that are never custom elements.
// 这些是保留的 SVG 和 MathML 元素,永远不会是自定义元素。
// https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-core-concepts
case 'annotation-xml':
case 'color-profile':
case 'font-face':
case 'font-face-src':
case 'font-face-uri':
case 'font-face-format':
case 'font-face-name':
case 'missing-glyph': {
break;
}
// Preamble start tags
// 序言开始标签
case 'head':
return pushStartHead(
target,
props,
renderState,
preambleState,
formatContext,
);
case 'body':
return pushStartBody(
target,
props,
renderState,
preambleState,
formatContext,
);
case 'html': {
return pushStartHtml(
target,
props,
renderState,
preambleState,
formatContext,
);
}
default: {
if (type.indexOf('-') !== -1) {
// Custom element
// 自定义元素
return pushStartCustomElement(target, props, type, formatContext);
}
}
}
// Generic element
// 通用元素
return pushStartGenericElement(target, props, type, formatContext);
}

廿三、推送结束实例

export function pushEndInstance(
target: Array<Chunk | PrecomputedChunk>,
type: string,
props: Object,
resumableState: ResumableState,
formatContext: FormatContext,
): void {
switch (type) {
// We expect title and script tags to always be pushed in a unit and never
// return children. when we end up pushing the end tag we want to ensure
// there is no extra closing tag pushed
// 我们期望 title 和 script 标签总是作为一个单元推送,且永不返回子元素。当我们最终推送结束标签时,我们
// 希望确保没有额外的闭合标签被推送
case 'title':
case 'style':
case 'script':
// Omitted close tags
// TODO: Instead of repeating this switch we could try to pass a flag from above.
// That would require returning a tuple. Which might be ok if it gets inlined.
// fallthrough
// 省略了闭合标签
// TODO:与其重复这个 switch,我们可以尝试从上面传递一个标志。那将需要返回一个元组。如果它被内联,可能
// 没问题。继续执行
case 'area':
case 'base':
case 'br':
case 'col':
case 'embed':
case 'hr':
case 'img':
case 'input':
case 'keygen':
case 'link':
case 'meta':
case 'param':
case 'source':
case 'track':
case 'wbr': {
// No close tag needed.
// 不需要结束标签。
return;
}
// Postamble end tags
// When float is enabled we omit the end tags for body and html when
// they represent the Document.body and Document.documentElement Nodes.
// This is so we can withhold them until the postamble when we know
// we won't emit any more tags
// 后记结束标签
// 当启用浮动时,如果 body 和 html 表示 Document.body 和 Document.documentElement 节点,我们会
// 省略它们的结束标签。这是为了在后记阶段之前将它们保留,因为此时我们确定不会再输出任何标签
case 'body': {
if (formatContext.insertionMode <= HTML_HTML_MODE) {
resumableState.hasBody = true;
return;
}
break;
}
case 'html':
if (formatContext.insertionMode === ROOT_HTML_MODE) {
resumableState.hasHtml = true;
return;
}
break;
case 'head':
if (formatContext.insertionMode <= HTML_HTML_MODE) {
return;
}
break;
}
target.push(endChunkForTag(type));
}

廿四、提升前言状态

export function hoistPreambleState(
renderState: RenderState,
preambleState: PreambleState,
) {
const rootPreamble = renderState.preamble;
if (rootPreamble.htmlChunks === null && preambleState.htmlChunks) {
rootPreamble.htmlChunks = preambleState.htmlChunks;
}
if (rootPreamble.headChunks === null && preambleState.headChunks) {
rootPreamble.headChunks = preambleState.headChunks;
}
if (rootPreamble.bodyChunks === null && preambleState.bodyChunks) {
rootPreamble.bodyChunks = preambleState.bodyChunks;
}
}

廿五、前导已准备好

export function isPreambleReady(
renderState: RenderState,
// This means there are unfinished Suspense boundaries which could contain
// a preamble. In the case of DOM we constrain valid programs to only having
// one instance of each singleton so we can determine the preamble is ready
// as long as we have chunks for each of these tags.
// 这意味着存在未完成的 Suspense 边界,这些边界可能包含前言。在 DOM 的情况下,我们将有效程序限制为每个单
// 例只有一个实例,因此只要我们拥有每个这些标记的块,就可以确定前言已准备好。
hasPendingPreambles: boolean,
): boolean {
const preamble = renderState.preamble;
return (
// There are no remaining boundaries which might contain a preamble so
// the preamble is as complete as it is going to get
// 没有剩余的边界可能包含前言,因此前言已经尽可能完整
hasPendingPreambles === false ||
// we have a head and body tag. we don't need to wait for any more
// because it would be invalid to render additional copies of these tags
// 我们有 head 和 body 标签。我们不需要再等待其他的,因为渲染这些标签的额外副本将是无效的
!!(preamble.headChunks && preamble.bodyChunks)
);
}

廿六、写入完成根

备注
export function writeCompletedRoot(
destination: Destination,
resumableState: ResumableState,
renderState: RenderState,
isComplete: boolean,
): boolean {
if (!isComplete) {
// If we're not already fully complete, we might complete another boundary. If so,
// we need to track the paint time of the shell so we know how much to throttle the reveal.
// 如果我们还没有完全完成,我们可能会完成另一个边界。如果是这样,我们需要跟踪外壳的绘制时间,以便我们知道
// 需要多少节流来显示。
writeShellTimeInstruction(destination, resumableState, renderState);
}
if (enableFizzBlockingRender) {
const preamble = renderState.preamble;
if (preamble.htmlChunks || preamble.headChunks) {
// If we rendered the whole document, then we emitted a rel="expect" that needs a
// matching target. Normally we use one of the bootstrap scripts for this but if
// there are none, then we need to emit a tag to complete the shell.
// 如果我们渲染整个文档,那么我们会发出一个需要匹配目标的 rel="expect"。
// 通常我们会使用其中一个引导脚本来完成这个操作,但如果没有,则我们需要发出一个标签来完成外壳。
if (
(resumableState.instructions & SentCompletedShellId) ===
NothingSent
) {
writeChunk(destination, startChunkForTag('template'));
writeCompletedShellIdAttribute(destination, resumableState);
writeChunk(destination, endOfStartTag);
writeChunk(destination, endChunkForTag('template'));
}
}
}
return writeBootstrap(destination, renderState);
}

廿七、写占位符

备注
export function writePlaceholder(
destination: Destination,
renderState: RenderState,
id: number,
): boolean {
writeChunk(destination, placeholder1);
writeChunk(destination, renderState.placeholderPrefix);
const formattedID = stringToChunk(id.toString(16));
writeChunk(destination, formattedID);
return writeChunkAndReturn(destination, placeholder2);
}

廿八、启动活动边界

export function pushStartActivityBoundary(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
): void {
target.push(startActivityBoundary);
}

廿九、推送结束活动边界

export function pushEndActivityBoundary(
target: Array<Chunk | PrecomputedChunk>,
renderState: RenderState,
): void {
target.push(endActivityBoundary);
}

卅、写开始完成挂起边界

备注
export function writeStartCompletedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
): boolean {
return writeChunkAndReturn(destination, startCompletedSuspenseBoundary);
}

卅一、写入开始待处理悬挂起边界

备注
export function writeStartPendingSuspenseBoundary(
destination: Destination,
renderState: RenderState,
id: number,
): boolean {
writeChunk(destination, startPendingSuspenseBoundary1);

if (id === null) {
throw new Error(
'An ID must have been assigned before we can complete the boundary.',
);
}

writeChunk(destination, renderState.boundaryPrefix);
writeChunk(destination, stringToChunk(id.toString(16)));
return writeChunkAndReturn(destination, startPendingSuspenseBoundary2);
}

卅二、写开始客户端渲染的挂起边界

备注
export function writeStartClientRenderedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
errorDigest: ?string,
errorMessage: ?string,
errorStack: ?string,
errorComponentStack: ?string,
): boolean {
let result;
result = writeChunkAndReturn(
destination,
startClientRenderedSuspenseBoundary,
);
writeChunk(destination, clientRenderedSuspenseBoundaryError1);
if (errorDigest) {
writeChunk(destination, clientRenderedSuspenseBoundaryError1A);
writeChunk(destination, stringToChunk(escapeTextForBrowser(errorDigest)));
writeChunk(
destination,
clientRenderedSuspenseBoundaryErrorAttrInterstitial,
);
}
if (__DEV__) {
if (errorMessage) {
writeChunk(destination, clientRenderedSuspenseBoundaryError1B);
writeChunk(
destination,
stringToChunk(escapeTextForBrowser(errorMessage)),
);
writeChunk(
destination,
clientRenderedSuspenseBoundaryErrorAttrInterstitial,
);
}
if (errorStack) {
writeChunk(destination, clientRenderedSuspenseBoundaryError1C);
writeChunk(destination, stringToChunk(escapeTextForBrowser(errorStack)));
writeChunk(
destination,
clientRenderedSuspenseBoundaryErrorAttrInterstitial,
);
}
if (errorComponentStack) {
writeChunk(destination, clientRenderedSuspenseBoundaryError1D);
writeChunk(
destination,
stringToChunk(escapeTextForBrowser(errorComponentStack)),
);
writeChunk(
destination,
clientRenderedSuspenseBoundaryErrorAttrInterstitial,
);
}
}
result = writeChunkAndReturn(
destination,
clientRenderedSuspenseBoundaryError2,
);
return result;
}

卅三、写结束已完成的挂起边界

备注
export function writeEndCompletedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
): boolean {
return writeChunkAndReturn(destination, endSuspenseBoundary);
}

卅四、写结束未决挂起边界

备注
export function writeEndPendingSuspenseBoundary(
destination: Destination,
renderState: RenderState,
): boolean {
return writeChunkAndReturn(destination, endSuspenseBoundary);
}

卅五、写结束客户端渲染的挂起边界

备注
export function writeEndClientRenderedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
): boolean {
return writeChunkAndReturn(destination, endSuspenseBoundary);
}

卅六、写开始段

备注
export function writeStartSegment(
destination: Destination,
renderState: RenderState,
formatContext: FormatContext,
id: number,
): boolean {
switch (formatContext.insertionMode) {
case ROOT_HTML_MODE:
case HTML_HTML_MODE:
case HTML_HEAD_MODE:
case HTML_MODE: {
writeChunk(destination, startSegmentHTML);
writeChunk(destination, renderState.segmentPrefix);
writeChunk(destination, stringToChunk(id.toString(16)));
return writeChunkAndReturn(destination, startSegmentHTML2);
}
case SVG_MODE: {
writeChunk(destination, startSegmentSVG);
writeChunk(destination, renderState.segmentPrefix);
writeChunk(destination, stringToChunk(id.toString(16)));
return writeChunkAndReturn(destination, startSegmentSVG2);
}
case MATHML_MODE: {
writeChunk(destination, startSegmentMathML);
writeChunk(destination, renderState.segmentPrefix);
writeChunk(destination, stringToChunk(id.toString(16)));
return writeChunkAndReturn(destination, startSegmentMathML2);
}
case HTML_TABLE_MODE: {
writeChunk(destination, startSegmentTable);
writeChunk(destination, renderState.segmentPrefix);
writeChunk(destination, stringToChunk(id.toString(16)));
return writeChunkAndReturn(destination, startSegmentTable2);
}
// TODO: For the rest of these, there will be extra wrapper nodes that never
// get deleted from the document. We need to delete the table too as part
// of the injected scripts. They are invisible though so it's not too terrible
// and it's kind of an edge case to suspend in a table. Totally supported though.
// 待办事项:对于剩下的这些,会有额外的包装节点,它们永远不会从文档中删除。我们也需要删除表格,作为注入脚
// 本的一部分。不过它们是不可见的,所以问题不大,而且在表格中暂停算是一种边缘情况。不过完全支持。
case HTML_TABLE_BODY_MODE: {
writeChunk(destination, startSegmentTableBody);
writeChunk(destination, renderState.segmentPrefix);
writeChunk(destination, stringToChunk(id.toString(16)));
return writeChunkAndReturn(destination, startSegmentTableBody2);
}
case HTML_TABLE_ROW_MODE: {
writeChunk(destination, startSegmentTableRow);
writeChunk(destination, renderState.segmentPrefix);
writeChunk(destination, stringToChunk(id.toString(16)));
return writeChunkAndReturn(destination, startSegmentTableRow2);
}
case HTML_COLGROUP_MODE: {
writeChunk(destination, startSegmentColGroup);
writeChunk(destination, renderState.segmentPrefix);
writeChunk(destination, stringToChunk(id.toString(16)));
return writeChunkAndReturn(destination, startSegmentColGroup2);
}
default: {
throw new Error('Unknown insertion mode. This is a bug in React.');
}
}
}

卅七、写结束段

备注
export function writeEndSegment(
destination: Destination,
formatContext: FormatContext,
): boolean {
switch (formatContext.insertionMode) {
case ROOT_HTML_MODE:
case HTML_HTML_MODE:
case HTML_HEAD_MODE:
case HTML_MODE: {
return writeChunkAndReturn(destination, endSegmentHTML);
}
case SVG_MODE: {
return writeChunkAndReturn(destination, endSegmentSVG);
}
case MATHML_MODE: {
return writeChunkAndReturn(destination, endSegmentMathML);
}
case HTML_TABLE_MODE: {
return writeChunkAndReturn(destination, endSegmentTable);
}
case HTML_TABLE_BODY_MODE: {
return writeChunkAndReturn(destination, endSegmentTableBody);
}
case HTML_TABLE_ROW_MODE: {
return writeChunkAndReturn(destination, endSegmentTableRow);
}
case HTML_COLGROUP_MODE: {
return writeChunkAndReturn(destination, endSegmentColGroup);
}
default: {
throw new Error('Unknown insertion mode. This is a bug in React.');
}
}
}

卅八、写完成的部分指令

备注
export function writeCompletedSegmentInstruction(
destination: Destination,
resumableState: ResumableState,
renderState: RenderState,
contentSegmentID: number,
): boolean {
const scriptFormat =
!enableFizzExternalRuntime ||
resumableState.streamingFormat === ScriptStreamingFormat;
if (scriptFormat) {
writeChunk(destination, renderState.startInlineScript);
writeChunk(destination, endOfStartTag);
if (
(resumableState.instructions & SentCompleteSegmentFunction) ===
NothingSent
) {
// The first time we write this, we'll need to include the full implementation.
// 第一次写这个时,我们需要包含完整的实现。
resumableState.instructions |= SentCompleteSegmentFunction;
writeChunk(destination, completeSegmentScript1Full);
} else {
// Future calls can just reuse the same function.
// 将来调用可以重复使用同一个函数。
writeChunk(destination, completeSegmentScript1Partial);
}
} else {
writeChunk(destination, completeSegmentData1);
}

// Write function arguments, which are string literals
// 编写函数参数,它们是字符串字面量
writeChunk(destination, renderState.segmentPrefix);
const formattedID = stringToChunk(contentSegmentID.toString(16));
writeChunk(destination, formattedID);
if (scriptFormat) {
writeChunk(destination, completeSegmentScript2);
} else {
writeChunk(destination, completeSegmentData2);
}
writeChunk(destination, renderState.placeholderPrefix);
writeChunk(destination, formattedID);

if (scriptFormat) {
return writeChunkAndReturn(destination, completeSegmentScriptEnd);
} else {
return writeChunkAndReturn(destination, completeSegmentDataEnd);
}
}

卅九、写完成边界指令

备注
export function writeCompletedBoundaryInstruction(
destination: Destination,
resumableState: ResumableState,
renderState: RenderState,
id: number,
hoistableState: HoistableState,
): boolean {
const requiresStyleInsertion = renderState.stylesToHoist;
const requiresViewTransitions =
enableViewTransition &&
(resumableState.instructions & NeedUpgradeToViewTransitions) !==
NothingSent;
// If necessary stylesheets will be flushed with this instruction.
// Any style tags not yet hoisted in the Document will also be hoisted.
// We reset this state since after this instruction executes all styles
// up to this point will have been hoisted
// 如果需要,样式表将通过此指令刷新。文档中尚未提升的任何样式标签也将被提升。我们重置此状态,因为在此指令执
// 行后,到目前为止的所有样式都将已被提升
renderState.stylesToHoist = false;
const scriptFormat =
!enableFizzExternalRuntime ||
resumableState.streamingFormat === ScriptStreamingFormat;
if (scriptFormat) {
writeChunk(destination, renderState.startInlineScript);
writeChunk(destination, endOfStartTag);
if (requiresStyleInsertion) {
if (
(resumableState.instructions & SentClientRenderFunction) ===
NothingSent
) {
// The completeBoundaryWithStyles function depends on the client render function.
// `completeBoundaryWithStyles` 函数依赖于客户端渲染函数。
resumableState.instructions |= SentClientRenderFunction;
writeChunk(destination, clientRenderScriptFunctionOnly);
}
if (
(resumableState.instructions & SentCompleteBoundaryFunction) ===
NothingSent
) {
// The completeBoundaryWithStyles function depends on the complete boundary function.
// `completeBoundaryWithStyles` 功能依赖于完整边界功能。
resumableState.instructions |= SentCompleteBoundaryFunction;
writeChunk(destination, completeBoundaryScriptFunctionOnly);
}
if (
requiresViewTransitions &&
(resumableState.instructions & SentUpgradeToViewTransitions) ===
NothingSent
) {
resumableState.instructions |= SentUpgradeToViewTransitions;
writeChunk(
destination,
completeBoundaryUpgradeToViewTransitionsInstruction,
);
}
if (
(resumableState.instructions & SentStyleInsertionFunction) ===
NothingSent
) {
resumableState.instructions |= SentStyleInsertionFunction;
writeChunk(destination, completeBoundaryWithStylesScript1FullPartial);
} else {
writeChunk(destination, completeBoundaryWithStylesScript1Partial);
}
} else {
if (
(resumableState.instructions & SentCompleteBoundaryFunction) ===
NothingSent
) {
resumableState.instructions |= SentCompleteBoundaryFunction;
writeChunk(destination, completeBoundaryScriptFunctionOnly);
}
if (
requiresViewTransitions &&
(resumableState.instructions & SentUpgradeToViewTransitions) ===
NothingSent
) {
resumableState.instructions |= SentUpgradeToViewTransitions;
writeChunk(
destination,
completeBoundaryUpgradeToViewTransitionsInstruction,
);
}
writeChunk(destination, completeBoundaryScript1Partial);
}
} else {
if (requiresStyleInsertion) {
writeChunk(destination, completeBoundaryWithStylesData1);
} else {
writeChunk(destination, completeBoundaryData1);
}
}

const idChunk = stringToChunk(id.toString(16));

writeChunk(destination, renderState.boundaryPrefix);
writeChunk(destination, idChunk);

// Write function arguments, which are string and array literals
// 编写函数参数,它们是字符串和数组字面量
if (scriptFormat) {
writeChunk(destination, completeBoundaryScript2);
} else {
writeChunk(destination, completeBoundaryData2);
}
writeChunk(destination, renderState.segmentPrefix);
writeChunk(destination, idChunk);
if (requiresStyleInsertion) {
// Script and data writers must format this differently:
// 脚本和数据编写者必须以不同的方式进行格式化:
// - script writer emits an array literal, whose string elements are
// - 脚本编写者输出数组字面量,其字符串元素
// escaped for javascript e.g. ["A", "B"]
// 为 JavaScript 转义,例如 ["A", "B"]
// - data writer emits a string literal, which is escaped as html
// - 数据编写者输出字符串字面量,其内容以 HTML 转义
// e.g. [&#34;A&#34;, &#34;B&#34;]
if (scriptFormat) {
writeChunk(destination, completeBoundaryScript3a);
// hoistableState encodes an array literal
// 可提升状态编码一个数组字面量
writeStyleResourceDependenciesInJS(destination, hoistableState);
} else {
writeChunk(destination, completeBoundaryData3a);
writeStyleResourceDependenciesInAttr(destination, hoistableState);
}
} else {
if (scriptFormat) {
writeChunk(destination, completeBoundaryScript3b);
}
}
let writeMore;
if (scriptFormat) {
writeMore = writeChunkAndReturn(destination, completeBoundaryScriptEnd);
} else {
writeMore = writeChunkAndReturn(destination, completeBoundaryDataEnd);
}
return writeBootstrap(destination, renderState) && writeMore;
}

肆、编写客户端渲染边界指令

备注
export function writeClientRenderBoundaryInstruction(
destination: Destination,
resumableState: ResumableState,
renderState: RenderState,
id: number,
errorDigest: ?string,
errorMessage: ?string,
errorStack: ?string,
errorComponentStack: ?string,
): boolean {
const scriptFormat =
!enableFizzExternalRuntime ||
resumableState.streamingFormat === ScriptStreamingFormat;
if (scriptFormat) {
writeChunk(destination, renderState.startInlineScript);
writeChunk(destination, endOfStartTag);
if (
(resumableState.instructions & SentClientRenderFunction) ===
NothingSent
) {
// The first time we write this, we'll need to include the full implementation.
// 第一次写这个时,我们需要包含完整的实现。
resumableState.instructions |= SentClientRenderFunction;
writeChunk(destination, clientRenderScript1Full);
} else {
// Future calls can just reuse the same function.
// 将来调用可以重复使用同一个函数。
writeChunk(destination, clientRenderScript1Partial);
}
} else {
// <template data-rxi="" data-bid="
writeChunk(destination, clientRenderData1);
}

writeChunk(destination, renderState.boundaryPrefix);
writeChunk(destination, stringToChunk(id.toString(16)));
if (scriptFormat) {
// " needs to be inserted for scripts, since ArgInterstitual does not contain
// leading or trailing quotes
// “需要在脚本中插入,因为 ArgInterstitual 不包含前导或尾随引号
writeChunk(destination, clientRenderScript1A);
}

if (errorDigest || errorMessage || errorStack || errorComponentStack) {
if (scriptFormat) {
// ,"JSONString"
writeChunk(destination, clientRenderErrorScriptArgInterstitial);
writeChunk(
destination,
stringToChunk(escapeJSStringsForInstructionScripts(errorDigest || '')),
);
} else {
// " data-dgst="HTMLString
writeChunk(destination, clientRenderData2);
writeChunk(
destination,
stringToChunk(escapeTextForBrowser(errorDigest || '')),
);
}
}
if (errorMessage || errorStack || errorComponentStack) {
if (scriptFormat) {
// ,"JSONString"
writeChunk(destination, clientRenderErrorScriptArgInterstitial);
writeChunk(
destination,
stringToChunk(escapeJSStringsForInstructionScripts(errorMessage || '')),
);
} else {
// " data-msg="HTMLString
writeChunk(destination, clientRenderData3);
writeChunk(
destination,
stringToChunk(escapeTextForBrowser(errorMessage || '')),
);
}
}
if (errorStack || errorComponentStack) {
// ,"JSONString"
if (scriptFormat) {
writeChunk(destination, clientRenderErrorScriptArgInterstitial);
writeChunk(
destination,
stringToChunk(escapeJSStringsForInstructionScripts(errorStack || '')),
);
} else {
// " data-stck="HTMLString
writeChunk(destination, clientRenderData4);
writeChunk(
destination,
stringToChunk(escapeTextForBrowser(errorStack || '')),
);
}
}
if (errorComponentStack) {
// ,"JSONString"
if (scriptFormat) {
writeChunk(destination, clientRenderErrorScriptArgInterstitial);
writeChunk(
destination,
stringToChunk(
escapeJSStringsForInstructionScripts(errorComponentStack),
),
);
} else {
// " data-cstck="HTMLString
writeChunk(destination, clientRenderData5);
writeChunk(
destination,
stringToChunk(escapeTextForBrowser(errorComponentStack)),
);
}
}

if (scriptFormat) {
// ></script>
return writeChunkAndReturn(destination, clientRenderScriptEnd);
} else {
// "></template>
return writeChunkAndReturn(destination, clientRenderDataEnd);
}
}

肆一、为边界写可提升项

export function writeHoistablesForBoundary(
destination: Destination,
hoistableState: HoistableState,
renderState: RenderState,
): boolean {
// Reset these on each invocation, they are only safe to read in this function
// 在每次调用时重置这些,它们只在此函数中安全读取
currentlyRenderingBoundaryHasStylesToHoist = false;
destinationHasCapacity = true;

// Flush style tags for each precedence this boundary depends on
// 刷新每个该边界所依赖的优先级的样式标签
currentlyFlushingRenderState = renderState;
hoistableState.styles.forEach(flushStyleTagsLateForBoundary, destination);
currentlyFlushingRenderState = null;

// Determine if this boundary has stylesheets that need to be awaited upon completion
// 确定此边界是否有需要等待完成的样式表
hoistableState.stylesheets.forEach(hasStylesToHoist);

// We don't actually want to flush any hoistables until the boundary is complete so we omit
// any further writing here. This is becuase unlike Resources, Hoistable Elements act more like
// regular elements, each rendered element has a unique representation in the DOM. We don't want
// these elements to appear in the DOM early, before the boundary has actually completed
// 我们实际上不希望在边界完成之前刷新任何可提升的元素,所以这里省略了任何进一步的写入。这是因为与资源不同,
// 可提升元素更像普通元素,每个渲染的元素在 DOM 中都有唯一的表示。我们不希望这些元素在边界实际完成之前就出
// 现在 DOM 中

if (currentlyRenderingBoundaryHasStylesToHoist) {
renderState.stylesToHoist = true;
}
return destinationHasCapacity;
}

肆二、写入前言开始

备注
// We don't bother reporting backpressure at the moment because we expect to
// flush the entire preamble in a single pass. This probably should be modified
// in the future to be backpressure sensitive but that requires a larger refactor
// of the flushing code in Fizz.
// 目前我们不打算报告背压,因为我们预计会在一次传递中刷新整个前导部分。将来可能应该修改为对背压敏感,
// 但这需要对 Fizz 中的刷新代码进行更大的重构。
export function writePreambleStart(
destination: Destination,
resumableState: ResumableState,
renderState: RenderState,
skipBlockingShell: boolean,
): void {
// This function must be called exactly once on every request
// 这个函数必须在每个请求中精确调用一次
if (enableFizzExternalRuntime && renderState.externalRuntimeScript) {
// If the root segment is incomplete due to suspended tasks
// 如果根段因挂起的任务而不完整
// (e.g. willFlushAllSegments = false) and we are using data
// streaming format, ensure the external runtime is sent.
// (例如 willFlushAllSegments = false)并且我们使用数据流格式,确保发送外部运行时。
// (User code could choose to send this even earlier by calling
// preinit(...), if they know they will suspend).
// (用户代码可以选择更早发送此内容,通过调用 preinit(...),如果他们知道会挂起)。
const { src, chunks } = renderState.externalRuntimeScript;
internalPreinitScript(resumableState, renderState, src, chunks);
}

const preamble = renderState.preamble;

const htmlChunks = preamble.htmlChunks;
const headChunks = preamble.headChunks;

let i = 0;

// Emit open tags before Hoistables and Resources
// 在可提升元素和资源之前发出打开标签
if (htmlChunks) {
// We have an <html> to emit as part of the preamble
// 我们有一个 <html> 要作为前言的一部分发出
for (i = 0; i < htmlChunks.length; i++) {
writeChunk(destination, htmlChunks[i]);
}
if (headChunks) {
for (i = 0; i < headChunks.length; i++) {
writeChunk(destination, headChunks[i]);
}
} else {
// We did not render a head but we emitted an <html> so we emit one now
// 我们没有渲染头部,但我们发出了一个 <html>,所以现在我们发出一个
writeChunk(destination, startChunkForTag('head'));
writeChunk(destination, endOfStartTag);
}
} else if (headChunks) {
// We do not have an <html> but we do have a <head>
// 我们没有 <html>,但我们有 <head>
for (i = 0; i < headChunks.length; i++) {
writeChunk(destination, headChunks[i]);
}
}

// Emit high priority Hoistables
// 发出高优先级可提升项
const charsetChunks = renderState.charsetChunks;
for (i = 0; i < charsetChunks.length; i++) {
writeChunk(destination, charsetChunks[i]);
}
charsetChunks.length = 0;

// emit preconnect resources
// 发出预连接资源
renderState.preconnects.forEach(flushResource, destination);
renderState.preconnects.clear();

const viewportChunks = renderState.viewportChunks;
for (i = 0; i < viewportChunks.length; i++) {
writeChunk(destination, viewportChunks[i]);
}
viewportChunks.length = 0;

renderState.fontPreloads.forEach(flushResource, destination);
renderState.fontPreloads.clear();

renderState.highImagePreloads.forEach(flushResource, destination);
renderState.highImagePreloads.clear();

// Flush unblocked stylesheets by precedence
// 按优先级刷新未阻塞的样式表
currentlyFlushingRenderState = renderState;
renderState.styles.forEach(flushStylesInPreamble, destination);
currentlyFlushingRenderState = null;

const importMapChunks = renderState.importMapChunks;
for (i = 0; i < importMapChunks.length; i++) {
writeChunk(destination, importMapChunks[i]);
}
importMapChunks.length = 0;

renderState.bootstrapScripts.forEach(flushResource, destination);

renderState.scripts.forEach(flushResource, destination);
renderState.scripts.clear();

renderState.bulkPreloads.forEach(flushResource, destination);
renderState.bulkPreloads.clear();

if ((htmlChunks || headChunks) && !skipBlockingShell) {
// If we have any html or head chunks we know that we're rendering a full document.
// A full document should block display until the full shell has downloaded.
// Therefore we insert a render blocking instruction referring to the last body
// element that's considered part of the shell. We do this after the important loads
// have already been emitted so we don't do anything to delay them but early so that
// the browser doesn't risk painting too early.
// 如果我们有任何 html 或 head 块,我们就知道我们正在渲染一个完整的文档。
// 一个完整的文档应该阻止显示,直到完整的外壳下载完成。
// 因此,我们插入一个渲染阻塞指令,指向被认为是外壳一部分的最后一个 body 元素。
// 我们在重要资源已经发出之后再执行此操作,这样就不会延迟它们,但要足够早,以免浏览器过早渲染。
writeBlockingRenderInstruction(destination, resumableState, renderState);
} else {
// We don't need to add the shell id so mark it as if sent.
// Currently it might still be sent if it was already added to a bootstrap script.
// 我们不需要添加 shell id,所以标记为已发送。
// 如果它已经被添加到引导脚本中,目前它仍可能被发送。
resumableState.instructions |= SentCompletedShellId;
}

// Write embedding hoistableChunks
// 写入可提升的嵌入块
const hoistableChunks = renderState.hoistableChunks;
for (i = 0; i < hoistableChunks.length; i++) {
writeChunk(destination, hoistableChunks[i]);
}
hoistableChunks.length = 0;
}

肆三、写前言结束

备注
// We don't bother reporting backpressure at the moment because we expect to
// flush the entire preamble in a single pass. This probably should be modified
// in the future to be backpressure sensitive but that requires a larger refactor
// of the flushing code in Fizz.
// 目前我们不打算报告背压,因为我们预计会在一次传递中刷新整个前导部分。将来可能应该修改为对背压敏感,
// 但这需要对 Fizz 中的刷新代码进行更大的重构。
export function writePreambleEnd(
destination: Destination,
renderState: RenderState,
): void {
const preamble = renderState.preamble;
const htmlChunks = preamble.htmlChunks;
const headChunks = preamble.headChunks;
if (htmlChunks || headChunks) {
// we have an <html> but we inserted an implicit <head> tag. We need
// to close it since the main content won't have it
// 我们有一个 <html>,但我们插入了一个隐含的 <head> 标签。我们需要关闭它,因为主要内容不会有它
writeChunk(destination, endChunkForTag('head'));
}

const bodyChunks = preamble.bodyChunks;
if (bodyChunks) {
for (let i = 0; i < bodyChunks.length; i++) {
writeChunk(destination, bodyChunks[i]);
}
}
}

肆四、写可提升项

备注
// We don't bother reporting backpressure at the moment because we expect to
// flush the entire preamble in a single pass. This probably should be modified
// in the future to be backpressure sensitive but that requires a larger refactor
// of the flushing code in Fizz.
// 目前我们不打算报告背压,因为我们预计会在一次传递中刷新整个前导部分。将来可能应该修改为对背压敏感,但这需要
// 对 Fizz 中的刷新代码进行更大的重构。
export function writeHoistables(
destination: Destination,
resumableState: ResumableState,
renderState: RenderState,
): void {
let i = 0;

// Emit high priority Hoistables
// 发出高优先级可提升项

// We omit charsetChunks because we have already sent the shell and if it wasn't
// already sent it is too late now.
// 我们省略 charsetChunks,因为我们已经发送了 shell,如果还没有发送,现在也太迟了。

const viewportChunks = renderState.viewportChunks;
for (i = 0; i < viewportChunks.length; i++) {
writeChunk(destination, viewportChunks[i]);
}
viewportChunks.length = 0;

renderState.preconnects.forEach(flushResource, destination);
renderState.preconnects.clear();

renderState.fontPreloads.forEach(flushResource, destination);
renderState.fontPreloads.clear();

renderState.highImagePreloads.forEach(flushResource, destination);
renderState.highImagePreloads.clear();

// Preload any stylesheets. these will emit in a render instruction that follows this
// but we want to kick off preloading as soon as possible
// 预加载任何样式表。这些将在后续的渲染指令中发出。但我们希望尽快启动预加载
renderState.styles.forEach(preloadLateStyles, destination);

// We only hoist importmaps that are configured through createResponse and that will
// always flush in the preamble. Generally we don't expect people to render them as
// tags when using React but if you do they are going to be treated like regular inline
// scripts and flush after other hoistables which is problematic
// 我们只提升通过 createResponse 配置的 importmaps,并且它们将始终在前言中刷新。通常我们不期望人们在使
// 用 React 时将它们渲染为 <script type="importmap"> 标签,但如果你这么做,它们将被当作普通的内联脚
// 本处理,并在其他可提升的脚本之后刷新,这可能会有问题

// bootstrap scripts should flush above script priority but these can only flush in the preamble
// so we elide the code here for performance
// 引导脚本应该在脚本优先级之上刷新,但这些脚本只能在前言中刷新,因此为了性能我们在这里省略代码
renderState.scripts.forEach(flushResource, destination);
renderState.scripts.clear();

renderState.bulkPreloads.forEach(flushResource, destination);
renderState.bulkPreloads.clear();

// Write embedding hoistableChunks
// 写入可提升的嵌入块
const hoistableChunks = renderState.hoistableChunks;
for (i = 0; i < hoistableChunks.length; i++) {
writeChunk(destination, hoistableChunks[i]);
}
hoistableChunks.length = 0;
}

肆五、写后记

备注
export function writePostamble(
destination: Destination,
resumableState: ResumableState,
): void {
if (resumableState.hasBody) {
writeChunk(destination, endChunkForTag('body'));
}
if (resumableState.hasHtml) {
writeChunk(destination, endChunkForTag('html'));
}
}

肆六、创建可提升状态

export function createHoistableState(): HoistableState {
return {
styles: new Set(),
stylesheets: new Set(),
suspenseyImages: false,
};
}

肆七、可提升物

export function hoistHoistables(
parentState: HoistableState,
childState: HoistableState,
): void {
childState.styles.forEach(hoistStyleQueueDependency, parentState);
childState.stylesheets.forEach(hoistStylesheetDependency, parentState);
if (childState.suspenseyImages) {
// If the child has suspensey images, the parent now does too if it's inlined.
// 如果子组件有挂起图像,那么父组件现在也会有,如果它是内联的。
// Similarly, if a SuspenseList row has a suspensey image then effectively
// the next row should be blocked on it as well since the next row can't show earlier.
// 同样地,如果一个 SuspenseList 行有挂起图像,那么实际上下一行也应阻塞它,因为下一行不能提前显示。
// In practice, since the child will be outlined this transferring
// may never matter but is conceptually correct.
// 实际上,由于子组件将被轮廓显示,这种传递可能永远不会重要,但从概念上来说是正确的。
parentState.suspenseyImages = true;
}
}

肆八、有挂起内容

export function hasSuspenseyContent(
hoistableState: HoistableState,
flushingInShell: boolean,
): boolean {
if (flushingInShell) {
// When flushing the shell, stylesheets with precedence are already emitted
// in the <head> which blocks paint. There's no benefit to outlining for CSS
// alone during the shell flush. However, suspensey images (for ViewTransition
// animation reveals) should still trigger outlining even during the shell.
// 在刷新 shell 时,具有优先级的样式表已经在 <head> 中发出,这会阻塞渲染。在 shell 刷新期间,
// 仅对 CSS 进行轮廓没有好处。然而,悬挂的图像(用于 ViewTransition 动画显示)即使在 shell
// 中也应触发轮廓。
return hoistableState.suspenseyImages;
}
return hoistableState.stylesheets.size > 0 || hoistableState.suspenseyImages;
}

肆九、提前发出预加载

// This function is called at various times depending on whether we are rendering
// or prerendering. In this implementation we only actually emit headers once and
// subsequent calls are ignored. We track whether the request has a completed shell
// to determine whether we will follow headers with a flush including stylesheets.
// 这个函数会根据我们是在渲染还是预渲染而在不同时间被调用。在这个实现中,我们实际上只会发出一次头部,随后的调
// 用会被忽略。我们会跟踪请求是否有一个完整的外壳,以确定我们是否会在头部之后进行刷新,包括样式表。
// In the context of prerrender we don't have a completed shell when the request finishes
// with a postpone in the shell. In the context of a render we don't have a completed shell
// if this is called before the shell finishes rendering which usually will happen anytime
// anything suspends in the shell.
// 在预渲染的上下文中,当请求在外壳中延期完成时,我们没有完整的外壳。在渲染的上下文中,如果在外壳完成渲染之前
// 调用此函数,我们也没有完整的外壳,通常这会在外壳中任何内容挂起时发生。
export function emitEarlyPreloads(
renderState: RenderState,
resumableState: ResumableState,
shellComplete: boolean,
): void {
const onHeaders = renderState.onHeaders;
if (onHeaders) {
const headers = renderState.headers;
if (headers) {
// Even if onHeaders throws we don't want to call this again so
// we drop the headers state from this point onwards.
// 即使 onHeaders 抛出异常,我们也不想再次调用它,所以从这一点起我们放弃 headers 状态。
renderState.headers = null;

let linkHeader = headers.preconnects;
if (headers.fontPreloads) {
if (linkHeader) {
linkHeader += ', ';
}
linkHeader += headers.fontPreloads;
}
if (headers.highImagePreloads) {
if (linkHeader) {
linkHeader += ', ';
}
linkHeader += headers.highImagePreloads;
}

if (!shellComplete) {
// We use raw iterators because we want to be able to halt iteration
// 我们使用原始迭代器是因为我们希望能够停止迭代
// We could refactor renderState to store these dually in arrays to
// make this more efficient at the cost of additional memory and
// write overhead. However this code only runs once per request so
// for now I consider this sufficient.
// 我们可以重构 renderState,将这些同时存储在数组中,以提高效率,但代价是增加额外的
// 内存和写入开销。然而,这段代码每个请求只运行一次,因此目前我认为这样已经足够。
const queueIter = renderState.styles.values();
outer: for (
let queueStep = queueIter.next();
headers.remainingCapacity > 0 && !queueStep.done;
queueStep = queueIter.next()
) {
const sheets = queueStep.value.sheets;
const sheetIter = sheets.values();
for (
let sheetStep = sheetIter.next();
headers.remainingCapacity > 0 && !sheetStep.done;
sheetStep = sheetIter.next()
) {
const sheet = sheetStep.value;
const props = sheet.props;
const key = getResourceKey(props.href);

const header = getStylesheetPreloadAsHeader(sheet);
// We mutate the capacity b/c we don't want to keep checking if later headers will fit.
// 我们会修改容量,因为我们不想不断检查后续的头是否能够放得下。
// This means that a particularly long header might close out the header queue where later
// headers could still fit. We could in the future alter the behavior here based on prerender vs render
// since during prerender we aren't as concerned with pure runtime performance.
// 这意味着一个特别长的头可能会关闭头队列,而后续的头本来可能还可以放得下。将来我们可以根据预渲
// 染与渲染来改变这里的行为,因为在预渲染期间,我们并不太关心纯粹的运行时性能。
if ((headers.remainingCapacity -= header.length + 2) >= 0) {
renderState.resets.style[key] = PRELOAD_NO_CREDS;
if (linkHeader) {
linkHeader += ', ';
}
linkHeader += header;

// We already track that the resource exists in resumableState however
// if the resumableState resets because we postponed in the shell
// which is what is happening in this branch if we are prerendering
// then we will end up resetting the resumableState. When it resets we
// want to record the fact that this stylesheet was already preloaded
// 我们已经在 resumableState 中跟踪了该资源是否存在,然而如果 resumableState 因为
// 我们在 shell 中推迟而重置,这就是在我们进行预渲染时在这个分支中发生的情况,那么我们
// 最终会重置 resumableState。当它重置时,我们想要记录该样式表已经预加载的事实
renderState.resets.style[key] =
typeof props.crossOrigin === 'string' ||
typeof props.integrity === 'string'
? [props.crossOrigin, props.integrity]
: PRELOAD_NO_CREDS;
} else {
break outer;
}
}
}
}
if (linkHeader) {
onHeaders({
Link: linkHeader,
});
} else {
// We still call this with no headers because a user may be using it as a signal that
// it React will not provide any headers
// 我们仍然在没有头信息的情况下调用它,因为用户可能将其用作信号,表明 React 不会提供任何头信息
onHeaders({});
}
return;
}
}
}

伍、常量

1. 前调度器

备注

源码中 96 - 108 行

// 上一次调度器
const previousDispatcher =
// ReactDOM 当前调度器
ReactDOMSharedInternals.d; /* ReactDOMCurrentDispatcher */

// ReactDOM 当前调度器
ReactDOMSharedInternals.d /* ReactDOMCurrentDispatcher */ = {
// 同步刷新工作
f /* flushSyncWork */: previousDispatcher.f /* flushSyncWork */,
// 请求表单重置
r /* requestFormReset */: previousDispatcher.r /* requestFormReset */,
// 预取DNS
D /* prefetchDNS */: prefetchDNS,
// 预连接
C /* preconnect */: preconnect,
// 预加载
L /* preload */: preload,
// 预加载模块
m /* preloadModule */: preloadModule,
// 预初始化脚本
X /* preinitScript */: preinitScript,
// 预初始化样式
S /* preinitStyle */: preinitStyle,
// 预初始化模块脚本
M /* preinitModuleScript */: preinitModuleScript,
};

2. 脚本流式格式

备注

源码 123 - 124 行

// 脚本流式格式
const ScriptStreamingFormat: StreamingFormat = 0;
// 数据流格式
const DataStreamingFormat: StreamingFormat = 1;

3. 未发送任何内容

备注

源码中 127 - 136 行

// 未发送任何内容
const NothingSent /* */ = 0b000000000;
// 发送完成段功能
const SentCompleteSegmentFunction /* */ = 0b000000001;
// 发送完成边界函数
const SentCompleteBoundaryFunction /* */ = 0b000000010;
// 发送客户端渲染函数
const SentClientRenderFunction /* */ = 0b000000100;
// 句子风格插入函数
const SentStyleInsertionFunction /* */ = 0b000001000;
// 已发送表单重放运行时
const SentFormReplayingRuntime /* */ = 0b000010000;
// 已完成发送的外壳ID
const SentCompletedShellId /* */ = 0b000100000;
// 已发送标记外壳时间
const SentMarkShellTime /* */ = 0b001000000;
// 需要升级以查看过渡
const NeedUpgradeToViewTransitions /* */ = 0b010000000;
// 已发送升级以查看过渡
const SentUpgradeToViewTransitions /* */ = 0b100000000;

4. 存在

备注

源码中 250 - 258 行

const EXISTS: Exists = null;

// This constant is to mark preloads that have no unique credentials
// to convey. It should never be checked by identity and we should not
// assume Preload values in ResumableState equal this value because they
// will have come from some parsed input.

// 这个常量用于标记没有唯一凭据的预加载不应通过身份来检查它,也不应假设 ResumableState 中的 Preload 值等于
// 此值,因为它们将来自某些解析后的输入。

// 预加载无凭据
const PRELOAD_NO_CREDS: Preloaded = [];
if (__DEV__) {
Object.freeze(PRELOAD_NO_CREDS);
}

5. 数据元素引用结束

备注

源码中 314 - 326 行

// 数据元素引用结束
const dataElementQuotedEnd = stringToPrecomputedChunk('"></template>');

// 开始内联脚本
const startInlineScript = stringToPrecomputedChunk('<script');
// 结束内联脚本
const endInlineScript = stringToPrecomputedChunk('</script>');

// 开始脚本来源
const startScriptSrc = stringToPrecomputedChunk('<script src="');
// 开始模块来源
const startModuleSrc = stringToPrecomputedChunk('<script type="module" src="');
// 脚本随机数
const scriptNonce = stringToPrecomputedChunk(' nonce="');
// 脚本完整性
const scriptIntegirty = stringToPrecomputedChunk(' integrity="');
// 脚本跨来源
const scriptCrossOrigin = stringToPrecomputedChunk(' crossorigin="');
// 结束异步脚本
const endAsyncScript = stringToPrecomputedChunk(' async=""></script>');

// 开始内联样式
const startInlineStyle = stringToPrecomputedChunk('<style');

6. 脚本正则

备注

源码中 344 - 350 行

// 脚本正则
const scriptRegex = /(<\/|<)(s)(cript)/gi;
// 脚本替换器
const scriptReplacer = (
match: string,
prefix: string,
s: string,
suffix: string,
) => `${prefix}${s === 's' ? '\\u0073' : '\\u0053'}${suffix}`;

7. 导入 Map 脚本开始

备注

源码中 362 - 374 行

// 导入 Map 脚本开始
const importMapScriptStart = stringToPrecomputedChunk(
'<script type="importmap">',
);
// 导入 Map 脚本结束
const importMapScriptEnd = stringToPrecomputedChunk('</script>');

// Since we store headers as strings we deal with their length in utf16 code units
// rather than visual characters or the utf8 encoding that is used for most binary
// serialization. Some common HTTP servers only allow for headers to be 4kB in length.
// 由于我们将头部存储为字符串,所以我们处理的是它们在 utf16 代码单元中的长度而不是视觉字符的长度,也不是用于
// 大多数二进制序列化的 utf8 编码。某些常见的 HTTP 服务器只允许头部长度为 4kB。
// We choose a default length that is likely to be well under this already limited length however
// pathological cases may still cause the utf-8 encoding of the headers to approach this limit.
// 我们选择了一个默认长度,这个长度很可能远低于这个已限制的长度,但极端情况下仍可能导致头部的 utf-8 编码接近
// 该限制。
// It should also be noted that this maximum is a soft maximum. we have not reached the limit we will
// allow one more header to be captured which means in practice if the limit is approached it will be exceeded
// 还应注意,这个最大值是一个软性最大值。如果还没有达到限制,我们将允许再捕获一个头部,这意味着在实际操作中,如
// 果接近限制,它可能会被超过。
const DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS = 2000;

8. HTML HTML 模式

备注

源码中 769 - 781 行

// We have a less than HTML_HTML_MODE check elsewhere. If you add more cases here, make sure it
// still makes sense
// 我们在其他地方有一个小于 HTML_HTML_MODE 的检查。如果你在这里添加更多情况,请确保它仍然有意义

// 如果 <html> 位于顶层,则使用此项。
// HTML HTML 模式
const HTML_HTML_MODE = 1; // Used for the <html> if it is at the top level.
// HTML 模式
const HTML_MODE = 2;
// HTML 头部模式
const HTML_HEAD_MODE = 3;
// SVG 模式
const SVG_MODE = 4;
// MATHML 模式
const MATHML_MODE = 5;
// HTML 表格模式
const HTML_TABLE_MODE = 6;
// HTML 表格躯干模式
const HTML_TABLE_BODY_MODE = 7;
// HTML 表格行模式
const HTML_TABLE_ROW_MODE = 8;
// HTML 表格列组模式
const HTML_COLGROUP_MODE = 9;
// We have a greater than HTML_TABLE_MODE check elsewhere. If you add more cases here, make sure it
// still makes sense
// 我们在其他地方有一个大于 HTML_TABLE_MODE 的检查。如果你在这里添加更多情况,请确保它仍然有意义

9. 无范围

备注

源码 785 - 796 行

// 无范围
const NO_SCOPE = /* */ 0b0000000;
// 无脚本范围
const NOSCRIPT_SCOPE = /* */ 0b0000001;
// 图片范围
const PICTURE_SCOPE = /* */ 0b0000010;
// 回退范围
const FALLBACK_SCOPE = /* */ 0b0000100;

// Suspense 回退下面的直接实例是唯一可以“退出”的东西
// 退出范围
const EXIT_SCOPE = /* */ 0b0001000; // A direct Instance below a Suspense fallback is the only thing that can "exit"

// Suspense 内容下的直接实例是唯一可以“进入”的东西
// 进入范围
const ENTER_SCOPE = /* */ 0b0010000; // A direct Instance below Suspense content is the only thing that can "enter"

// 在一个作用域内,如果这里的任何内容发生变化,将应用“update”视图过渡。
// 更新范围
const UPDATE_SCOPE = /* */ 0b0100000; // Inside a scope that applies "update" ViewTransitions if anything mutates here.

// 以下 Suspense 内容子树可能出现在“进入”动画或“共享”动画中。
// 显示范围
const APPEARING_SCOPE = /* */ 0b1000000; // Below Suspense content subtree which might appear in an "enter" animation or "shared" animation.

// Everything not listed here are tracked for the whole subtree as opposed to just
// until the next Instance.
// 此处未列出的所有内容都会被追踪整个子树,而不仅仅是直到下一个实例。

// /子树范围
const SUBTREE_SCOPE = ~(ENTER_SCOPE | EXIT_SCOPE);

10. 文本分隔符

备注

源码中 1114 行

const textSeparator = stringToPrecomputedChunk('<!-- -->');

11. 样式名称缓存

备注

源码中 1183 行

const styleNameCache: Map<string, PrecomputedChunk> = new Map();

12. 样式属性开始

备注

源码中 1196 - 1198 行

// 样式属性开始
const styleAttributeStart = stringToPrecomputedChunk(' style="');
// 样式分配
const styleAssign = stringToPrecomputedChunk(':');
// 样式分隔符
const styleSeparator = stringToPrecomputedChunk(';');

13. 属性分隔符

备注

源码中 1279 - 1282 行

// 属性分隔符
const attributeSeparator = stringToPrecomputedChunk(' ');
// 属性分配
const attributeAssign = stringToPrecomputedChunk('="');
// 属性结束
const attributeEnd = stringToPrecomputedChunk('"');
// 属性为空字符串
const attributeEmptyString = stringToPrecomputedChunk('=""');

14. 动作JavaScript网址

备注

源码中 1320 - 1329 行

// Since this will likely be repeated a lot in the HTML, we use a more concise message
// than on the client and hopefully it's googleable.
// 由于这可能会在 HTML 中被频繁重复,我们使用比客户端更简洁的消息,希望它能被谷歌搜索到。
const actionJavaScriptURL = stringToPrecomputedChunk(
escapeTextForBrowser(
"javascript:throw new Error('React form unexpectedly submitted.')",
),
);

// 开始隐藏输入块
const startHiddenInputChunk = stringToPrecomputedChunk('<input type="hidden"');

15. 开始标签结束

备注

源码中 1850 - 1851 行

// 开始标签结束
const endOfStartTag = stringToPrecomputedChunk('>');
// 开始标签自闭合结束
const endOfStartTagSelfClosing = stringToPrecomputedChunk('/>');

16. 选定标记属性

备注

源码中 2129 行

const selectedMarkerAttribute = stringToPrecomputedChunk(' selected=""');

17. 表单重放运行时脚本

备注

源码中 2232 - 2233 行

const formReplayingRuntimeScript =
stringToPrecomputedChunk(formReplayingRuntime);

18. 表单状态标记匹配中

备注

源码中 2276 - 2277 行

// 表单状态标记匹配中
const formStateMarkerIsMatching = stringToPrecomputedChunk('<!--F!-->');
// 表单状态标记不匹配
const formStateMarkerIsNotMatching = stringToPrecomputedChunk('<!--F-->');

19. 样式正则

备注

源码中 3203 - 3209 行

// 样式正则
const styleRegex = /(<\/|<)(s)(tyle)/gi;
// 样式替换器
const styleReplacer = (
match: string,
prefix: string,
s: string,
suffix: string,
) => `${prefix}${s === 's' ? '\\73 ' : '\\53 '}${suffix}`;

20. 头部序言贡献块

备注

源码中 3653 - 3657 行

// These are used by the client if we clear a boundary and we find these, then we
// also clear the singleton as well.
// 如果客户端清理一个边界并发现这些,我们也会清理单例。

// 头部序言贡献块
const headPreambleContributionChunk = stringToPrecomputedChunk('<!--head-->');
// 正文前言贡献块
const bodyPreambleContributionChunk = stringToPrecomputedChunk('<!--body-->');
// html 前言贡献块
const htmlPreambleContributionChunk = stringToPrecomputedChunk('<!--html-->');

21. 前导换行

备注

源码中 4049 行

const leadingNewline = stringToPrecomputedChunk('\n');

22. 有效标签正则

备注

源码中 4130 - 4134 行

// We accept any tag to be rendered but since this gets injected into arbitrary
// HTML, we want to make sure that it's a safe tag.
// 我们接受任何要渲染的标签,但由于这会被注入到任意 HTML 中,我们希望确保它是一个安全的标签。
// http://www.w3.org/TR/REC-xml/#NT-Name

// // 简化子集
// 有效标签正则
const VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; // Simplified subset
// 已验证标签缓存
const validatedTagCache = new Map<string, PrecomputedChunk>();

23. 结束标签缓存

备注

源码中 4357 行

const endTagCache = new Map<string, PrecomputedChunk>();

24. shell 时间运行脚本

备注

源码中 4482 行

const shellTimeRuntimeScript = stringToPrecomputedChunk(markShellTime);

25. 占位符 1

备注

源码中 4539 - 4545 行

// Structural Nodes
// 结构节点

// A placeholder is a node inside a hidden partial tree that can be filled in later, but before
// display. It's never visible to users. We use the template tag because it can be used in every
// type of parent. <script> tags also work in every other tag except <colgroup>.
// 占位符是隐藏的部分树中的一个节点,可以在稍后填充,但必须在显示之前。它对用户永远不可见。我们使用
// template 标签,因为它可以用于每种类型的父元素。<script> 标签在除 <colgroup> 外的所有其他标签中也能使用。

// 占位符 1
const placeholder1 = stringToPrecomputedChunk('<template id="');
// 占位符 2
const placeholder2 = stringToPrecomputedChunk('"></template>');

26. 启动活动边界

备注

源码中 4558 - 4560 行

// Activity boundaries are encoded as comments.
// 活动边界被编码为注释。
// 活动边界「开始」
const startActivityBoundary = stringToPrecomputedChunk('<!--&-->');
// 活动边界「结束」
const endActivityBoundary = stringToPrecomputedChunk('<!--/&-->');

27. 开始完成挂起边界

备注

源码中 4576 - 4599 行

// Suspense boundaries are encoded as comments.
// 挂起边界被编码为注释。

// 开始完成挂起边界
const startCompletedSuspenseBoundary = stringToPrecomputedChunk('<!--$-->');
// 开始待处理悬而未决边界 1
const startPendingSuspenseBoundary1 = stringToPrecomputedChunk(
'<!--$?--><template id="',
);
// 开始待处理悬而未决边界 2
const startPendingSuspenseBoundary2 = stringToPrecomputedChunk('"></template>');
// 启动客户端渲染的挂起边界
const startClientRenderedSuspenseBoundary =
stringToPrecomputedChunk('<!--$!-->');
// 结束挂起边界
const endSuspenseBoundary = stringToPrecomputedChunk('<!--/$-->');

// 客户端渲染等待边界错误 1
const clientRenderedSuspenseBoundaryError1 =
stringToPrecomputedChunk('<template');
// 客户端渲染挂起边界错误属性插页
const clientRenderedSuspenseBoundaryErrorAttrInterstitial =
stringToPrecomputedChunk('"');
// 客户端渲染的挂起边界错误 1A
const clientRenderedSuspenseBoundaryError1A =
stringToPrecomputedChunk(' data-dgst="');
// 客户端渲染的挂起边界错误 1B
const clientRenderedSuspenseBoundaryError1B =
stringToPrecomputedChunk(' data-msg="');
// 客户端渲染的挂起边界错误 1C
const clientRenderedSuspenseBoundaryError1C =
stringToPrecomputedChunk(' data-stck="');
// 客户端渲染的挂起边界错误 1D
const clientRenderedSuspenseBoundaryError1D =
stringToPrecomputedChunk(' data-cstck="');
// 客户端渲染等待边界错误 2
const clientRenderedSuspenseBoundaryError2 =
stringToPrecomputedChunk('></template>');

28. 开始段落 HTML

备注

源码中 4703 - 4737 行

// 开始段落 HTML
const startSegmentHTML = stringToPrecomputedChunk('<div hidden id="');
// 开始段落 HTML 2
const startSegmentHTML2 = stringToPrecomputedChunk('">');
// 结束段落 HTML
const endSegmentHTML = stringToPrecomputedChunk('</div>');

// 开始段落 SVG
const startSegmentSVG = stringToPrecomputedChunk(
'<svg aria-hidden="true" style="display:none" id="',
);
// 开始段落 SVG 2
const startSegmentSVG2 = stringToPrecomputedChunk('">');
// 结束段落 SVG
const endSegmentSVG = stringToPrecomputedChunk('</svg>');

// 开始段落 MathML
const startSegmentMathML = stringToPrecomputedChunk(
'<math aria-hidden="true" style="display:none" id="',
);
// 开始段落 MathML 2
const startSegmentMathML2 = stringToPrecomputedChunk('">');
// 结束段落 MathML
const endSegmentMathML = stringToPrecomputedChunk('</math>');

// 开始段落表格
const startSegmentTable = stringToPrecomputedChunk('<table hidden id="');
// 开始段落表格 2
const startSegmentTable2 = stringToPrecomputedChunk('">');
// 结束段落表格
const endSegmentTable = stringToPrecomputedChunk('</table>');

// 开始段落表格躯干
const startSegmentTableBody = stringToPrecomputedChunk(
'<table hidden><tbody id="',
);
// 开始段落表格躯干 2
const startSegmentTableBody2 = stringToPrecomputedChunk('">');
// 结束段落表格躯干
const endSegmentTableBody = stringToPrecomputedChunk('</tbody></table>');

// 开始段落表格行
const startSegmentTableRow = stringToPrecomputedChunk('<table hidden><tr id="');
// 开始段落表格行 2
const startSegmentTableRow2 = stringToPrecomputedChunk('">');
// 结束段落表格行
const endSegmentTableRow = stringToPrecomputedChunk('</tr></table>');

// 开始段落表格列组
const startSegmentColGroup = stringToPrecomputedChunk(
'<table hidden><colgroup id="',
);
// 开始段落表格列组 2
const startSegmentColGroup2 = stringToPrecomputedChunk('">');
// 结束段落表格列组
const endSegmentColGroup = stringToPrecomputedChunk('</colgroup></table>');

29. 完成段落脚本 1 完整

备注

源码中 4835 - 4846 行

// 完成段落脚本 1 完整
const completeSegmentScript1Full = stringToPrecomputedChunk(
completeSegmentFunction + '$RS("',
);
// 完成段脚本 1 部分
const completeSegmentScript1Partial = stringToPrecomputedChunk('$RS("');
// 完成段落脚本 2
const completeSegmentScript2 = stringToPrecomputedChunk('","');
// 完成段落脚本结束
const completeSegmentScriptEnd = stringToPrecomputedChunk('")</script>');

// 完整段数据 1
const completeSegmentData1 = stringToPrecomputedChunk(
'<template data-rsi="" data-sid="',
);
// 完成段数据 2
const completeSegmentData2 = stringToPrecomputedChunk('" data-pid="');
// 完成段数据结束
const completeSegmentDataEnd = dataElementQuotedEnd;

30. 仅完成边界脚本功能

备注

源码中 4894 - 4921 行

// 仅完成边界脚本功能
const completeBoundaryScriptFunctionOnly = stringToPrecomputedChunk(
completeBoundaryFunction,
);
// 完成边界升级到视图过渡说明
const completeBoundaryUpgradeToViewTransitionsInstruction = stringToChunk(
upgradeToViewTransitionsInstruction,
);
// 完成边界脚本1部分
const completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("');
// 完整边界含样式脚本 1 全部分
const completeBoundaryWithStylesScript1FullPartial = stringToPrecomputedChunk(
styleInsertionFunction + '$RR("',
);

// 完整边界含样式脚本1部分
const completeBoundaryWithStylesScript1Partial =
stringToPrecomputedChunk('$RR("');
// 完成边界脚本2
const completeBoundaryScript2 = stringToPrecomputedChunk('","');
// 完成边界脚本 3a
const completeBoundaryScript3a = stringToPrecomputedChunk('",');
// 完成边界脚本 3b
const completeBoundaryScript3b = stringToPrecomputedChunk('"');
// 完成边界脚本结束
const completeBoundaryScriptEnd = stringToPrecomputedChunk(')</script>');

// 完整边界数据 1
const completeBoundaryData1 = stringToPrecomputedChunk(
'<template data-rci="" data-bid="',
);
// 完整边界与样式数据 1
const completeBoundaryWithStylesData1 = stringToPrecomputedChunk(
'<template data-rri="" data-bid="',
);
// 完整边界数据 2
const completeBoundaryData2 = stringToPrecomputedChunk('" data-sid="');
// 完整边界数据 3 a
const completeBoundaryData3a = stringToPrecomputedChunk('" data-sty="');
// 完整边界数据结束
const completeBoundaryDataEnd = dataElementQuotedEnd;

31. 仅客户端渲染脚本函数

备注

源码中 5053 - 5071 行

// 仅客户端渲染脚本函数
const clientRenderScriptFunctionOnly =
stringToPrecomputedChunk(clientRenderFunction);

// 客户端渲染脚本 1 完整
const clientRenderScript1Full = stringToPrecomputedChunk(
clientRenderFunction + ';$RX("',
);
// 客户端渲染脚本 1 部分
const clientRenderScript1Partial = stringToPrecomputedChunk('$RX("');
// 客户端渲染脚本 1A
const clientRenderScript1A = stringToPrecomputedChunk('"');
// 客户端渲染错误脚本参数插页式广告
const clientRenderErrorScriptArgInterstitial = stringToPrecomputedChunk(',');
// 客户端渲染脚本结束
const clientRenderScriptEnd = stringToPrecomputedChunk(')</script>');

// 客户端渲染数据 1
const clientRenderData1 = stringToPrecomputedChunk(
'<template data-rxi="" data-bid="',
);
// 客户端渲染数据 2
const clientRenderData2 = stringToPrecomputedChunk('" data-dgst="');
// 客户端渲染数据 3
const clientRenderData3 = stringToPrecomputedChunk('" data-msg="');
// 客户端渲染数据 4
const clientRenderData4 = stringToPrecomputedChunk('" data-stck="');
// 客户端渲染数据 5
const clientRenderData5 = stringToPrecomputedChunk('" data-cstck="');
// 客户端渲染数据结束
const clientRenderDataEnd = dataElementQuotedEnd;

32. 用于指令脚本中 JS 字符串的正则表达式

备注

源码中 5193 行

const regexForJSStringsInInstructionScripts = /[<\u2028\u2029]/g;

33. 用于脚本中JS字符串的正则表达式

备注

源码中 5215 行

const regexForJSStringsInScripts = /[&><\u2028\u2029]/g;

34. 晚期样式标签资源打开 1

备注

源码中 5241 - 5246 行

// 晚期样式标签资源打开 1
const lateStyleTagResourceOpen1 = stringToPrecomputedChunk(
' media="not all" data-precedence="',
);
// 晚期样式标签资源打开 2
const lateStyleTagResourceOpen2 = stringToPrecomputedChunk('" data-href="');
// 晚期样式标签资源打开 3
const lateStyleTagResourceOpen3 = stringToPrecomputedChunk('">');
// 晚期样式标签资源关闭
const lateStyleTagTemplateClose = stringToPrecomputedChunk('</style>');

35. 样式表刷新队列

备注

源码中 5348 行

const stylesheetFlushingQueue: Array<Chunk | PrecomputedChunk> = [];

36. 样式标签资源打开1

备注

源码中 5368 - 5373 行

// 样式标签资源打开 1
const styleTagResourceOpen1 = stringToPrecomputedChunk(' data-precedence="');
// 样式标签资源打开 2
const styleTagResourceOpen2 = stringToPrecomputedChunk('" data-href="');
// 空格分隔符
const spaceSeparator = stringToPrecomputedChunk(' ');
// 样式标签资源打开 3
const styleTagResourceOpen3 = stringToPrecomputedChunk('">');

// 样式标签资源结束
const styleTagResourceClose = stringToPrecomputedChunk('</style>');

37. 阻塞渲染块开始

备注

源码中 5439 - 5444 行

// 阻塞渲染块开始
const blockingRenderChunkStart = stringToPrecomputedChunk(
'<link rel="expect" href="#',
);
// 阻塞渲染块结束
const blockingRenderChunkEnd = stringToPrecomputedChunk(
'" blocking="render"/>',
);

38. 已完成的 ShellId 属性开始

备注

源码中 5460 行

const completedShellIdAttributeStart = stringToPrecomputedChunk(' id="');

39. 数组第一个开括号

备注

源码中 5702 - 5705 行

// 数组第一个开括号
const arrayFirstOpenBracket = stringToPrecomputedChunk('[');
// 数组后续左括号
const arraySubsequentOpenBracket = stringToPrecomputedChunk(',[');
// 数组元素分割符
const arrayInterstitial = stringToPrecomputedChunk(',');
// 数组右括号
const arrayCloseBracket = stringToPrecomputedChunk(']');

40. 待定

备注

源码中 6101 - 6104 行

// 待定
const PENDING: StylesheetState = 0;
// 预加载
const PRELOADED: StylesheetState = 1;
// 序言
const PREAMBLE: StylesheetState = 2;
// 迟到
const LATE: StylesheetState = 3;

伍一、变量

1. 当前正在刷新渲染状态

备注

源码中 312 行

let currentlyFlushingRenderState: RenderState | null = null;

2. 对具有空值的新布尔属性发出警告

备注

源码中 376 - 379 行

let didWarnForNewBooleanPropsWithEmptyValue: { [string]: boolean };
if (__DEV__) {
didWarnForNewBooleanPropsWithEmptyValue = {};
}

3. 二进制大对象缓存

备注

源码中 1492 行

let blobCache: null | WeakMap<Blob, Thenable<string>> = null;

4. 已警告默认输入值

备注

源码中 1883 - 1895 行

// TODO: Move these to RenderState so that we warn for every request.
// It would help debugging in stateful servers (e.g. service worker).
// 待办事项:将这些移动到 RenderState,以便我们对每个请求发出警告。这将有助于调试有状态的服务器(例如,服务
// 工作者)。
// 已警告默认输入值
let didWarnDefaultInputValue = false;
// 已警告默认选中
let didWarnDefaultChecked = false;
// 已警告默认选择值
let didWarnDefaultSelectValue = false;
// 已警告默认文本区域值
let didWarnDefaultTextareaValue = false;
// 已警告无效的选项子项
let didWarnInvalidOptionChildren = false;
// 已警告无效的选项InnerHTML
let didWarnInvalidOptionInnerHTML = false;
// 已警告所选集合的选项
let didWarnSelectedSetOnOption = false;
// 已警告表单操作类型
let didWarnFormActionType = false;
// 已警告表单操作名称
let didWarnFormActionName = false;
// 已警告表单操作目标
let didWarnFormActionTarget = false;
// 已警告表单操作方法
let didWarnFormActionMethod = false;

5. 当前渲染边界有样式需要提升

备注

源码中 5248 - 5253 行

// Tracks whether the boundary currently flushing is flushign style tags or has any
// stylesheet dependencies not flushed in the Preamble.
// 跟踪边界当前是否正在刷新样式标签或在序言中是否有任何尚未刷新的样式表依赖。

// 当前渲染边界有样式需要提升
let currentlyRenderingBoundaryHasStylesToHoist = false;

// Acts as a return value for the forEach execution of style tag flushing.
// 用作 style 标签刷新的 forEach 执行的返回值。

// 目标有容量
let destinationHasCapacity = true;

伍二、工具

1. 转义整个内联脚本内容

/**
* This escaping function is designed to work with with inline scripts where the entire
* contents are escaped. Because we know we are escaping the entire script we can avoid for instance
* escaping html comment string sequences that are valid javascript as well because
* if there are no sebsequent <script sequences the html parser will never enter
* script data double escaped state (see: https://www.w3.org/TR/html53/syntax.html#script-data-double-escaped-state)
* 这个转义函数旨在与内联脚本一起使用,其中整个内容都被转义。因为我们知道我们正在转义整个脚本,所以我们可以避免
* 例如转义 HTML 注释字符串序列,这些序列在 JavaScript 中也是有效的,因为如果没有后续的 <script 序列,
* HTML 解析器永远不会进入脚本数据双重转义状态
*
* While untrusted script content should be made safe before using this api it will
* ensure that the script cannot be early terminated or never terminated state
* 在使用此 API 之前,应确保不受信任的脚本内容是安全的,它将确保脚本不会被提前终止或永远不终止的状态
*/
function escapeEntireInlineScriptContent(scriptText: string) {
if (__DEV__) {
checkHtmlStringCoercion(scriptText);
}
return ('' + scriptText).replace(scriptRegex, scriptReplacer);
}

2. 创建格式上下文

function createFormatContext(
insertionMode: InsertionMode,
selectedValue: null | string | Array<string>,
tagScope: number,
viewTransition: null | ViewTransitionContext,
): FormatContext {
return {
insertionMode,
selectedValue,
tagScope,
viewTransition,
};
}

3. 获取悬挂视图过渡

function getSuspenseViewTransition(
parentViewTransition: null | ViewTransitionContext,
): null | ViewTransitionContext {
if (parentViewTransition === null) {
return null;
}
// If a ViewTransition wraps a Suspense boundary it applies to the children Instances
// in both the fallback and the content.
// 如果一个 ViewTransition 包裹了一个 Suspense 边界,它会应用到备用和内容中的子实例。
// Since we only have a representation of ViewTransitions on the Instances themselves
// we cannot model the parent ViewTransition activating "enter", "exit" or "share"
// since those would be ambiguous with the Suspense boundary changing states and
// affecting the same Instances.
// 由于我们只在实例本身上有 ViewTransitions 的表示我们无法建模父 ViewTransition 激活“enter”、
// “exit”或“share”。因为这些操作会与 Suspense 边界状态的变化产生歧义,并影响同样的实例。
// We also can't model an "update" when that update is fallback nodes swapping for
// content nodes. However, we can model is as a "share" from the fallback nodes to
// the content nodes using the same name. We just have to assign the same name that
// we would've used (the parent ViewTransition name or auto-assign one).
// 当更新是备用节点与内容节点交换时,我们也无法建模“update”。然而,我们可以将其建模为使用相同名称从备用节
// 点到内容节点的“share”。我们只需分配与我们将使用的名称相同的名称(父 ViewTransition 名称或自动分配一个)
const viewTransition: ViewTransitionContext = {
// 用于深度更新。
update: parentViewTransition.update, // For deep updates.
enter: 'none',
exit: 'none',
// 用于显示的退出或进入。
share: parentViewTransition.update, // For exit or enter of reveals.
name: parentViewTransition.autoName,
autoName: parentViewTransition.autoName,
// TOOD: If we have more than just this Suspense boundary as a child of the ViewTransition
// then the parent needs to isolate the names so that they don't conflict.
// TODO: 如果我们在 ViewTransition 下的子元素不止这个 Suspense 边界那么父组件需要隔离这些名字,以
// 防它们冲突。
nameIdx: 0,
};
return viewTransition;
}

4. 编码 HTML 文本节点

备注
function encodeHTMLTextNode(text: string): string {
return escapeTextForBrowser(text);
}

5. 处理样式名称

备注
function processStyleName(styleName: string): PrecomputedChunk {
const chunk = styleNameCache.get(styleName);
if (chunk !== undefined) {
return chunk;
}
const result = stringToPrecomputedChunk(
escapeTextForBrowser(hyphenateStyleName(styleName)),
);
styleNameCache.set(styleName, result);
return result;
}

6. 推送样式属性

备注
function pushStyleAttribute(
target: Array<Chunk | PrecomputedChunk>,
style: Object,
): void {
if (typeof style !== 'object') {
throw new Error(
'The `style` prop expects a mapping from style properties to values, ' +
"not a string. For example, style={{marginRight: spacing + 'em'}} when " +
'using JSX.',
);
}

let isFirst = true;
for (const styleName in style) {
if (!hasOwnProperty.call(style, styleName)) {
continue;
}
// If you provide unsafe user data here they can inject arbitrary CSS
// which may be problematic (I couldn't repro this):
// 如果你在这里提供不安全的用户数据,他们可以注入任意的 CSS 这可能会有问题(我无法复现这个问题):
// https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
// http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/
// This is not an XSS hole but instead a potential CSS injection issue
// which has lead to a greater discussion about how we're going to
// trust URLs moving forward. See #2115901
// 这不是一个 XSS 漏洞,而是一个潜在的 CSS 注入问题。这引发了关于我们今后如何信任 URL 的更大讨论。
const styleValue = style[styleName];
if (
styleValue == null ||
typeof styleValue === 'boolean' ||
styleValue === ''
) {
// TODO: We used to set empty string as a style with an empty value. Does that ever make sense?
// 待办事项:我们过去会将空字符串作为样式并赋予空值。这有意义吗?
continue;
}

let nameChunk;
let valueChunk;
const isCustomProperty = styleName.indexOf('--') === 0;
if (isCustomProperty) {
nameChunk = stringToChunk(escapeTextForBrowser(styleName));
if (__DEV__) {
checkCSSPropertyStringCoercion(styleValue, styleName);
}
valueChunk = stringToChunk(
escapeTextForBrowser(('' + styleValue).trim()),
);
} else {
if (__DEV__) {
warnValidStyle(styleName, styleValue);
}

nameChunk = processStyleName(styleName);
if (typeof styleValue === 'number') {
if (styleValue !== 0 && !isUnitlessNumber(styleName)) {
// 假定无单位数字的隐式 'px' 后缀
valueChunk = stringToChunk(styleValue + 'px'); // Presumes implicit 'px' suffix for unitless numbers
} else {
valueChunk = stringToChunk('' + styleValue);
}
} else {
if (__DEV__) {
checkCSSPropertyStringCoercion(styleValue, styleName);
}
valueChunk = stringToChunk(
escapeTextForBrowser(('' + styleValue).trim()),
);
}
}
if (isFirst) {
isFirst = false;
// If it's first, we don't need any separators prefixed.
// 如果是第一个,我们不需要任何前置分隔符。
target.push(styleAttributeStart, nameChunk, styleAssign, valueChunk);
} else {
target.push(styleSeparator, nameChunk, styleAssign, valueChunk);
}
}
if (!isFirst) {
target.push(attributeEnd);
}
}

7. 推送布尔属性

备注
function pushBooleanAttribute(
target: Array<Chunk | PrecomputedChunk>,
name: string,
// 非空或未定义
value: string | boolean | number | Function | Object, // not null or undefined
): void {
if (value && typeof value !== 'function' && typeof value !== 'symbol') {
target.push(attributeSeparator, stringToChunk(name), attributeEmptyString);
}
}

8. 推送文本属性

备注
function pushStringAttribute(
target: Array<Chunk | PrecomputedChunk>,
name: string,
// 非空或未定义
value: string | boolean | number | Function | Object, // not null or undefined
): void {
if (
typeof value !== 'function' &&
typeof value !== 'symbol' &&
typeof value !== 'boolean'
) {
target.push(
attributeSeparator,
stringToChunk(name),
attributeAssign,
stringToChunk(escapeTextForBrowser(value)),
attributeEnd,
);
}
}

9. 创建表单字段前缀

function makeFormFieldPrefix(resumableState: ResumableState): string {
// TODO: Make this deterministic.
// 待办事项:使其具有确定性。
const id = resumableState.nextFormID++;
return resumableState.idPrefix + id;
}

10. 推送附加表单字段

function pushAdditionalFormField(
this: Array<Chunk | PrecomputedChunk>,
value: string | File,
key: string,
): void {
const target: Array<Chunk | PrecomputedChunk> = this;
target.push(startHiddenInputChunk);
validateAdditionalFormField(value, key);
pushStringAttribute(target, 'name', key);
pushStringAttribute(target, 'value', value);
target.push(endOfStartTagSelfClosing);
}

11. 推送附加表单字段

function pushAdditionalFormFields(
target: Array<Chunk | PrecomputedChunk>,
formData: void | null | FormData,
) {
if (formData != null) {
formData.forEach(pushAdditionalFormField, target);
}
}

12. 验证附加表单字段

function validateAdditionalFormField(value: string | File, key: string): void {
if (typeof value !== 'string') {
throw new Error(
'File/Blob fields are not yet supported in progressive forms. ' +
'Will fallback to client hydration.',
);
}
}

13. 验证附加表单字段

function validateAdditionalFormFields(formData: void | null | FormData) {
if (formData != null) {
formData.forEach(validateAdditionalFormField);
}
return formData;
}

14. 获取自定义表单字段

function getCustomFormFields(
resumableState: ResumableState,
formAction: any,
): null | ReactCustomFormAction {
const customAction = formAction.$$FORM_ACTION;
if (typeof customAction === 'function') {
const prefix = makeFormFieldPrefix(resumableState);
try {
const customFields = formAction.$$FORM_ACTION(prefix);
if (customFields) {
validateAdditionalFormFields(customFields.data);
}
return customFields;
} catch (x) {
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
// Rethrow suspense.
// 重新抛出 suspense。
throw x;
}
// If we fail to encode the form action for progressive enhancement for some reason,
// fallback to trying replaying on the client instead of failing the page. It might
// work there.
// 如果由于某种原因我们无法为渐进增强对表单操作进行编码,则回退到尝试在客户端重新播放,而不是使页面失
// 败。它可能在那边有效。
if (__DEV__) {
// TODO: Should this be some kind of recoverable error?
// 待办:这应该是某种可恢复的错误吗?
console.error(
'Failed to serialize an action for progressive enhancement:\n%s',
x,
);
}
}
}
return null;
}

15. 推送表单操作属性

function pushFormActionAttribute(
target: Array<Chunk | PrecomputedChunk>,
resumableState: ResumableState,
renderState: RenderState,
formAction: any,
formEncType: any,
formMethod: any,
formTarget: any,
name: any,
): void | null | FormData {
let formData = null;
if (typeof formAction === 'function') {
// Function form actions cannot control the form properties
// 表单函数操作无法控制表单属性
if (__DEV__) {
if (name !== null && !didWarnFormActionName) {
didWarnFormActionName = true;
console.error(
'Cannot specify a "name" prop for a button that specifies a function as a formAction. ' +
'React needs it to encode which action should be invoked. It will get overridden.',
);
}
if (
(formEncType !== null || formMethod !== null) &&
!didWarnFormActionMethod
) {
didWarnFormActionMethod = true;
console.error(
'Cannot specify a formEncType or formMethod for a button that specifies a ' +
'function as a formAction. React provides those automatically. They will get overridden.',
);
}
if (formTarget !== null && !didWarnFormActionTarget) {
didWarnFormActionTarget = true;
console.error(
'Cannot specify a formTarget for a button that specifies a function as a formAction. ' +
'The function will always be executed in the same window.',
);
}
}
const customFields = getCustomFormFields(resumableState, formAction);
if (customFields !== null) {
// This action has a custom progressive enhancement form that can submit the form
// back to the server if it's invoked before hydration. Such as a Server Action.
// 此操作具有自定义渐进增强表单,如果在水合之前调用,它可以将表单提交回服务器。例如,一个服务器操作
name = customFields.name;
formAction = customFields.action || '';
formEncType = customFields.encType;
formMethod = customFields.method;
formTarget = customFields.target;
formData = customFields.data;
} else {
// Set a javascript URL that doesn't do anything. We don't expect this to be invoked
// because we'll preventDefault in the Fizz runtime, but it can happen if a form is
// manually submitted or if someone calls stopPropagation before React gets the event.
// If CSP is used to block javascript: URLs that's fine too. It just won't show this
// error message but the URL will be logged.
// 设置一个不会执行任何操作的 JavaScript URL。我们不期望它被调用,因为我们会在 Fizz 运行时使用
// preventDefault,但如果表单被手动提交,或者有人在 React 获取事件之前调用 stopPropagation,这
// 种情况仍可能发生。如果使用 CSP 阻止 javascript: URL,这也没关系。它只是不显示此错误消息,但
// URL 会被记录。
target.push(
attributeSeparator,
stringToChunk('formAction'),
attributeAssign,
actionJavaScriptURL,
attributeEnd,
);
name = null;
formAction = null;
formEncType = null;
formMethod = null;
formTarget = null;
injectFormReplayingRuntime(resumableState, renderState);
}
}
if (name != null) {
pushAttribute(target, 'name', name);
}
if (formAction != null) {
pushAttribute(target, 'formAction', formAction);
}
if (formEncType != null) {
pushAttribute(target, 'formEncType', formEncType);
}
if (formMethod != null) {
pushAttribute(target, 'formMethod', formMethod);
}
if (formTarget != null) {
pushAttribute(target, 'formTarget', formTarget);
}
return formData;
}

16. 推送源对象属性

备注
function pushSrcObjectAttribute(
target: Array<Chunk | PrecomputedChunk>,
blob: Blob,
): void {
// Throwing a Promise style suspense read of the Blob content.
// 以 Promise 风格的方式异步读取 Blob 内容。
if (blobCache === null) {
blobCache = new WeakMap();
}
const suspenseCache: WeakMap<Blob, Thenable<string>> = blobCache;
let thenable = suspenseCache.get(blob);
if (thenable === undefined) {
thenable = readAsDataURL(blob) as any as Thenable<string>;
thenable.then(
result => {
(thenable as any).status = 'fulfilled';
(thenable as any).value = result;
},
error => {
(thenable as any).status = 'rejected';
(thenable as any).reason = error;
},
);
suspenseCache.set(blob, thenable);
}
if (thenable.status === 'rejected') {
throw thenable.reason;
} else if (thenable.status !== 'fulfilled') {
throw thenable;
}
const url = thenable.value;
target.push(
attributeSeparator,
stringToChunk('src'),
attributeAssign,
stringToChunk(escapeTextForBrowser(url)),
attributeEnd,
);
}

17. 推送属性

备注
function pushAttribute(
target: Array<Chunk | PrecomputedChunk>,
name: string,
// 非空或未定义
value: string | boolean | number | Function | Object, // not null or undefined
): void {
switch (name) {
// These are very common props and therefore are in the beginning of the switch.
// 这些是非常常见的属性,因此出现在 switch 的开头。
// TODO: aria-label is a very common prop but allows booleans so is not like the others
// but should ideally go in this list too.
// TODO: aria-label 是一个非常常见的属性,但允许布尔值,因此不像其他属性,但理想情况下也应该出现在这
// 个列表中。
case 'className': {
pushStringAttribute(target, 'class', value);
break;
}
case 'tabIndex': {
pushStringAttribute(target, 'tabindex', value);
break;
}
case 'dir':
case 'role':
case 'viewBox':
case 'width':
case 'height': {
pushStringAttribute(target, name, value);
break;
}
case 'style': {
pushStyleAttribute(target, value);
return;
}
case 'src': {
if (enableSrcObject && typeof value === 'object' && value !== null) {
if (typeof Blob === 'function' && value instanceof Blob) {
pushSrcObjectAttribute(target, value);
return;
}
}
// Fallthrough to general urls
// 继续处理一般的 URL
}
case 'href': {
if (value === '') {
if (__DEV__) {
if (name === 'src') {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'This may cause the browser to download the whole page again over the network. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
name,
name,
);
} else {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
name,
name,
);
}
}
return;
}
}
// Fall through to the last case which shouldn't remove empty strings.
// 继续执行到最后一个情况,该情况不应删除空字符串。
case 'action':
case 'formAction': {
// TODO: Consider only special casing these for each tag.
// 待办事项:考虑仅对每个标签进行这些特殊处理。
if (
value == null ||
typeof value === 'function' ||
typeof value === 'symbol' ||
typeof value === 'boolean'
) {
return;
}
if (__DEV__) {
checkAttributeStringCoercion(value, name);
}
const sanitizedValue = sanitizeURL('' + value);
target.push(
attributeSeparator,
stringToChunk(name),
attributeAssign,
stringToChunk(escapeTextForBrowser(sanitizedValue)),
attributeEnd,
);
return;
}
case 'defaultValue':
// 这些不应该作为属性设置在通用 HTML 元素上。
case 'defaultChecked': // These shouldn't be set as attributes on generic HTML elements.
// 必须改用 dangerouslySetInnerHTML。
case 'innerHTML': // Must use dangerouslySetInnerHTML instead.
case 'suppressContentEditableWarning':
case 'suppressHydrationWarning':
case 'ref':
// Ignored. These are built-in to React on the client.
// 忽略。这些是内置在客户端的 React 中。
return;
case 'autoFocus':
case 'multiple':
case 'muted': {
pushBooleanAttribute(target, name.toLowerCase(), value);
return;
}
case 'xlinkHref': {
if (
typeof value === 'function' ||
typeof value === 'symbol' ||
typeof value === 'boolean'
) {
return;
}
if (__DEV__) {
checkAttributeStringCoercion(value, name);
}
const sanitizedValue = sanitizeURL('' + value);
target.push(
attributeSeparator,
stringToChunk('xlink:href'),
attributeAssign,
stringToChunk(escapeTextForBrowser(sanitizedValue)),
attributeEnd,
);
return;
}
case 'contentEditable':
case 'spellCheck':
case 'draggable':
case 'value':
case 'autoReverse':
case 'externalResourcesRequired':
case 'focusable':
case 'preserveAlpha': {
// Booleanish String
// These are "enumerated" attributes that accept "true" and "false".
// In React, we let users pass `true` and `false` even though technically
// these aren't boolean attributes (they are coerced to strings).
// 布尔类型字符串。这些是接受 “true” 和 “false” 的“枚举”属性。在 React 中,我们允许用户传递
// `true` 和 `false`,尽管从技术上讲这些不是布尔属性(它们会被强制转换为字符串)。
if (typeof value !== 'function' && typeof value !== 'symbol') {
target.push(
attributeSeparator,
stringToChunk(name),
attributeAssign,
stringToChunk(escapeTextForBrowser(value)),
attributeEnd,
);
}
return;
}
case 'inert': {
if (__DEV__) {
if (value === '' && !didWarnForNewBooleanPropsWithEmptyValue[name]) {
didWarnForNewBooleanPropsWithEmptyValue[name] = true;
console.error(
'Received an empty string for a boolean attribute `%s`. ' +
'This will treat the attribute as if it were false. ' +
'Either pass `false` to silence this warning, or ' +
'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.',
name,
);
}
}
}
// Fallthrough for boolean props that don't have a warning for empty strings.
// 对于没有空字符串警告的布尔属性的贯通处理。
case 'allowFullScreen':
case 'async':
case 'autoPlay':
case 'controls':
case 'default':
case 'defer':
case 'disabled':
case 'disablePictureInPicture':
case 'disableRemotePlayback':
case 'formNoValidate':
case 'hidden':
case 'loop':
case 'noModule':
case 'noValidate':
case 'open':
case 'playsInline':
case 'readOnly':
case 'required':
case 'reversed':
case 'scoped':
case 'seamless':
case 'itemScope': {
// Boolean
// 布尔
if (value && typeof value !== 'function' && typeof value !== 'symbol') {
target.push(
attributeSeparator,
stringToChunk(name),
attributeEmptyString,
);
}
return;
}
case 'capture':
case 'download': {
// Overloaded Boolean
// 重载布尔值
if (value === true) {
target.push(
attributeSeparator,
stringToChunk(name),
attributeEmptyString,
);
} else if (value === false) {
// Ignored
// 忽略
} else if (typeof value !== 'function' && typeof value !== 'symbol') {
target.push(
attributeSeparator,
stringToChunk(name),
attributeAssign,
stringToChunk(escapeTextForBrowser(value)),
attributeEnd,
);
}
return;
}
case 'cols':
case 'rows':
case 'size':
case 'span': {
// These are HTML attributes that must be positive numbers.
// 这些是必须为正数的 HTML 属性。
if (
typeof value !== 'function' &&
typeof value !== 'symbol' &&
!isNaN(value) &&
(value as any) >= 1
) {
target.push(
attributeSeparator,
stringToChunk(name),
attributeAssign,
stringToChunk(escapeTextForBrowser(value)),
attributeEnd,
);
}
return;
}
case 'rowSpan':
case 'start': {
// These are HTML attributes that must be numbers.
// 这些是必须为数字的 HTML 属性。
if (
typeof value !== 'function' &&
typeof value !== 'symbol' &&
!isNaN(value)
) {
target.push(
attributeSeparator,
stringToChunk(name),
attributeAssign,
stringToChunk(escapeTextForBrowser(value)),
attributeEnd,
);
}
return;
}
case 'xlinkActuate':
pushStringAttribute(target, 'xlink:actuate', value);
return;
case 'xlinkArcrole':
pushStringAttribute(target, 'xlink:arcrole', value);
return;
case 'xlinkRole':
pushStringAttribute(target, 'xlink:role', value);
return;
case 'xlinkShow':
pushStringAttribute(target, 'xlink:show', value);
return;
case 'xlinkTitle':
pushStringAttribute(target, 'xlink:title', value);
return;
case 'xlinkType':
pushStringAttribute(target, 'xlink:type', value);
return;
case 'xmlBase':
pushStringAttribute(target, 'xml:base', value);
return;
case 'xmlLang':
pushStringAttribute(target, 'xml:lang', value);
return;
case 'xmlSpace':
pushStringAttribute(target, 'xml:space', value);
return;
default:
if (
// shouldIgnoreAttribute
// We have already filtered out null/undefined and reserved words.
// 是否忽略属性。我们已经过滤掉了 null/undefined 和保留字。
name.length > 2 &&
(name[0] === 'o' || name[0] === 'O') &&
(name[1] === 'n' || name[1] === 'N')
) {
return;
}

const attributeName = getAttributeAlias(name);
if (isAttributeNameSafe(attributeName)) {
// shouldRemoveAttribute
// 是否应移除属性
switch (typeof value) {
case 'function':
case 'symbol':
return;
case 'boolean': {
const prefix = attributeName.toLowerCase().slice(0, 5);
if (prefix !== 'data-' && prefix !== 'aria-') {
return;
}
}
}
target.push(
attributeSeparator,
stringToChunk(attributeName),
attributeAssign,
stringToChunk(escapeTextForBrowser(value)),
attributeEnd,
);
}
}
}

18. 推送内部 HTML

备注
function pushInnerHTML(
target: Array<Chunk | PrecomputedChunk>,
innerHTML: any,
children: any,
) {
if (innerHTML != null) {
if (children != null) {
throw new Error(
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
);
}

if (typeof innerHTML !== 'object' || !('__html' in innerHTML)) {
throw new Error(
'`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
'Please visit https://react.dev/link/dangerously-set-inner-html ' +
'for more information.',
);
}

const html = innerHTML.__html;
if (html !== null && html !== undefined) {
if (__DEV__) {
checkHtmlStringCoercion(html);
}
target.push(stringToChunk('' + html));
}
}
}

19. 检查选择属性

备注
function checkSelectProp(props: any, propName: string) {
if (__DEV__) {
const value = props[propName];
if (value != null) {
const array = isArray(value);
if (props.multiple && !array) {
console.error(
'The `%s` prop supplied to <select> must be an array if ' +
'`multiple` is true.',
propName,
);
} else if (!props.multiple && array) {
console.error(
'The `%s` prop supplied to <select> must be a scalar ' +
'value if `multiple` is false.',
propName,
);
}
}
}
}

20. 推动起始锚

备注
function pushStartAnchor(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
formatContext: FormatContext,
): ReactNodeList {
target.push(startChunkForTag('a'));

let children = null;
let innerHTML = null;
for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
children = propValue;
break;
case 'dangerouslySetInnerHTML':
innerHTML = propValue;
break;
case 'href':
if (propValue === '') {
// Empty `href` is special on anchors so we're short-circuiting here.
// On other tags it should trigger a warning
// 空的 `href` 在锚点上是特殊的,所以我们在这里短路。在其他标签上应该触发警告
pushStringAttribute(target, 'href', '');
} else {
pushAttribute(target, propKey, propValue);
}
break;
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}

pushViewTransitionAttributes(target, formatContext);

target.push(endOfStartTag);
pushInnerHTML(target, innerHTML, children);
if (typeof children === 'string') {
// Special case children as a string to avoid the unnecessary comment.
// 将 children 作为字符串处理以避免不必要的注释。
// TODO: Remove this special case after the general optimization is in place.
// TODO:在通用优化就位后移除此特殊处理。
target.push(stringToChunk(encodeHTMLTextNode(children)));
return null;
}
return children;
}

21. 推送启动对象

备注
function pushStartObject(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
formatContext: FormatContext,
): ReactNodeList {
target.push(startChunkForTag('object'));

let children = null;
let innerHTML = null;
for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
children = propValue;
break;
case 'dangerouslySetInnerHTML':
innerHTML = propValue;
break;
case 'data': {
if (__DEV__) {
checkAttributeStringCoercion(propValue, 'data');
}
const sanitizedValue = sanitizeURL('' + propValue);
if (sanitizedValue === '') {
if (__DEV__) {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
propKey,
propKey,
);
}
break;
}
target.push(
attributeSeparator,
stringToChunk('data'),
attributeAssign,
stringToChunk(escapeTextForBrowser(sanitizedValue)),
attributeEnd,
);
break;
}
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}

pushViewTransitionAttributes(target, formatContext);

target.push(endOfStartTag);
pushInnerHTML(target, innerHTML, children);
if (typeof children === 'string') {
// Special case children as a string to avoid the unnecessary comment.
// 将 children 作为字符串处理以避免不必要的注释。
// TODO: Remove this special case after the general optimization is in place.
// TODO:在通用优化就位后移除此特殊处理。
target.push(stringToChunk(encodeHTMLTextNode(children)));
return null;
}
return children;
}

22. 按下开始选择

备注
function pushStartSelect(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
formatContext: FormatContext,
): ReactNodeList {
if (__DEV__) {
checkControlledValueProps('select', props);

checkSelectProp(props, 'value');
checkSelectProp(props, 'defaultValue');

if (
props.value !== undefined &&
props.defaultValue !== undefined &&
!didWarnDefaultSelectValue
) {
console.error(
'Select elements must be either controlled or uncontrolled ' +
'(specify either the value prop, or the defaultValue prop, but not ' +
'both). Decide between using a controlled or uncontrolled select ' +
'element and remove one of these props. More info: ' +
'https://react.dev/link/controlled-components',
);
didWarnDefaultSelectValue = true;
}
}

target.push(startChunkForTag('select'));

let children = null;
let innerHTML = null;
for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
children = propValue;
break;
case 'dangerouslySetInnerHTML':
// TODO: This doesn't really make sense for select since it can't use the controlled
// value in the innerHTML.
// 待办:对于 select 来说,这其实不太合理,因为它不能在 innerHTML 中使用受控值。
innerHTML = propValue;
break;
case 'defaultValue':
case 'value':
// These are set on the Context instead and applied to the nested options.
// 这些被设置在上下文中,并应用于嵌套选项。
break;
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}

pushViewTransitionAttributes(target, formatContext);

target.push(endOfStartTag);
pushInnerHTML(target, innerHTML, children);
return children;
}

23. 扁平化选项子项

function flattenOptionChildren(children: mixed): string {
let content = '';
// Flatten children and warn if they aren't strings or numbers;
// invalid types are ignored.
// 展平子元素,并在它们不是字符串或数字时发出警告;无效类型将被忽略。
Children.forEach(children as any, function (child) {
if (child == null) {
return;
}
content += child as any;
if (__DEV__) {
if (
!didWarnInvalidOptionChildren &&
typeof child !== 'string' &&
typeof child !== 'number' &&
typeof child !== 'bigint'
) {
didWarnInvalidOptionChildren = true;
console.error(
'Cannot infer the option value of complex children. ' +
'Pass a `value` prop or use a plain string as children to <option>.',
);
}
}
});
return content;
}

24. 启动选项

备注
function pushStartOption(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
formatContext: FormatContext,
): ReactNodeList {
const selectedValue = formatContext.selectedValue;

target.push(startChunkForTag('option'));

let children = null;
let value = null;
let selected = null;
let innerHTML = null;
for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
children = propValue;
break;
case 'selected':
// ignore
// 忽略
selected = propValue;
if (__DEV__) {
// TODO: Remove support for `selected` in <option>.
// 待办事项:移除 <option> 中对 `selected` 的支持。
if (!didWarnSelectedSetOnOption) {
console.error(
'Use the `defaultValue` or `value` props on <select> instead of ' +
'setting `selected` on <option>.',
);
didWarnSelectedSetOnOption = true;
}
}
break;
case 'dangerouslySetInnerHTML':
innerHTML = propValue;
break;
case 'value':
value = propValue;
// We intentionally fallthrough to also set the attribute on the node.
// 我们故意继续执行以在节点上也设置该属性。
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}

if (selectedValue != null) {
let stringValue;
if (value !== null) {
if (__DEV__) {
checkAttributeStringCoercion(value, 'value');
}
stringValue = '' + value;
} else {
if (__DEV__) {
if (innerHTML !== null) {
if (!didWarnInvalidOptionInnerHTML) {
didWarnInvalidOptionInnerHTML = true;
console.error(
'Pass a `value` prop if you set dangerouslyInnerHTML so React knows ' +
'which value should be selected.',
);
}
}
}
stringValue = flattenOptionChildren(children);
}
if (isArray(selectedValue)) {
// multiple
// 多重
for (let i = 0; i < selectedValue.length; i++) {
if (__DEV__) {
checkAttributeStringCoercion(selectedValue[i], 'value');
}
const v = '' + selectedValue[i];
if (v === stringValue) {
target.push(selectedMarkerAttribute);
break;
}
}
} else {
if (__DEV__) {
checkAttributeStringCoercion(selectedValue, 'select.value');
}
if ('' + selectedValue === stringValue) {
target.push(selectedMarkerAttribute);
}
}
} else if (selected) {
target.push(selectedMarkerAttribute);
}

// Options never participate as ViewTransitions.
// 选项从不作为视图过渡参与。
target.push(endOfStartTag);
pushInnerHTML(target, innerHTML, children);
return children;
}

25. 注入表单重放运行时

备注
function injectFormReplayingRuntime(
resumableState: ResumableState,
renderState: RenderState,
): void {
// If we haven't sent it yet, inject the runtime that tracks submitted JS actions
// for later replaying by Fiber. If we use an external runtime, we don't need
// to emit anything. It's always used.
// 如果我们还没有发送它,请注入跟踪已提交 JS 操作的运行时,以便 Fiber 以后重放。如果我们使用外部运行时,
// 则不需要发出任何东西。它总是被使用。
if (
(resumableState.instructions & SentFormReplayingRuntime) === NothingSent &&
(!enableFizzExternalRuntime || !renderState.externalRuntimeScript)
) {
resumableState.instructions |= SentFormReplayingRuntime;
const preamble = renderState.preamble;
const bootstrapChunks = renderState.bootstrapChunks;
if (
(preamble.htmlChunks || preamble.headChunks) &&
bootstrapChunks.length === 0
) {
// If we rendered the whole document, then we emitted a rel="expect" that needs a
// matching target. If we haven't emitted that yet, we need to include it in this
// script tag.
// 如果我们渲染了整个文档,那么我们发出了一个 rel="expect",它需要一个匹配的目标。 如果我们还没
// 有发出它,我们需要在这个 script 标签中包含它。
bootstrapChunks.push(renderState.startInlineScript);
pushCompletedShellIdAttribute(bootstrapChunks, resumableState);
bootstrapChunks.push(
endOfStartTag,
formReplayingRuntimeScript,
endInlineScript,
);
} else {
// Otherwise we added to the beginning of the scripts. This will mean that it
// appears before the shell ID unfortunately.
// 否则我们会将其添加到脚本的开头。这将意味着它不幸地出现在 shell ID 之前。
bootstrapChunks.unshift(
renderState.startInlineScript,
endOfStartTag,
formReplayingRuntimeScript,
endInlineScript,
);
}
}
}

26. 启动表单

备注
function pushStartForm(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
resumableState: ResumableState,
renderState: RenderState,
formatContext: FormatContext,
): ReactNodeList {
target.push(startChunkForTag('form'));

let children = null;
let innerHTML = null;
let formAction = null;
let formEncType = null;
let formMethod = null;
let formTarget = null;

for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
children = propValue;
break;
case 'dangerouslySetInnerHTML':
innerHTML = propValue;
break;
case 'action':
formAction = propValue;
break;
case 'encType':
formEncType = propValue;
break;
case 'method':
formMethod = propValue;
break;
case 'target':
formTarget = propValue;
break;
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}

let formData = null;
let formActionName = null;
if (typeof formAction === 'function') {
// Function form actions cannot control the form properties
// 表单函数操作无法控制表单属性
if (__DEV__) {
if (
(formEncType !== null || formMethod !== null) &&
!didWarnFormActionMethod
) {
didWarnFormActionMethod = true;
console.error(
'Cannot specify a encType or method for a form that specifies a ' +
'function as the action. React provides those automatically. ' +
'They will get overridden.',
);
}
if (formTarget !== null && !didWarnFormActionTarget) {
didWarnFormActionTarget = true;
console.error(
'Cannot specify a target for a form that specifies a function as the action. ' +
'The function will always be executed in the same window.',
);
}
}
const customFields = getCustomFormFields(resumableState, formAction);
if (customFields !== null) {
// This action has a custom progressive enhancement form that can submit the form
// back to the server if it's invoked before hydration. Such as a Server Action.
// 此操作具有自定义渐进增强表单,如果在水合之前调用,它可以将表单提交回服务器。例如,一个服务器操作
formAction = customFields.action || '';
formEncType = customFields.encType;
formMethod = customFields.method;
formTarget = customFields.target;
formData = customFields.data;
formActionName = customFields.name;
} else {
// Set a javascript URL that doesn't do anything. We don't expect this to be invoked
// because we'll preventDefault in the Fizz runtime, but it can happen if a form is
// manually submitted or if someone calls stopPropagation before React gets the event.
// If CSP is used to block javascript: URLs that's fine too. It just won't show this
// error message but the URL will be logged.
// 设置一个不会执行任何操作的 JavaScript URL。我们不期望它被调用。因为我们会在 Fizz 运行时使用
// preventDefault,但如果表单被手动提交,或者有人在 React 获取事件之前调用 stopPropagation,这
// 种情况仍可能发生。如果使用 CSP 阻止 javascript: URL,这也没关系。它只是不显示此错误消息,但
// URL 会被记录。
target.push(
attributeSeparator,
stringToChunk('action'),
attributeAssign,
actionJavaScriptURL,
attributeEnd,
);
formAction = null;
formEncType = null;
formMethod = null;
formTarget = null;
injectFormReplayingRuntime(resumableState, renderState);
}
}
if (formAction != null) {
pushAttribute(target, 'action', formAction);
}
if (formEncType != null) {
pushAttribute(target, 'encType', formEncType);
}
if (formMethod != null) {
pushAttribute(target, 'method', formMethod);
}
if (formTarget != null) {
pushAttribute(target, 'target', formTarget);
}

pushViewTransitionAttributes(target, formatContext);

target.push(endOfStartTag);

if (formActionName !== null) {
target.push(startHiddenInputChunk);
pushStringAttribute(target, 'name', formActionName);
target.push(endOfStartTagSelfClosing);
pushAdditionalFormFields(target, formData);
}

pushInnerHTML(target, innerHTML, children);
if (typeof children === 'string') {
// Special case children as a string to avoid the unnecessary comment.
// 将 children 作为字符串处理以避免不必要的注释。
// TODO: Remove this special case after the general optimization is in place.
// TODO:在通用优化就位后移除此特殊处理。
target.push(stringToChunk(encodeHTMLTextNode(children)));
return null;
}
return children;
}

27. 推送输入

备注
function pushInput(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
resumableState: ResumableState,
renderState: RenderState,
formatContext: FormatContext,
): ReactNodeList {
if (__DEV__) {
checkControlledValueProps('input', props);
}

target.push(startChunkForTag('input'));

let name = null;
let formAction = null;
let formEncType = null;
let formMethod = null;
let formTarget = null;
let value = null;
let defaultValue = null;
let checked = null;
let defaultChecked = null;

for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
case 'dangerouslySetInnerHTML':
throw new Error(
`${'input'} is a self-closing tag and must neither have \`children\` nor ` +
'use `dangerouslySetInnerHTML`.',
);
case 'name':
name = propValue;
break;
case 'formAction':
formAction = propValue;
break;
case 'formEncType':
formEncType = propValue;
break;
case 'formMethod':
formMethod = propValue;
break;
case 'formTarget':
formTarget = propValue;
break;
case 'defaultChecked':
defaultChecked = propValue;
break;
case 'defaultValue':
defaultValue = propValue;
break;
case 'checked':
checked = propValue;
break;
case 'value':
value = propValue;
break;
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}

if (__DEV__) {
if (
formAction !== null &&
props.type !== 'image' &&
props.type !== 'submit' &&
!didWarnFormActionType
) {
didWarnFormActionType = true;
console.error(
'An input can only specify a formAction along with type="submit" or type="image".',
);
}
}

const formData = pushFormActionAttribute(
target,
resumableState,
renderState,
formAction,
formEncType,
formMethod,
formTarget,
name,
);

if (__DEV__) {
if (checked !== null && defaultChecked !== null && !didWarnDefaultChecked) {
console.error(
'%s contains an input of type %s with both checked and defaultChecked props. ' +
'Input elements must be either controlled or uncontrolled ' +
'(specify either the checked prop, or the defaultChecked prop, but not ' +
'both). Decide between using a controlled or uncontrolled input ' +
'element and remove one of these props. More info: ' +
'https://react.dev/link/controlled-components',
'A component',
props.type,
);
didWarnDefaultChecked = true;
}
if (value !== null && defaultValue !== null && !didWarnDefaultInputValue) {
console.error(
'%s contains an input of type %s with both value and defaultValue props. ' +
'Input elements must be either controlled or uncontrolled ' +
'(specify either the value prop, or the defaultValue prop, but not ' +
'both). Decide between using a controlled or uncontrolled input ' +
'element and remove one of these props. More info: ' +
'https://react.dev/link/controlled-components',
'A component',
props.type,
);
didWarnDefaultInputValue = true;
}
}

if (checked !== null) {
pushBooleanAttribute(target, 'checked', checked);
} else if (defaultChecked !== null) {
pushBooleanAttribute(target, 'checked', defaultChecked);
}
if (value !== null) {
pushAttribute(target, 'value', value);
} else if (defaultValue !== null) {
pushAttribute(target, 'value', defaultValue);
}

pushViewTransitionAttributes(target, formatContext);

target.push(endOfStartTagSelfClosing);

// We place any additional hidden form fields after the input.
// 我们将任何额外的隐藏表单字段放在输入之后。
pushAdditionalFormFields(target, formData);

return null;
}

28. 按下开始按钮

备注
function pushStartButton(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
resumableState: ResumableState,
renderState: RenderState,
formatContext: FormatContext,
): ReactNodeList {
target.push(startChunkForTag('button'));

let children = null;
let innerHTML = null;
let name = null;
let formAction = null;
let formEncType = null;
let formMethod = null;
let formTarget = null;

for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
children = propValue;
break;
case 'dangerouslySetInnerHTML':
innerHTML = propValue;
break;
case 'name':
name = propValue;
break;
case 'formAction':
formAction = propValue;
break;
case 'formEncType':
formEncType = propValue;
break;
case 'formMethod':
formMethod = propValue;
break;
case 'formTarget':
formTarget = propValue;
break;
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}

if (__DEV__) {
if (
formAction !== null &&
props.type != null &&
props.type !== 'submit' &&
!didWarnFormActionType
) {
didWarnFormActionType = true;
console.error(
'A button can only specify a formAction along with type="submit" or no type.',
);
}
}

const formData = pushFormActionAttribute(
target,
resumableState,
renderState,
formAction,
formEncType,
formMethod,
formTarget,
name,
);

pushViewTransitionAttributes(target, formatContext);

target.push(endOfStartTag);

// We place any additional hidden form fields we need to include inside the button itself.
// 我们将需要包含的任何额外隐藏表单字段放置在按钮本身内。
pushAdditionalFormFields(target, formData);

pushInnerHTML(target, innerHTML, children);
if (typeof children === 'string') {
// Special case children as a string to avoid the unnecessary comment.
// 将 children 作为字符串处理以避免不必要的注释。
// TODO: Remove this special case after the general optimization is in place.
// TODO:在通用优化就位后移除此特殊处理。
target.push(stringToChunk(encodeHTMLTextNode(children)));
return null;
}

return children;
}

29. 推送开始文本区

备注
function pushStartTextArea(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
formatContext: FormatContext,
): ReactNodeList {
if (__DEV__) {
checkControlledValueProps('textarea', props);
if (
props.value !== undefined &&
props.defaultValue !== undefined &&
!didWarnDefaultTextareaValue
) {
console.error(
'Textarea elements must be either controlled or uncontrolled ' +
'(specify either the value prop, or the defaultValue prop, but not ' +
'both). Decide between using a controlled or uncontrolled textarea ' +
'and remove one of these props. More info: ' +
'https://react.dev/link/controlled-components',
);
didWarnDefaultTextareaValue = true;
}
}

target.push(startChunkForTag('textarea'));

let value = null;
let defaultValue = null;
let children = null;
for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
children = propValue;
break;
case 'value':
value = propValue;
break;
case 'defaultValue':
defaultValue = propValue;
break;
case 'dangerouslySetInnerHTML':
throw new Error(
'`dangerouslySetInnerHTML` does not make sense on <textarea>.',
);
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}
if (value === null && defaultValue !== null) {
value = defaultValue;
}

pushViewTransitionAttributes(target, formatContext);

target.push(endOfStartTag);

// TODO (yungsters): Remove support for children content in <textarea>.
// 待办 (yungsters):移除 <textarea> 中对子内容的支持。
if (children != null) {
if (__DEV__) {
console.error(
'Use the `defaultValue` or `value` props instead of setting ' +
'children on <textarea>.',
);
}

if (value != null) {
throw new Error(
'If you supply `defaultValue` on a <textarea>, do not pass children.',
);
}

if (isArray(children)) {
if (children.length > 1) {
throw new Error('<textarea> can only have at most one child.');
}

// TODO: remove the coercion and the DEV check below because it will
// always be overwritten by the coercion several lines below it. #22309
// TODO:移除下面的强制转换和 DEV 检查,因为它将总是被下面几行的强制转换覆盖。
if (__DEV__) {
checkHtmlStringCoercion(children[0]);
}
value = '' + children[0];
}
if (__DEV__) {
checkHtmlStringCoercion(children);
}
value = '' + children;
}

if (typeof value === 'string' && value[0] === '\n') {
// text/html ignores the first character in these tags if it's a newline
// Prefer to break application/xml over text/html (for now) by adding
// a newline specifically to get eaten by the parser. (Alternately for
// textareas, replacing "^\n" with "\r\n" doesn't get eaten, and the first
// \r is normalized out by HTMLTextAreaElement#value.)
// text/html 会忽略这些标签中的第一个字符,如果它是换行符
// 目前更倾向于通过添加换行符来打断 application/xml,而不是 text/html,这样解析器会吞掉它。(或者对
// 于文本区域,将 "^n" 替换为 "rn" 不会被吞掉,并且第一个 r 会被 HTMLTextAreaElement#value 归一
// 化。)
// See: <http://www.w3.org/TR/html-polyglot/#newlines-in-textarea-and-pre>
// See: <http://www.w3.org/TR/html5/syntax.html#element-restrictions>
// See: <http://www.w3.org/TR/html5/syntax.html#newlines>
// See: Parsing of "textarea" "listing" and "pre" elements
// from <http://www.w3.org/TR/html5/syntax.html#parsing-main-inbody>
target.push(leadingNewline);
}

// ToString and push directly instead of recurse over children.
// We don't really support complex children in the value anyway.
// This also currently avoids a trailing comment node which breaks textarea.
// 直接使用 ToString 并推送,而不是递归遍历子节点。反正我们并不真正支持值中的复杂子节点。
// 这目前也可以避免在 textarea 中出现会导致问题的尾随注释节点。
if (value !== null) {
if (__DEV__) {
checkAttributeStringCoercion(value, 'value');
}
target.push(stringToChunk(encodeHTMLTextNode('' + value)));
}

return null;
}

30. 推送元数据

function pushMeta(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
renderState: RenderState,
textEmbedded: boolean,
formatContext: FormatContext,
): null {
const noscriptTagInScope = formatContext.tagScope & NOSCRIPT_SCOPE;
const isFallback = formatContext.tagScope & FALLBACK_SCOPE;
if (
formatContext.insertionMode === SVG_MODE ||
noscriptTagInScope ||
props.itemProp != null
) {
return pushSelfClosing(target, props, 'meta', formatContext);
} else {
if (textEmbedded) {
// This link follows text but we aren't writing a tag. while not as efficient as possible we need
// to be safe and assume text will follow by inserting a textSeparator
// 这个链接跟随文本,但我们没有写标签。虽然不如可能的效率高,但我们需要保持安全,并假设文本会跟随,通
// 过插入 textSeparator
target.push(textSeparator);
}

if (isFallback) {
// Hoistable Elements for fallbacks are simply omitted. we don't want to emit them early
// because they are likely superceded by primary content and we want to avoid needing to clean
// them up when the primary content is ready. They are never hydrated on the client anyway because
// boundaries in fallback are awaited or client render, in either case there is never hydration
// 可提升元素用于回退内容会被简单地省略。我们不想提前输出它们。因为它们很可能会被主要内容替代,同时我
// 们也想避免在主要内容准备好时,需要清理它们。它们在客户端无论如何从不进行水合,因为回退中的边界会被
// 等待或客户端渲染,在任何情况下都不会进行水合
return null;
} else if (typeof props.charSet === 'string') {
// "charset" Should really be config and not picked up from tags however since this is
// the only way to embed the tag today we flush it on a special queue on the Request so it
// can go before everything else. Like viewport this means that the tag will escape it's
// parent container.
// “charset” 实际上应该是配置,而不是从标签中获取,但是由于这是今天嵌入标签的唯一方法,我们将在请求
// 的一个特殊队列中刷新它,这样它就可以排在其他所有内容之前。像 viewport 一样,这意味着该标签将逃出
// 它的父容器。
return pushSelfClosing(
renderState.charsetChunks,
props,
'meta',
formatContext,
);
} else if (props.name === 'viewport') {
// "viewport" is flushed on the Request so it can go earlier that Float resources that
// might be affected by it. This means it can escape the boundary it is rendered within.
// This is a pragmatic solution to viewport being incredibly sensitive to document order
// without requiring all hoistables to be flushed too early.
// “viewport” 会在请求时被刷新,这样它可以比可能受到其影响的 Float 资源更早出现。这意味着它可以超出
// 其渲染的边界。这是对 viewport 对文档顺序极其敏感的一种务实解决方案,而无需让所有可提升的内容也过
// 早刷新。
return pushSelfClosing(
renderState.viewportChunks,
props,
'meta',
formatContext,
);
} else {
return pushSelfClosing(
renderState.hoistableChunks,
props,
'meta',
formatContext,
);
}
}
}

31. 推送链接

备注
function pushLink(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
resumableState: ResumableState,
renderState: RenderState,
hoistableState: null | HoistableState,
textEmbedded: boolean,
formatContext: FormatContext,
): null {
const noscriptTagInScope = formatContext.tagScope & NOSCRIPT_SCOPE;
const isFallback = formatContext.tagScope & FALLBACK_SCOPE;
const rel = props.rel;
const href = props.href;
const precedence = props.precedence;
if (
formatContext.insertionMode === SVG_MODE ||
noscriptTagInScope ||
props.itemProp != null ||
typeof rel !== 'string' ||
typeof href !== 'string' ||
href === ''
) {
if (__DEV__) {
if (rel === 'stylesheet' && typeof props.precedence === 'string') {
if (typeof href !== 'string' || !href) {
console.error(
'React encountered a `<link rel="stylesheet" .../>` with a `precedence` prop and expected the `href` prop to be a non-empty string but ecountered %s instead. If your intent was to have React hoist and deduplciate this stylesheet using the `precedence` prop ensure there is a non-empty string `href` prop as well, otherwise remove the `precedence` prop.',
getValueDescriptorExpectingObjectForWarning(href),
);
}
}
}
pushLinkImpl(target, props);
return null;
}

if (props.rel === 'stylesheet') {
// This <link> may hoistable as a Stylesheet Resource, otherwise it will emit in place
// 这个 <link> 可能被提升为样式表资源,否则它将在原位置发出
const key = getResourceKey(href);
if (
typeof precedence !== 'string' ||
props.disabled != null ||
props.onLoad ||
props.onError
) {
// This stylesheet is either not opted into Resource semantics or has conflicting properties which
// disqualify it for such. We can still create a preload resource to help it load faster on the
// client
// 该样式表要么未选择资源语义,要么具有冲突属性,因此不符合要求。我们仍然可以创建一个预加载资源以帮助
// 它在客户端更快加载
if (__DEV__) {
if (typeof precedence === 'string') {
if (props.disabled != null) {
console.error(
'React encountered a `<link rel="stylesheet" .../>` with a `precedence` prop and a `disabled` prop. The presence of the `disabled` prop indicates an intent to manage the stylesheet active state from your from your Component code and React will not hoist or deduplicate this stylesheet. If your intent was to have React hoist and deduplciate this stylesheet using the `precedence` prop remove the `disabled` prop, otherwise remove the `precedence` prop.',
);
} else if (props.onLoad || props.onError) {
const propDescription =
props.onLoad && props.onError
? '`onLoad` and `onError` props'
: props.onLoad
? '`onLoad` prop'
: '`onError` prop';
console.error(
'React encountered a `<link rel="stylesheet" .../>` with a `precedence` prop and %s. The presence of loading and error handlers indicates an intent to manage the stylesheet loading state from your from your Component code and React will not hoist or deduplicate this stylesheet. If your intent was to have React hoist and deduplciate this stylesheet using the `precedence` prop remove the %s, otherwise remove the `precedence` prop.',
propDescription,
propDescription,
);
}
}
}
return pushLinkImpl(target, props);
} else {
// This stylesheet refers to a Resource and we create a new one if necessary
// 该样式表引用了一个资源,如果有必要我们会创建一个新的
let styleQueue = renderState.styles.get(precedence);
const hasKey = resumableState.styleResources.hasOwnProperty(key);
const resourceState = hasKey
? resumableState.styleResources[key]
: undefined;
if (resourceState !== EXISTS) {
// We are going to create this resource now so it is marked as Exists
// 我们现在将创建此资源,因此它被标记为已存在
resumableState.styleResources[key] = EXISTS;

// If this is the first time we've encountered this precedence we need
// to create a StyleQueue
// 如果这是我们第一次遇到这个优先级,我们需要创建一个 StyleQueue
if (!styleQueue) {
styleQueue = {
precedence: stringToChunk(escapeTextForBrowser(precedence)),
rules: [] as Array<Chunk | PrecomputedChunk>,
hrefs: [] as Array<Chunk | PrecomputedChunk>,
sheets: new Map() as Map<string, StylesheetResource>,
};
renderState.styles.set(precedence, styleQueue);
}

const resource: StylesheetResource = {
state: PENDING,
props: stylesheetPropsFromRawProps(props),
};

if (resourceState) {
// When resourceState is truty it is a Preload state. We cast it for clarity
// 当 resourceState 为真时,它处于预加载状态。我们进行类型转换以便更清楚
const preloadState: Preloaded | PreloadedWithCredentials =
resourceState;
if (preloadState.length === 2) {
adoptPreloadCredentials(resource.props, preloadState);
}

const preloadResource = renderState.preloads.stylesheets.get(key);
if (preloadResource && preloadResource.length > 0) {
// The Preload for this resource was created in this render pass and has not flushed yet so
// we need to clear it to avoid it flushing.
// 此资源的预加载是在此渲染过程中创建的,并且尚未刷新,因此我们需要清除它以避免刷新。
preloadResource.length = 0;
} else {
// Either the preload resource from this render already flushed in this render pass
// or the preload flushed in a prior pass (prerender). In either case we need to mark
// this resource as already having been preloaded.
// 要么是在本渲染通道中已经刷新了预加载资源,要么是在之前的通道(预渲染)中刷新了预加载资源。无
// 论哪种情况,我们都需要将该资源标记为已预加载。
resource.state = PRELOADED;
}
} else {
// We don't need to check whether a preloadResource exists in the renderState
// because if it did exist then the resourceState would also exist and we would
// have hit the primary if condition above.
// 我们不需要检查 preloadResource 是否存在于 renderState 中。因为如果它存在,那么
// resourceState 也会存在,我们就会在上面的主要 if 条件中被命中。
}

// We add the newly created resource to our StyleQueue and if necessary
// track the resource with the currently rendering boundary
// 我们将新创建的资源添加到我们的 StyleQueue 中,如果有必要使用当前渲染的边界跟踪该资源
styleQueue.sheets.set(key, resource);
if (hoistableState) {
hoistableState.stylesheets.add(resource);
}
} else {
// We need to track whether this boundary should wait on this resource or not.
// Typically this resource should always exist since we either had it or just created
// it. However, it's possible when you resume that the style has already been emitted
// and then it wouldn't be recreated in the RenderState and there's no need to track
// it again since we should've hoisted it to the shell already.
// 我们需要跟踪这个边界是否应该等待这个资源。通常这个资源应该总是存在,因为我们要么已经有它,要么刚
// 创建它。然而,当你恢复时,有可能样式已经被发出,然后它不会在 RenderState 中被重新创建,也不需
// 要再次跟踪它,因为我们应该已经将它提升到 shell 中了。
if (styleQueue) {
const resource = styleQueue.sheets.get(key);
if (resource) {
if (hoistableState) {
hoistableState.stylesheets.add(resource);
}
}
}
}
if (textEmbedded) {
// This link follows text but we aren't writing a tag. while not as efficient as possible we need
// to be safe and assume text will follow by inserting a textSeparator
// 这个链接跟随文本,但我们没有写标签。虽然不如可能的效率高,但我们需要保持安全,并假设文本会跟随,
// 通过插入 textSeparator
target.push(textSeparator);
}
return null;
}
} else if (props.onLoad || props.onError) {
// When using load handlers we cannot hoist and need to emit links in place
// 使用加载处理程序时,我们无法提升,且需要在原地发出链接
return pushLinkImpl(target, props);
} else {
// We can hoist this link so we may need to emit a text separator.
// @TODO refactor text separators so we don't have to defensively add
// them when we don't end up emitting a tag as a result of pushStartInstance
// 我们可以提升这个链接,所以我们可能需要发出一个文本分隔符。
// @TODO 重构文本分隔符,这样当我们最终没有因为 pushStartInstance 而发出标签时,就不必防御性地添加它
// 们
if (textEmbedded) {
// This link follows text but we aren't writing a tag. while not as efficient as possible we need
// to be safe and assume text will follow by inserting a textSeparator
// 这个链接跟随文本,但我们没有写标签。虽然不如可能的效率高,但我们需要安全起见,假设文本将跟随,通过
// 插入一个 textSeparator
target.push(textSeparator);
}

if (isFallback) {
// Hoistable Elements for fallbacks are simply omitted. we don't want to emit them early
// because they are likely superceded by primary content and we want to avoid needing to clean
// them up when the primary content is ready. They are never hydrated on the client anyway because
// boundaries in fallback are awaited or client render, in either case there is never hydration
// 可提升元素用于回退内容会被简单地省略。我们不想提前输出它们。因为它们很可能会被主要内容替代,并且我
// 们希望避免在主要内容准备好时需要清理它们。它们在客户端也从不被水合,因为回退中的边界要么被等待,要
// 么是客户端渲染,在任何情况下都不会进行水合
return null;
} else {
return pushLinkImpl(renderState.hoistableChunks, props);
}
}
}

32. 推送链接实现

备注
function pushLinkImpl(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
): null {
target.push(startChunkForTag('link'));

for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
case 'dangerouslySetInnerHTML':
throw new Error(
`${'link'} is a self-closing tag and must neither have \`children\` nor ` +
'use `dangerouslySetInnerHTML`.',
);
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}

// Link never participate as a ViewTransition
// 链接从不作为 ViewTransition 参与

target.push(endOfStartTagSelfClosing);
return null;
}

33. 推送样式

备注
function pushStyle(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
resumableState: ResumableState,
renderState: RenderState,
hoistableState: null | HoistableState,
textEmbedded: boolean,
formatContext: FormatContext,
): ReactNodeList {
const noscriptTagInScope = formatContext.tagScope & NOSCRIPT_SCOPE;
if (__DEV__) {
if (hasOwnProperty.call(props, 'children')) {
const children = props.children;

const child = Array.isArray(children)
? children.length < 2
? children[0]
: null
: children;

if (
typeof child === 'function' ||
typeof child === 'symbol' ||
Array.isArray(child)
) {
const childType =
typeof child === 'function'
? 'a Function'
: typeof child === 'symbol'
? 'a Sybmol'
: 'an Array';
console.error(
'React expect children of <style> tags to be a string, number, or object with a `toString` method but found %s instead. ' +
'In browsers style Elements can only have `Text` Nodes as children.',
childType,
);
}
}
}
const precedence = props.precedence;
const href = props.href;
const nonce = props.nonce;

if (
formatContext.insertionMode === SVG_MODE ||
noscriptTagInScope ||
props.itemProp != null ||
typeof precedence !== 'string' ||
typeof href !== 'string' ||
href === ''
) {
// This style tag is not able to be turned into a Style Resource
// 这个样式标签无法转换为样式资源
return pushStyleImpl(target, props);
}

if (__DEV__) {
if (href.includes(' ')) {
console.error(
'React expected the `href` prop for a <style> tag opting into hoisting semantics using the `precedence` prop to not have any spaces but ecountered spaces instead. using spaces in this prop will cause hydration of this style to fail on the client. The href for the <style> where this ocurred is "%s".',
href,
);
}
}

const key = getResourceKey(href);
let styleQueue = renderState.styles.get(precedence);
const hasKey = resumableState.styleResources.hasOwnProperty(key);
const resourceState = hasKey ? resumableState.styleResources[key] : undefined;
if (resourceState !== EXISTS) {
// We are going to create this resource now so it is marked as Exists
// 我们现在将创建此资源,因此它被标记为存在
resumableState.styleResources[key] = EXISTS;

if (__DEV__) {
if (resourceState) {
console.error(
'React encountered a hoistable style tag for the same href as a preload: "%s". When using a style tag to inline styles you should not also preload it as a stylsheet.',
href,
);
}
}

if (!styleQueue) {
// This is the first time we've encountered this precedence we need
// to create a StyleQueue.
// 这是我们第一次遇到这个优先级,我们需要创建一个 StyleQueue。
styleQueue = {
precedence: stringToChunk(escapeTextForBrowser(precedence)),
rules: [] as Array<Chunk | PrecomputedChunk>,
hrefs: [] as Array<Chunk | PrecomputedChunk>,
sheets: new Map() as Map<string, StylesheetResource>,
};
renderState.styles.set(precedence, styleQueue);
}

const nonceStyle = renderState.nonce.style;
if (!nonceStyle || nonceStyle === nonce) {
if (__DEV__) {
if (!nonceStyle && nonce) {
console.error(
'React encountered a style tag with `precedence` "%s" and `nonce` "%s". When React manages style rules using `precedence` it will only include a nonce attributes if you also provide the same style nonce value as a render option.',
precedence,
nonce,
);
}
}
styleQueue.hrefs.push(stringToChunk(escapeTextForBrowser(href)));
pushStyleContents(styleQueue.rules, props);
} else if (__DEV__) {
console.error(
'React encountered a style tag with `precedence` "%s" and `nonce` "%s". When React manages style rules using `precedence` it will only include rules if the nonce matches the style nonce "%s" that was included with this render.',
precedence,
nonce,
nonceStyle,
);
}
}
if (styleQueue) {
// We need to track whether this boundary should wait on this resource or not.
// Typically this resource should always exist since we either had it or just created
// it. However, it's possible when you resume that the style has already been emitted
// and then it wouldn't be recreated in the RenderState and there's no need to track
// it again since we should've hoisted it to the shell already.
// 我们需要跟踪这个边界是否应该等待这个资源。通常这个资源应该总是存在,因为我们要么已经有它,要么刚创建
// 它。然而,当你恢复时,有可能样式已经被发出,然后它不会在 RenderState 中被重新创建,也不需要再次跟踪
// 它,因为我们应该已经将它提升到 shell 中了。
if (hoistableState) {
hoistableState.styles.add(styleQueue);
}
}

if (textEmbedded) {
// This link follows text but we aren't writing a tag. while not as efficient as possible we need
// to be safe and assume text will follow by inserting a textSeparator
// 这个链接跟随文本,但我们没有写标签。虽然不如可能的效率高,但我们需要安全起见,假设文本将跟随,插入一
// 个 textSeparator
target.push(textSeparator);
}
}

34. 转义样式文本内容

备注
/**
* This escaping function is designed to work with style tag textContent only.
* 这个转义函数仅适用于 style 标签的 textContent。
*
* While untrusted style content should be made safe before using this api it will
* ensure that the style cannot be early terminated or never terminated state
*
* 虽然在使用此 API 之前应确保不受信任的 style 内容是安全的,
* 它将确保样式不会被提前终止或永远无法终止。
*/
function escapeStyleTextContent(styleText: string) {
if (__DEV__) {
checkHtmlStringCoercion(styleText);
}
return ('' + styleText).replace(styleRegex, styleReplacer);
}

35. 推送样式实现

备注
function pushStyleImpl(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
): ReactNodeList {
target.push(startChunkForTag('style'));

let children = null;
let innerHTML = null;
for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
children = propValue;
break;
case 'dangerouslySetInnerHTML':
innerHTML = propValue;
break;
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}

// Style never participate as a ViewTransition.
// 样式从不参与作为 ViewTransition。
target.push(endOfStartTag);

const child = Array.isArray(children)
? children.length < 2
? children[0]
: null
: children;
if (
typeof child !== 'function' &&
typeof child !== 'symbol' &&
child !== null &&
child !== undefined
) {
target.push(stringToChunk(escapeStyleTextContent(child)));
}
pushInnerHTML(target, innerHTML, children);
target.push(endChunkForTag('style'));
return null;
}

36. 推送样式内容

备注
function pushStyleContents(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
): void {
let children = null;
let innerHTML = null;
for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
children = propValue;
break;
case 'dangerouslySetInnerHTML':
innerHTML = propValue;
break;
}
}
}

const child = Array.isArray(children)
? children.length < 2
? children[0]
: null
: children;
if (
typeof child !== 'function' &&
typeof child !== 'symbol' &&
child !== null &&
child !== undefined
) {
target.push(stringToChunk(escapeStyleTextContent(child)));
}
pushInnerHTML(target, innerHTML, children);
return;
}

37. 推送图片

备注
function pushImg(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
resumableState: ResumableState,
renderState: RenderState,
hoistableState: null | HoistableState,
formatContext: FormatContext,
): null {
const pictureOrNoScriptTagInScope =
formatContext.tagScope & (PICTURE_SCOPE | NOSCRIPT_SCOPE);
const { src, srcSet } = props;
if (
props.loading !== 'lazy' &&
(src || srcSet) &&
(typeof src === 'string' || src == null) &&
(typeof srcSet === 'string' || srcSet == null) &&
props.fetchPriority !== 'low' &&
!pictureOrNoScriptTagInScope &&
// We exclude data URIs in src and srcSet since these should not be preloaded
// 我们在 src 和 srcSet 中排除数据 URI,因为这些不应被预加载
!(
typeof src === 'string' &&
src[4] === ':' &&
(src[0] === 'd' || src[0] === 'D') &&
(src[1] === 'a' || src[1] === 'A') &&
(src[2] === 't' || src[2] === 'T') &&
(src[3] === 'a' || src[3] === 'A')
) &&
!(
typeof srcSet === 'string' &&
srcSet[4] === ':' &&
(srcSet[0] === 'd' || srcSet[0] === 'D') &&
(srcSet[1] === 'a' || srcSet[1] === 'A') &&
(srcSet[2] === 't' || srcSet[2] === 'T') &&
(srcSet[3] === 'a' || srcSet[3] === 'A')
)
) {
// We have a suspensey image and ought to preload it to optimize the loading of display blocking
// resumableState.
// 我们有一张挂起感的图片,应该预加载它以优化阻塞显示的加载 resumableState。

if (hoistableState !== null) {
// Mark this boundary's state as having suspensey images.
// Only do that if we have a ViewTransition that might trigger a parent Suspense boundary
// to animate its appearing. Since that's the only case we'd actually apply suspensey images
// for SSR reveals.
// 将此边界的状态标记为包含挂起图像。仅在我们有可能触发父 Suspense 边界以动画显示的
// ViewTransition 时才这么做。因为这是我们实际为 SSR 显示应用挂起图像的唯一情况。
const isInSuspenseWithEnterViewTransition =
formatContext.tagScope & APPEARING_SCOPE;
if (isInSuspenseWithEnterViewTransition) {
hoistableState.suspenseyImages = true;
}
}

const sizes = typeof props.sizes === 'string' ? props.sizes : undefined;
const key = getImageResourceKey(src, srcSet, sizes);

const promotablePreloads = renderState.preloads.images;

let resource = promotablePreloads.get(key);
if (resource) {
// We consider whether this preload can be promoted to higher priority flushing queue.
// The only time a resource will exist here is if it was created during this render
// and was not already in the high priority queue.
// 我们考虑是否可以将此预加载提升到更高优先级的刷新队列。资源只有在以下情况下才会存在于此:它是在此渲
// 染过程中创建的。并且尚未在高优先级队列中。
if (
props.fetchPriority === 'high' ||
renderState.highImagePreloads.size < 10
) {
// Delete the resource from the map since we are promoting it and don't want to
// reenter this branch in a second pass for duplicate img hrefs.
// 从映射中删除该资源,因为我们正在提升它,并且不希望在第二次处理重复图片链接时再次进入此分支。
promotablePreloads.delete(key);

renderState.highImagePreloads.add(resource);
}
} else if (!resumableState.imageResources.hasOwnProperty(key)) {
// We must construct a new preload resource
// 我们必须构建一个新的预加载资源
resumableState.imageResources[key] = PRELOAD_NO_CREDS;
const crossOrigin = getCrossOriginString(props.crossOrigin);

const headers = renderState.headers;
let header;
if (
headers &&
headers.remainingCapacity > 0 &&
// browsers today don't support preloading responsive images from link headers so we bail out
// if the img has srcset defined
// 现在的浏览器不支持从链接头预加载响应式图片,所以我们放弃。如果图片定义了 srcset
typeof props.srcSet !== 'string' &&
// this is a hueristic similar to capping element preloads to 10 unless explicitly
// fetchPriority="high". We use length here which means it will fit fewer images when
// the urls are long and more when short. arguably byte size is a better hueristic because
// it directly translates to how much we send down before content is actually seen.
// We could unify the counts and also make it so the total is tracked regardless of
// flushing output but since the headers are likely to be go earlier than content
// they don't really conflict so for now I've kept them separate
// 这是一个启发式方法,类似于将元素预加载数量限制为 10,除非明确指定 fetchPriority="high"。
// 我们这里使用长度,这意味着当 URL 很长时,它能容纳的图片会更少,而 URL 短时则能容纳更多。
// 可以说字节大小是一个更好的启发式方法,因为它直接对应于在内容实际显示之前我们发送的数据量。
// 我们可以统一计数,并且可以使总数跟踪独立于输出刷新进行,但由于头信息可能比内容先到达,它们实际上
// 不会冲突,所以目前我保持它们分开。
(props.fetchPriority === 'high' ||
headers.highImagePreloads.length < 500) &&
// We manually construct the options for the preload only from strings. We don't want to pollute
// the params list with arbitrary props and if we copied everything over as it we might get
// coercion errors. We have checks for this in Dev but it seems safer to just only accept values
// that are strings
// 我们仅从字符串手动构建预加载的选项。我们不想用任意属性污染参数列表,如果我们原样复制所有内容,可
// 能会出现类型强制错误。我们在开发环境中对此有检查,但似乎更安全的方法是仅接受字符串值
((header = getPreloadAsHeader(src, 'image', {
imageSrcSet: props.srcSet,
imageSizes: props.sizes,
crossOrigin,
integrity: props.integrity,
nonce: props.nonce,
type: props.type,
fetchPriority: props.fetchPriority,
referrerPolicy: props.refererPolicy,
})),
// We always consume the header length since once we find one header that doesn't fit
// we assume all the rest won't as well. This is to avoid getting into a situation
// where we have a very small remaining capacity but no headers will ever fit and we end
// up constantly trying to see if the next resource might make it. In the future we can
// make this behavior different between render and prerender since in the latter case
// we are less sensitive to the current requests runtime per and more sensitive to maximizing
// headers.
// 我们总是消耗头部长度,因为一旦我们发现有一个头部不适合,我们就假设其余的头部也不适合。这是为了避
// 免出现剩余容量非常小但没有任何头部能够适配的情况,从而我们不断尝试看下一个资源是否能适配。将来我
// 们可以在渲染和预渲染之间使这种行为有所不同,因为在预渲染的情况下,我们对当前请求的运行时间不太敏
// 感,更加关注最大化头部使用。
(headers.remainingCapacity -= header.length + 2) >= 0)
) {
// If we postpone in the shell we will still emit this preload so we track
// it to make sure we don't reset it.
// 如果我们在 shell 中推迟,我们仍然会发出这个预加载,因此我们跟踪它以确保我们不会重置它。
renderState.resets.image[key] = PRELOAD_NO_CREDS;
if (headers.highImagePreloads) {
headers.highImagePreloads += ', ';
}
headers.highImagePreloads += header;
} else {
resource = [];
pushLinkImpl(resource, {
rel: 'preload',
as: 'image',
// There is a bug in Safari where imageSrcSet is not respected on preload links
// so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
// This harms older browers that do not support imageSrcSet by making their preloads not work
// but this population is shrinking fast and is already small so we accept this tradeoff.
// Safari 中存在一个 bug,即在预加载链接上不尊重 imageSrcSet。因此如果我们有 imageSrcSet,
// 这里会省略 href,因为 Safari 会加载错误的图片。这会影响不支持 imageSrcSet 的旧浏览器,使
// 它们的预加载无法工作。但这类用户群体正在快速减少,并且已经很少,所以我们接受这个权衡。
href: srcSet ? undefined : src,
imageSrcSet: srcSet,
imageSizes: sizes,
crossOrigin: crossOrigin,
integrity: props.integrity,
type: props.type,
fetchPriority: props.fetchPriority,
referrerPolicy: props.referrerPolicy,
} as PreloadProps);
if (
props.fetchPriority === 'high' ||
renderState.highImagePreloads.size < 10
) {
renderState.highImagePreloads.add(resource);
} else {
renderState.bulkPreloads.add(resource);
// We can bump the priority up if the same img is rendered later
// with fetchPriority="high"
// 如果稍后使用 fetchPriority="high" 渲染相同的图像,我们可以提高优先级
promotablePreloads.set(key, resource);
}
}
}
}
return pushSelfClosing(target, props, 'img', formatContext);
}

38. 推送自关闭

备注
function pushSelfClosing(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
tag: string,
formatContext: FormatContext,
): null {
target.push(startChunkForTag(tag));

for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
case 'dangerouslySetInnerHTML':
throw new Error(
`${tag} is a self-closing tag and must neither have \`children\` nor ` +
'use `dangerouslySetInnerHTML`.',
);
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}

pushViewTransitionAttributes(target, formatContext);

target.push(endOfStartTagSelfClosing);
return null;
}

39. 点击开始菜单项

备注
function pushStartMenuItem(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
formatContext: FormatContext,
): ReactNodeList {
target.push(startChunkForTag('menuitem'));

for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
case 'dangerouslySetInnerHTML':
throw new Error(
'menuitems cannot have `children` nor `dangerouslySetInnerHTML`.',
);
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}

pushViewTransitionAttributes(target, formatContext);

target.push(endOfStartTag);
return null;
}

40. 推送标题

备注
function pushTitle(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
renderState: RenderState,
formatContext: FormatContext,
): ReactNodeList {
const noscriptTagInScope = formatContext.tagScope & NOSCRIPT_SCOPE;
const isFallback = formatContext.tagScope & FALLBACK_SCOPE;
if (__DEV__) {
if (hasOwnProperty.call(props, 'children')) {
const children = props.children;

const child = Array.isArray(children)
? children.length < 2
? children[0]
: null
: children;

if (Array.isArray(children) && children.length > 1) {
console.error(
'React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an Array with length %s instead.' +
' Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert `children` of <title> tags to a single string value' +
' which is why Arrays of length greater than 1 are not supported. When using JSX it can be common to combine text nodes and value nodes.' +
' For example: <title>hello {nameOfUser}</title>. While not immediately apparent, `children` in this case is an Array with length 2. If your `children` prop' +
' is using this form try rewriting it using a template string: <title>{`hello ${nameOfUser}`}</title>.',
children.length,
);
} else if (typeof child === 'function' || typeof child === 'symbol') {
const childType =
typeof child === 'function' ? 'a Function' : 'a Sybmol';
console.error(
'React expect children of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found %s instead.' +
' Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title>' +
' tags to a single string value.',
childType,
);
} else if (child && child.toString === {}.toString) {
if (child.$$typeof != null) {
console.error(
'React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an object that appears to be' +
' a React element which never implements a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to' +
' be able to convert children of <title> tags to a single string value which is why rendering React elements is not supported. If the `children` of <title> is' +
' a React Component try moving the <title> tag into that component. If the `children` of <title> is some HTML markup change it to be Text only to be valid HTML.',
);
} else {
console.error(
'React expects the `children` prop of <title> tags to be a string, number, bigint, or object with a novel `toString` method but found an object that does not implement' +
' a suitable `toString` method. Browsers treat all child Nodes of <title> tags as Text content and React expects to be able to convert children of <title> tags' +
' to a single string value. Using the default `toString` method available on every object is almost certainly an error. Consider whether the `children` of this <title>' +
' is an object in error and change it to a string or number value if so. Otherwise implement a `toString` method that React can use to produce a valid <title>.',
);
}
}
}
}

if (
formatContext.insertionMode !== SVG_MODE &&
!noscriptTagInScope &&
props.itemProp == null
) {
if (isFallback) {
// Hoistable Elements for fallbacks are simply omitted. we don't want to emit them early
// because they are likely superceded by primary content and we want to avoid needing to clean
// them up when the primary content is ready. They are never hydrated on the client anyway because
// boundaries in fallback are awaited or client render, in either case there is never hydration
// 可提升元素用于回退内容会被简单地省略。我们不想提前输出它们。因为它们很可能会被主要内容替代,并且我
// 们希望避免在主要内容准备好时需要清理它们。它们在客户端也从不被水合,因为回退中的边界会被等待或客户
// 端渲染,在任何情况下都不会进行水合
return null;
} else {
pushTitleImpl(renderState.hoistableChunks, props);
}
} else {
return pushTitleImpl(target, props);
}
}

41. 推送标题实现

备注
function pushTitleImpl(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
): null {
target.push(startChunkForTag('title'));

let children = null;
let innerHTML = null;
for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
children = propValue;
break;
case 'dangerouslySetInnerHTML':
innerHTML = propValue;
break;
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}
// Title never participate as a ViewTransition
// 标题从不作为 ViewTransition 参与
target.push(endOfStartTag);

const child = Array.isArray(children)
? children.length < 2
? children[0]
: null
: children;
if (
typeof child !== 'function' &&
typeof child !== 'symbol' &&
child !== null &&
child !== undefined
) {
target.push(stringToChunk(escapeTextForBrowser('' + child)));
}
pushInnerHTML(target, innerHTML, children);
target.push(endChunkForTag('title'));
return null;
}

42. 推动开始头

function pushStartHead(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
renderState: RenderState,
preambleState: null | PreambleState,
formatContext: FormatContext,
): ReactNodeList {
if (formatContext.insertionMode < HTML_MODE) {
// This <head> is the Document.head and should be part of the preamble
// 这个 <head> 是 Document.head,应该是前言的一部分
const preamble = preambleState || renderState.preamble;

if (preamble.headChunks) {
throw new Error(`The ${'`<head>`'} tag may only be rendered once.`);
}

// Insert a marker in the body where the contribution to the head was in case we need to clear it.
// 在主体中插入一个标记,以防我们需要清除头部的贡献。
if (preambleState !== null) {
target.push(headPreambleContributionChunk);
}

preamble.headChunks = [];
return pushStartSingletonElement(
preamble.headChunks,
props,
'head',
formatContext,
);
} else {
// This <head> is deep and is likely just an error. we emit it inline though.
// Validation should warn that this tag is the the wrong spot.
// 这个 <head> 很深,可能只是一个错误。不过我们还是内联输出它。 验证时应警告该标签位置错误。
return pushStartGenericElement(target, props, 'head', formatContext);
}
}

43. 启动身体

function pushStartBody(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
renderState: RenderState,
preambleState: null | PreambleState,
formatContext: FormatContext,
): ReactNodeList {
if (formatContext.insertionMode < HTML_MODE) {
// This <body> is the Document.body
const preamble = preambleState || renderState.preamble;

if (preamble.bodyChunks) {
throw new Error(`The ${'`<body>`'} tag may only be rendered once.`);
}

// Insert a marker in the body where the contribution to the body tag was in case we need to clear it.
// 在 body 中插入一个标记,以防我们需要清除它之前对 body 标签的贡献。
if (preambleState !== null) {
target.push(bodyPreambleContributionChunk);
}

preamble.bodyChunks = [];
return pushStartSingletonElement(
preamble.bodyChunks,
props,
'body',
formatContext,
);
} else {
// This <head> is deep and is likely just an error. we emit it inline though.
// Validation should warn that this tag is the the wrong spot.
// 这个 <head> 很深,可能只是一个错误。不过我们还是内联输出它。验证时应警告该标签位置错误。
return pushStartGenericElement(target, props, 'body', formatContext);
}
}

44. 推送开始 HTML

function pushStartHtml(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
renderState: RenderState,
preambleState: null | PreambleState,
formatContext: FormatContext,
): ReactNodeList {
if (formatContext.insertionMode === ROOT_HTML_MODE) {
// This <html> is the Document.documentElement
// 这个 <html> 是 Document.documentElement
const preamble = preambleState || renderState.preamble;

if (preamble.htmlChunks) {
throw new Error(`The ${'`<html>`'} tag may only be rendered once.`);
}

// Insert a marker in the body where the contribution to the head was in case we need to clear it.
// 在主体中插入一个标记,以防我们需要清除头部的贡献。
if (preambleState !== null) {
target.push(htmlPreambleContributionChunk);
}

preamble.htmlChunks = [DOCTYPE];
return pushStartSingletonElement(
preamble.htmlChunks,
props,
'html',
formatContext,
);
} else {
// This <html> is deep and is likely just an error. we emit it inline though.
// Validation should warn that this tag is the the wrong spot.
// 这个 <html> 很深,可能只是一个错误。不过我们还是内联输出它。验证时应警告该标签位置错误。
return pushStartGenericElement(target, props, 'html', formatContext);
}
}

45. 推送脚本

function pushScript(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
resumableState: ResumableState,
renderState: RenderState,
textEmbedded: boolean,
formatContext: FormatContext,
): null {
const noscriptTagInScope = formatContext.tagScope & NOSCRIPT_SCOPE;
const asyncProp = props.async;
if (
typeof props.src !== 'string' ||
!props.src ||
!(
asyncProp &&
typeof asyncProp !== 'function' &&
typeof asyncProp !== 'symbol'
) ||
props.onLoad ||
props.onError ||
formatContext.insertionMode === SVG_MODE ||
noscriptTagInScope ||
props.itemProp != null
) {
// This script will not be a resource, we bailout early and emit it in place.
// 这个脚本不会作为资源,我们会提前终止并直接输出它。
return pushScriptImpl(target, props);
}

const src = props.src;
const key = getResourceKey(src);
// We can make this <script> into a ScriptResource
// 我们可以把这个 <script> 制作成一个 ScriptResource

let resources, preloads;
if (props.type === 'module') {
resources = resumableState.moduleScriptResources;
preloads = renderState.preloads.moduleScripts;
} else {
resources = resumableState.scriptResources;
preloads = renderState.preloads.scripts;
}

const hasKey = resources.hasOwnProperty(key);
const resourceState = hasKey ? resources[key] : undefined;
if (resourceState !== EXISTS) {
// We are going to create this resource now so it is marked as Exists
// 我们现在将创建此资源,因此它被标记为已存在
resources[key] = EXISTS;

let scriptProps = props;
if (resourceState) {
// When resourceState is truty it is a Preload state. We cast it for clarity
// 当 resourceState 为真时,它处于预加载状态。我们进行类型转换以便更清楚
const preloadState: Preloaded | PreloadedWithCredentials = resourceState;
if (preloadState.length === 2) {
scriptProps = { ...props };
adoptPreloadCredentials(scriptProps, preloadState);
}

const preloadResource = preloads.get(key);
if (preloadResource) {
// the preload resource exists was created in this render. Now that we have
// a script resource which will emit earlier than a preload would if it
// hasn't already flushed we prevent it from flushing by zeroing the length
// 预加载资源是在这个渲染中创建的。现在我们有了一个脚本资源,如果它还没有刷新,它将比预加载资源更早
// 发出,我们通过将长度归零来防止它刷新
preloadResource.length = 0;
}
}

const resource: Resource = [];
// Add to the script flushing queue
// 添加到脚本刷新队列
renderState.scripts.add(resource);
// encode the tag as Chunks
// 将标签编码为块
pushScriptImpl(resource, scriptProps);
}

if (textEmbedded) {
// This script follows text but we aren't writing a tag. while not as efficient as possible we need
// to be safe and assume text will follow by inserting a textSeparator
// 这个脚本会跟随文本,但我们不会写入标签。虽然效率不算最高,但我们需要安全起见,假设文本会跟随,通过插
// 入 textSeparator
target.push(textSeparator);
}
return null;
}

46. 推送脚本实现

备注
function pushScriptImpl(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
): null {
target.push(startChunkForTag('script'));

let children = null;
let innerHTML = null;
for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
children = propValue;
break;
case 'dangerouslySetInnerHTML':
innerHTML = propValue;
break;
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}
// Scripts never participate as a ViewTransition
// 脚本从不作为 ViewTransition 参与
target.push(endOfStartTag);

if (__DEV__) {
if (children != null && typeof children !== 'string') {
const descriptiveStatement =
typeof children === 'number'
? 'a number for children'
: Array.isArray(children)
? 'an array for children'
: 'something unexpected for children';
console.error(
'A script element was rendered with %s. If script element has children it must be a single string.' +
' Consider using dangerouslySetInnerHTML or passing a plain string as children.',
descriptiveStatement,
);
}
}

pushInnerHTML(target, innerHTML, children);
if (typeof children === 'string') {
target.push(stringToChunk(escapeEntireInlineScriptContent(children)));
}
target.push(endChunkForTag('script'));
return null;
}

47. 推送启动单例元素

备注
// This is a fork of pushStartGenericElement because we don't ever want to do
// the children as strign optimization on that path when rendering singletons.
// When we eliminate that special path we can delete this fork and unify it again
// 这是 pushStartGenericElement 的一个分支,因为在渲染单例时,我们在该路径上永远不想对子元素进行字符串优化。
// 当我们消除那个特殊路径时,我们可以删除这个分支并重新统一它
function pushStartSingletonElement(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
tag: string,
formatContext: FormatContext,
): ReactNodeList {
target.push(startChunkForTag(tag));

let children = null;
let innerHTML = null;
for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
children = propValue;
break;
case 'dangerouslySetInnerHTML':
innerHTML = propValue;
break;
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}

pushViewTransitionAttributes(target, formatContext);

target.push(endOfStartTag);
pushInnerHTML(target, innerHTML, children);
return children;
}

48. 推送开始通用元素

备注
function pushStartGenericElement(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
tag: string,
formatContext: FormatContext,
): ReactNodeList {
target.push(startChunkForTag(tag));

let children = null;
let innerHTML = null;
for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
children = propValue;
break;
case 'dangerouslySetInnerHTML':
innerHTML = propValue;
break;
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}

pushViewTransitionAttributes(target, formatContext);

target.push(endOfStartTag);
pushInnerHTML(target, innerHTML, children);
if (typeof children === 'string') {
// Special case children as a string to avoid the unnecessary comment.
// 将 children 作为字符串处理以避免不必要的注释。
// TODO: Remove this special case after the general optimization is in place.
// TODO:在通用优化就位后移除此特殊处理。
target.push(stringToChunk(encodeHTMLTextNode(children)));
return null;
}
return children;
}

49. 启动自定义元素

备注
function pushStartCustomElement(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
tag: string,
formatContext: FormatContext,
): ReactNodeList {
target.push(startChunkForTag(tag));

let children = null;
let innerHTML = null;
for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
let propValue = props[propKey];
if (propValue == null) {
continue;
}
let attributeName = propKey;
switch (propKey) {
case 'children':
children = propValue;
break;
case 'dangerouslySetInnerHTML':
innerHTML = propValue;
break;
case 'style':
pushStyleAttribute(target, propValue);
break;
case 'suppressContentEditableWarning':
case 'suppressHydrationWarning':
case 'ref':
// Ignored. These are built-in to React on the client.
// 忽略。这些是内置在客户端的 React 中。
break;
case 'className':
// className gets rendered as class on the client, so it should be
// rendered as class on the server.
// className 会在客户端被渲染为 class,因此在服务器上也应渲染为 class。
attributeName = 'class';
// intentional fallthrough
// 有意而为之的贯穿
default:
if (
isAttributeNameSafe(propKey) &&
typeof propValue !== 'function' &&
typeof propValue !== 'symbol'
) {
if (propValue === false) {
continue;
} else if (propValue === true) {
propValue = '';
} else if (typeof propValue === 'object') {
continue;
}
target.push(
attributeSeparator,
stringToChunk(attributeName),
attributeAssign,
stringToChunk(escapeTextForBrowser(propValue)),
attributeEnd,
);
}
break;
}
}
}

// TODO: ViewTransition attributes gets observed by the Custom Element which is a bit sketchy.
// 待办事项:ViewTransition 属性会被自定义元素观察到,这有点可疑。
pushViewTransitionAttributes(target, formatContext);

target.push(endOfStartTag);
pushInnerHTML(target, innerHTML, children);
return children;
}

50. 推送开始预格式化元素

备注
function pushStartPreformattedElement(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
tag: string,
formatContext: FormatContext,
): ReactNodeList {
target.push(startChunkForTag(tag));

let children = null;
let innerHTML = null;
for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
children = propValue;
break;
case 'dangerouslySetInnerHTML':
innerHTML = propValue;
break;
default:
pushAttribute(target, propKey, propValue);
break;
}
}
}

pushViewTransitionAttributes(target, formatContext);

target.push(endOfStartTag);

// text/html ignores the first character in these tags if it's a newline
// Prefer to break application/xml over text/html (for now) by adding
// a newline specifically to get eaten by the parser. (Alternately for
// textareas, replacing "^\n" with "\r\n" doesn't get eaten, and the first
// \r is normalized out by HTMLTextAreaElement#value.)
// text/html 会忽略这些标签中的第一个字符,如果它是换行符。目前更倾向于通过添加换行符让解析器吞掉它,从而
// 优先破坏 application/xml 而不是 text/html。(对于 textarea,可以替换 "^n" 为 "rn",这样不会被吞
// 掉,并且第一个 r 会被 HTMLTextAreaElement#value 规范化。)
// See: <http://www.w3.org/TR/html-polyglot/#newlines-in-textarea-and-pre>
// See: <http://www.w3.org/TR/html5/syntax.html#element-restrictions>
// See: <http://www.w3.org/TR/html5/syntax.html#newlines>
// See: Parsing of "textarea" "listing" and "pre" elements
// 参见:“textarea”“listing”和“pre”元素的解析
// from <http://www.w3.org/TR/html5/syntax.html#parsing-main-inbody>
// TODO: This doesn't deal with the case where the child is an array
// or component that returns a string.
// 待办:这没有处理子元素是数组或返回字符串的组件的情况。
if (innerHTML != null) {
if (children != null) {
throw new Error(
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
);
}

if (typeof innerHTML !== 'object' || !('__html' in innerHTML)) {
throw new Error(
'`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
'Please visit https://react.dev/link/dangerously-set-inner-html ' +
'for more information.',
);
}

const html = innerHTML.__html;
if (html !== null && html !== undefined) {
if (typeof html === 'string' && html.length > 0 && html[0] === '\n') {
target.push(leadingNewline, stringToChunk(html));
} else {
if (__DEV__) {
checkHtmlStringCoercion(html);
}
target.push(stringToChunk('' + html));
}
}
}
if (typeof children === 'string' && children[0] === '\n') {
target.push(leadingNewline);
}
return children;
}

51. 为标签开始块

备注
function startChunkForTag(tag: string): PrecomputedChunk {
let tagStartChunk = validatedTagCache.get(tag);
if (tagStartChunk === undefined) {
if (!VALID_TAG_REGEX.test(tag)) {
throw new Error(`Invalid tag: ${tag}`);
}

tagStartChunk = stringToPrecomputedChunk('<' + tag);
validatedTagCache.set(tag, tagStartChunk);
}
return tagStartChunk;
}

52. 结束标签块

备注
function endChunkForTag(tag: string): PrecomputedChunk {
let chunk = endTagCache.get(tag);
if (chunk === undefined) {
chunk = stringToPrecomputedChunk('</' + tag + '>');
endTagCache.set(tag, chunk);
}
return chunk;
}

53. 写引导程序

备注
function writeBootstrap(
destination: Destination,
renderState: RenderState,
): boolean {
const bootstrapChunks = renderState.bootstrapChunks;
let i = 0;
for (; i < bootstrapChunks.length - 1; i++) {
writeChunk(destination, bootstrapChunks[i]);
}
if (i < bootstrapChunks.length) {
const lastChunk = bootstrapChunks[i];
bootstrapChunks.length = 0;
return writeChunkAndReturn(destination, lastChunk);
}
return true;
}

54. 编写 Shell 时间指令

备注
function writeShellTimeInstruction(
destination: Destination,
resumableState: ResumableState,
renderState: RenderState,
): boolean {
if (
enableFizzExternalRuntime &&
resumableState.streamingFormat !== ScriptStreamingFormat
) {
// External runtime always tracks the shell time in the runtime.
// 外部运行时总是跟踪运行时中的 shell 时间。
return true;
}
if ((resumableState.instructions & SentMarkShellTime) !== NothingSent) {
// We already sent this instruction.
// 我们已经发送了这个指令。
return true;
}
resumableState.instructions |= SentMarkShellTime;
writeChunk(destination, renderState.startInlineScript);
writeCompletedShellIdAttribute(destination, resumableState);
writeChunk(destination, endOfStartTag);
writeChunk(destination, shellTimeRuntimeScript);
return writeChunkAndReturn(destination, endInlineScript);
}

55. 为指令脚本转义 JS 字符串

function escapeJSStringsForInstructionScripts(input: string): string {
const escaped = JSON.stringify(input);
return escaped.replace(regexForJSStringsInInstructionScripts, match => {
switch (match) {
// santizing breaking out of strings and script tags
// 对字符串和脚本标签进行清理以防止跳出
case '<':
return '\\u003c';
case '\u2028':
return '\\u2028';
case '\u2029':
return '\\u2029';
default: {
throw new Error(
'escapeJSStringsForInstructionScripts encountered a match it does not know how to replace. this means the match regex and the replacement characters are no longer in sync. This is a bug in React',
);
}
}
});
}

56. 为指令脚本转义 JS 对象

function escapeJSObjectForInstructionScripts(input: Object): string {
const escaped = JSON.stringify(input);
return escaped.replace(regexForJSStringsInScripts, match => {
switch (match) {
// santizing breaking out of strings and script tags
// 消毒以防止字符串和脚本标签跳出
case '&':
return '\\u0026';
case '>':
return '\\u003e';
case '<':
return '\\u003c';
case '\u2028':
return '\\u2028';
case '\u2029':
return '\\u2029';
default: {
throw new Error(
'escapeJSObjectForInstructionScripts encountered a match it does not know how to replace. this means the match regex and the replacement characters are no longer in sync. This is a bug in React',
);
}
}
});
}

57. 为边界延迟刷新样式标签

备注
function flushStyleTagsLateForBoundary(
this: Destination,
styleQueue: StyleQueue,
) {
const rules = styleQueue.rules;
const hrefs = styleQueue.hrefs;
if (__DEV__) {
if (rules.length > 0 && hrefs.length === 0) {
console.error(
'React expected to have at least one href for an a hoistable style but found none. This is a bug in React.',
);
}
}
let i = 0;
if (hrefs.length) {
writeChunk(
this,
(currentlyFlushingRenderState as any as RenderState).startInlineStyle,
);
writeChunk(this, lateStyleTagResourceOpen1);
writeChunk(this, styleQueue.precedence);
writeChunk(this, lateStyleTagResourceOpen2);
for (; i < hrefs.length - 1; i++) {
writeChunk(this, hrefs[i]);
writeChunk(this, spaceSeparator);
}
writeChunk(this, hrefs[i]);
writeChunk(this, lateStyleTagResourceOpen3);
for (i = 0; i < rules.length; i++) {
writeChunk(this, rules[i]);
}
destinationHasCapacity = writeChunkAndReturn(
this,
lateStyleTagTemplateClose,
);

// We wrote style tags for this boundary and we may need to emit a script
// to hoist them.
// 我们为这个边界编写了样式标签,并且我们可能需要发出一个脚本来提升它们。
currentlyRenderingBoundaryHasStylesToHoist = true;

// style resources can flush continuously since more rules may be written into
// them with new hrefs. Instead of marking it flushed, we simply reset the chunks
// and hrefs
// 样式资源可以持续刷新,因为可能会随着新的 href 写入更多规则。
// 我们并没有将其标记为已刷新,而是简单地重置块和 hrefs
rules.length = 0;
hrefs.length = 0;
}
}

58. 有要提升的样式

function hasStylesToHoist(stylesheet: StylesheetResource): boolean {
// We need to reveal boundaries with styles whenever a stylesheet it depends on is either
// not flushed or flushed after the preamble (shell).
// 每当依赖的样式表要么未刷新,要么在前言(shell)之后才刷新时,我们需要用样式显示边界。
if (stylesheet.state !== PREAMBLE) {
currentlyRenderingBoundaryHasStylesToHoist = true;
return true;
}
return false;
}

59. 刷新资源

备注
function flushResource(this: Destination, resource: Resource) {
for (let i = 0; i < resource.length; i++) {
writeChunk(this, resource[i]);
}
resource.length = 0;
}

60. 在前言中刷新样式

备注
function flushStyleInPreamble(
this: Destination,
stylesheet: StylesheetResource,
key: string,
map: Map<string, StylesheetResource>,
) {
// We still need to encode stylesheet chunks
// because unlike most Hoistables and Resources we do not eagerly encode
// them during render. This is because if we flush late we have to send a
// different encoding and we don't want to encode multiple times
// 我们仍然需要对样式表块进行编码,因为与大多数可提升元素和资源不同,我们不会在渲染时立即编码
// 这是因为如果我们晚些刷新,就必须发送不同的编码,而我们不想多次编码
pushLinkImpl(stylesheetFlushingQueue, stylesheet.props);
for (let i = 0; i < stylesheetFlushingQueue.length; i++) {
writeChunk(this, stylesheetFlushingQueue[i]);
}
stylesheetFlushingQueue.length = 0;
stylesheet.state = PREAMBLE;
}

61. 在前言中刷新样式

备注
function flushStylesInPreamble(
this: Destination,
styleQueue: StyleQueue,
precedence: string,
) {
const hasStylesheets = styleQueue.sheets.size > 0;
styleQueue.sheets.forEach(flushStyleInPreamble, this);
styleQueue.sheets.clear();

const rules = styleQueue.rules;
const hrefs = styleQueue.hrefs;
// If we don't emit any stylesheets at this precedence we still need to maintain the precedence
// order so even if there are no rules for style tags at this precedence we emit an empty style
// tag with the data-precedence attribute
// 如果在这个优先级下我们没有输出任何样式表,我们仍然需要保持优先级顺序
// 因此,即使在这个优先级下样式标签没有规则,我们也会输出一个带有 data-precedence 属性的空样式标签
if (!hasStylesheets || hrefs.length) {
writeChunk(
this,
(currentlyFlushingRenderState as any as RenderState).startInlineStyle,
);
writeChunk(this, styleTagResourceOpen1);
writeChunk(this, styleQueue.precedence);
let i = 0;
if (hrefs.length) {
writeChunk(this, styleTagResourceOpen2);
for (; i < hrefs.length - 1; i++) {
writeChunk(this, hrefs[i]);
writeChunk(this, spaceSeparator);
}
writeChunk(this, hrefs[i]);
}
writeChunk(this, styleTagResourceOpen3);
for (i = 0; i < rules.length; i++) {
writeChunk(this, rules[i]);
}
writeChunk(this, styleTagResourceClose);

// style resources can flush continuously since more rules may be written into
// them with new hrefs. Instead of marking it flushed, we simply reset the chunks
// and hrefs
// 样式资源可以持续刷新,因为可能会随着新的 href 写入更多规则。我们并没有将其标记为已刷新,而是简单地重
// 置块和 hrefs
rules.length = 0;
hrefs.length = 0;
}
}

62. 延迟样式预加载

备注
function preloadLateStyle(this: Destination, stylesheet: StylesheetResource) {
if (stylesheet.state === PENDING) {
stylesheet.state = PRELOADED;
const preloadProps = preloadAsStylePropsFromProps(
stylesheet.props.href,
stylesheet.props,
);
pushLinkImpl(stylesheetFlushingQueue, preloadProps);
for (let i = 0; i < stylesheetFlushingQueue.length; i++) {
writeChunk(this, stylesheetFlushingQueue[i]);
}
stylesheetFlushingQueue.length = 0;
}
}

63. 预加载延迟样式

function preloadLateStyles(this: Destination, styleQueue: StyleQueue) {
styleQueue.sheets.forEach(preloadLateStyle, this);
styleQueue.sheets.clear();
}

64. 写入阻塞渲染指令

备注
function writeBlockingRenderInstruction(
destination: Destination,
resumableState: ResumableState,
renderState: RenderState,
): void {
if (enableFizzBlockingRender) {
const idPrefix = resumableState.idPrefix;
const shellId = '_' + idPrefix + 'R_';
writeChunk(destination, blockingRenderChunkStart);
writeChunk(destination, stringToChunk(escapeTextForBrowser(shellId)));
writeChunk(destination, blockingRenderChunkEnd);
}
}

65. 写入已完成的 ShellId 属性

备注
function writeCompletedShellIdAttribute(
destination: Destination,
resumableState: ResumableState,
): void {
if ((resumableState.instructions & SentCompletedShellId) !== NothingSent) {
return;
}
resumableState.instructions |= SentCompletedShellId;
const idPrefix = resumableState.idPrefix;
const shellId = '_' + idPrefix + 'R_';
writeChunk(destination, completedShellIdAttributeStart);
writeChunk(destination, stringToChunk(escapeTextForBrowser(shellId)));
writeChunk(destination, attributeEnd);
}

66. 推送已完成的外壳ID属性

备注
function pushCompletedShellIdAttribute(
target: Array<Chunk | PrecomputedChunk>,
resumableState: ResumableState,
): void {
if ((resumableState.instructions & SentCompletedShellId) !== NothingSent) {
return;
}
resumableState.instructions |= SentCompletedShellId;
const idPrefix = resumableState.idPrefix;
const shellId = '_' + idPrefix + 'R_';
target.push(
completedShellIdAttributeStart,
stringToChunk(escapeTextForBrowser(shellId)),
attributeEnd,
);
}

67. 在 JS 中写入样式资源依赖

备注
// This function writes a 2D array of strings to be embedded in javascript.
// 这个函数将一个二维字符串数组写入以嵌入到 JavaScript 中。
// E.g.
// [["JS_escaped_string1", "JS_escaped_string2"]]
function writeStyleResourceDependenciesInJS(
destination: Destination,
hoistableState: HoistableState,
): void {
writeChunk(destination, arrayFirstOpenBracket);

let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
hoistableState.stylesheets.forEach(resource => {
if (resource.state === PREAMBLE) {
// We can elide this dependency because it was flushed in the shell and
// should be ready before content is shown on the client
// 我们可以省略这个依赖,因为它已经在 shell 中刷新过了。并且在内容显示给客户端之前应该已经准备好了
} else if (resource.state === LATE) {
// We only need to emit the href because this resource flushed in an earlier
// boundary already which encoded the attributes necessary to construct
// the resource instance on the client.
// 我们只需要发出 href,因为这个资源已经在之前的边界刷新过,并且已经编码了在客户端构建资源
// 实例所需的属性。
writeChunk(destination, nextArrayOpenBrackChunk);
writeStyleResourceDependencyHrefOnlyInJS(
destination,
resource.props.href,
);
writeChunk(destination, arrayCloseBracket);
nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
} else {
// We need to emit the whole resource for insertion on the client
// 我们需要输出整个资源以便在客户端插入
writeChunk(destination, nextArrayOpenBrackChunk);
writeStyleResourceDependencyInJS(
destination,
resource.props.href,
resource.props['data-precedence'],
resource.props,
);
writeChunk(destination, arrayCloseBracket);
nextArrayOpenBrackChunk = arraySubsequentOpenBracket;

resource.state = LATE;
}
});
writeChunk(destination, arrayCloseBracket);
}

68. 仅在 JS 中编写样式资源依赖Href

备注
/* Helper functions */
/* 辅助函数 */
function writeStyleResourceDependencyHrefOnlyInJS(
destination: Destination,
href: string,
) {
// We should actually enforce this earlier when the resource is created but for
// now we make sure we are actually dealing with a string here.
// 其实我们应该在资源创建时就强制执行这一点,但目前我们确保我们处理的确实是一个字符串。
if (__DEV__) {
checkAttributeStringCoercion(href, 'href');
}
const coercedHref = '' + (href as any);
writeChunk(
destination,
stringToChunk(escapeJSObjectForInstructionScripts(coercedHref)),
);
}

69. 在 JS 中写入样式资源依赖

备注
function writeStyleResourceDependencyInJS(
destination: Destination,
href: mixed,
precedence: mixed,
props: Object,
) {
const coercedHref = sanitizeURL('' + (href as any));
writeChunk(
destination,
stringToChunk(escapeJSObjectForInstructionScripts(coercedHref)),
);

if (__DEV__) {
checkAttributeStringCoercion(precedence, 'precedence');
}
const coercedPrecedence = '' + (precedence as any);
writeChunk(destination, arrayInterstitial);
writeChunk(
destination,
stringToChunk(escapeJSObjectForInstructionScripts(coercedPrecedence)),
);

for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'href':
case 'rel':
case 'precedence':
case 'data-precedence': {
break;
}
case 'children':
case 'dangerouslySetInnerHTML':
throw new Error(
`${'link'} is a self-closing tag and must neither have \`children\` nor ` +
'use `dangerouslySetInnerHTML`.',
);
default:
writeStyleResourceAttributeInJS(destination, propKey, propValue);
break;
}
}
}
return null;
}

70. 在 JS 中编写样式资源属性

备注
function writeStyleResourceAttributeInJS(
destination: Destination,
name: string,
value: string | boolean | number | Function | Object, // not null or undefined
): void {
let attributeName = name.toLowerCase();
let attributeValue;
switch (typeof value) {
case 'function':
case 'symbol':
return;
}

switch (name) {
// Reserved names
// 保留名称
case 'innerHTML':
case 'dangerouslySetInnerHTML':
case 'suppressContentEditableWarning':
case 'suppressHydrationWarning':
case 'style':
case 'ref':
// Ignored
// 忽略
return;

// Attribute renames
// 属性重命名
case 'className': {
attributeName = 'class';
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
attributeValue = '' + (value as any);
break;
}
// Booleans
// 布尔值
case 'hidden': {
if (value === false) {
return;
}
attributeValue = '';
break;
}
// Santized URLs
// 已净化的 URL
case 'src':
case 'href': {
value = sanitizeURL(value);
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
attributeValue = '' + (value as any);
break;
}
default: {
if (
// unrecognized event handlers are not SSR'd and we (apparently)
// use on* as hueristic for these handler props
// 未识别的事件处理程序不会进行 SSR,并且我们(显然)使用 on* 作为这些处理程序属性的启发式
name.length > 2 &&
(name[0] === 'o' || name[0] === 'O') &&
(name[1] === 'n' || name[1] === 'N')
) {
return;
}
if (!isAttributeNameSafe(name)) {
return;
}
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
attributeValue = '' + (value as any);
}
}
writeChunk(destination, arrayInterstitial);
writeChunk(
destination,
stringToChunk(escapeJSObjectForInstructionScripts(attributeName)),
);
writeChunk(destination, arrayInterstitial);
writeChunk(
destination,
stringToChunk(escapeJSObjectForInstructionScripts(attributeValue)),
);
}

71. 在属性中编写样式资源依赖项

备注
// This function writes a 2D array of strings to be embedded in an attribute
// value and read with JSON.parse in ReactDOMServerExternalRuntime.js
// 这个函数将一个二维字符串数组写入属性值中,以便在 ReactDOMServerExternalRuntime.js 中
// 使用 JSON.parse 读取
// E.g.
// [[&quot;JSON_escaped_string1&quot;, &quot;JSON_escaped_string2&quot;]]
function writeStyleResourceDependenciesInAttr(
destination: Destination,
hoistableState: HoistableState,
): void {
writeChunk(destination, arrayFirstOpenBracket);

let nextArrayOpenBrackChunk = arrayFirstOpenBracket;
hoistableState.stylesheets.forEach(resource => {
if (resource.state === PREAMBLE) {
// We can elide this dependency because it was flushed in the shell and
// should be ready before content is shown on the client
// 我们可以省略这个依赖,因为它已经在 shell 中刷新过了,并且在内容显示给客户端之前应该
// 已经准备好了
} else if (resource.state === LATE) {
// We only need to emit the href because this resource flushed in an earlier
// boundary already which encoded the attributes necessary to construct
// the resource instance on the client.
// 我们只需要发出 href,因为这个资源已经在之前的边界刷新过,并且已经编码了在客户端构建资源
// 实例所需的属性。
writeChunk(destination, nextArrayOpenBrackChunk);
writeStyleResourceDependencyHrefOnlyInAttr(
destination,
resource.props.href,
);
writeChunk(destination, arrayCloseBracket);
nextArrayOpenBrackChunk = arraySubsequentOpenBracket;
} else {
// We need to emit the whole resource for insertion on the client
// 我们需要输出整个资源以便在客户端插入
writeChunk(destination, nextArrayOpenBrackChunk);
writeStyleResourceDependencyInAttr(
destination,
resource.props.href,
resource.props['data-precedence'],
resource.props,
);
writeChunk(destination, arrayCloseBracket);
nextArrayOpenBrackChunk = arraySubsequentOpenBracket;

resource.state = LATE;
}
});
writeChunk(destination, arrayCloseBracket);
}

72. 仅在属性中编写样式资源依赖项的 href

备注
/* Helper functions */
/* 辅助函数 */
function writeStyleResourceDependencyHrefOnlyInAttr(
destination: Destination,
href: string,
) {
// We should actually enforce this earlier when the resource is created but for
// now we make sure we are actually dealing with a string here.
// 其实我们应该在资源创建时就强制执行这一点,但目前我们只是确保这里处理的确实是一个字符串。
if (__DEV__) {
checkAttributeStringCoercion(href, 'href');
}
const coercedHref = '' + (href as any);
writeChunk(
destination,
stringToChunk(escapeTextForBrowser(JSON.stringify(coercedHref))),
);
}

73. 在属性中写入样式资源依赖

备注
function writeStyleResourceDependencyInAttr(
destination: Destination,
href: mixed,
precedence: mixed,
props: Object,
) {
const coercedHref = sanitizeURL('' + (href as any));
writeChunk(
destination,
stringToChunk(escapeTextForBrowser(JSON.stringify(coercedHref))),
);

if (__DEV__) {
checkAttributeStringCoercion(precedence, 'precedence');
}
const coercedPrecedence = '' + (precedence as any);
writeChunk(destination, arrayInterstitial);
writeChunk(
destination,
stringToChunk(escapeTextForBrowser(JSON.stringify(coercedPrecedence))),
);

for (const propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'href':
case 'rel':
case 'precedence':
case 'data-precedence': {
break;
}
case 'children':
case 'dangerouslySetInnerHTML':
throw new Error(
`${'link'} is a self-closing tag and must neither have \`children\` nor ` +
'use `dangerouslySetInnerHTML`.',
);
default:
writeStyleResourceAttributeInAttr(destination, propKey, propValue);
break;
}
}
}
return null;
}

74. 在属性中写入样式资源属性

备注
function writeStyleResourceAttributeInAttr(
destination: Destination,
name: string,
// 非空或未定义
value: string | boolean | number | Function | Object, // not null or undefined
): void {
let attributeName = name.toLowerCase();
let attributeValue;
switch (typeof value) {
case 'function':
case 'symbol':
return;
}

switch (name) {
// Reserved names
// 保留名称
case 'innerHTML':
case 'dangerouslySetInnerHTML':
case 'suppressContentEditableWarning':
case 'suppressHydrationWarning':
case 'style':
case 'ref':
// Ignored
// 忽略
return;

// Attribute renames
// 属性重命名
case 'className': {
attributeName = 'class';
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
attributeValue = '' + (value as any);
break;
}

// Booleans
// 布尔值
case 'hidden': {
if (value === false) {
return;
}
attributeValue = '';
break;
}

// Santized URLs
// 已净化的 URL
case 'src':
case 'href': {
value = sanitizeURL(value);
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
attributeValue = '' + (value as any);
break;
}
default: {
if (
// unrecognized event handlers are not SSR'd and we (apparently)
// use on* as hueristic for these handler props
// 未识别的事件处理程序不会进行 SSR,并且我们(显然)
// 使用 on* 作为这些处理程序属性的启发式
name.length > 2 &&
(name[0] === 'o' || name[0] === 'O') &&
(name[1] === 'n' || name[1] === 'N')
) {
return;
}
if (!isAttributeNameSafe(name)) {
return;
}
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
attributeValue = '' + (value as any);
}
}
writeChunk(destination, arrayInterstitial);
writeChunk(
destination,
stringToChunk(escapeTextForBrowser(JSON.stringify(attributeName))),
);
writeChunk(destination, arrayInterstitial);
writeChunk(
destination,
stringToChunk(escapeTextForBrowser(JSON.stringify(attributeValue))),
);
}

75. 获取资源键

function getResourceKey(href: string): string {
return href;
}

76. 获取图片资源键

function getImageResourceKey(
href: string,
imageSrcSet?: ?string,
imageSizes?: ?string,
): string {
if (imageSrcSet) {
return imageSrcSet + '\n' + (imageSizes || '');
}
return href;
}

77. 预取 DNS

备注
function prefetchDNS(href: string) {
const request = resolveRequest();
if (!request) {
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
// fetching) and we don't want to warn in those cases.
// 在异步上下文中,我们有时可以从 AsyncLocalStorage 中解析资源。如果无法解析,我们也可以
// 在非异步上下文中尝试从堆栈获取资源。由于在这两种情况下我们都无法解析此调用的资源,
// 因此我们选择不做任何操作。我们可以考虑将其作为警告,但有时在渲染之外调用函数是故意的(
// 例如,为了预加载数据),在这些情况下我们不希望发出警告。
previousDispatcher.D(/* prefetchDNS */ href);
return;
}
const resumableState = getResumableState(request);
const renderState = getRenderState(request);

if (typeof href === 'string' && href) {
const key = getResourceKey(href);
if (!resumableState.dnsResources.hasOwnProperty(key)) {
resumableState.dnsResources[key] = EXISTS;

const headers = renderState.headers;
let header;
if (
headers &&
headers.remainingCapacity > 0 &&
// Compute the header since we might be able to fit it in the max length
// 计算头部,因为我们可能能够将其放入最大长度
((header = getPrefetchDNSAsHeader(href)),
// We always consume the header length since once we find one header that doesn't fit
// we assume all the rest won't as well. This is to avoid getting into a situation
// where we have a very small remaining capacity but no headers will ever fit and we end
// up constantly trying to see if the next resource might make it. In the future we can
// make this behavior different between render and prerender since in the latter case
// we are less sensitive to the current requests runtime per and more sensitive to maximizing
// headers.
// 我们总是消耗头部长度,因为一旦我们发现有一个头部不适合,我们就假设其余的头部也不适
// 合。这是为了避免出现剩余容量非常小但没有任何头部能够适配的情况,从而我们不断尝试看下
// 一个资源是否能适配。将来我们可以在渲染和预渲染之间使这种行为有所不同,因为在预渲染的
// 情况下,我们对当前请求的运行时间不太敏感,更加关注最大化头部。
(headers.remainingCapacity -= header.length + 2) >= 0)
) {
// Store this as resettable in case we are prerendering and postpone in the Shell
// 将其存储为可重置,以防我们正在预渲染并在 Shell 中延迟
renderState.resets.dns[key] = EXISTS;
if (headers.preconnects) {
headers.preconnects += ', ';
}
headers.preconnects += header;
} else {
// Encode as element
// 编码为元素
const resource: Resource = [];
pushLinkImpl(resource, {
href,
rel: 'dns-prefetch',
} as PreconnectProps);
renderState.preconnects.add(resource);
}
}
flushResources(request);
}
}

78. 预连接

备注
function preconnect(href: string, crossOrigin: ?CrossOriginEnum) {
const request = resolveRequest();
if (!request) {
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
// fetching) and we don't want to warn in those cases.
// 在异步上下文中,我们有时可以从 AsyncLocalStorage 中解析资源。如果无法解析,我们也可以
// 在不处于异步上下文时尝试从堆栈中获取它们。由于在这两种情况下我们都无法解析此调用的资源,
// 所以我们选择不做任何处理。我们可以考虑将其作为警告,但在某些情况下在渲染之外调用函数可能
// 是有意的(例如,为了预先加载数据),在这些情况下我们不希望发出警告。
previousDispatcher.C(/* preconnect */ href, crossOrigin);
return;
}
const resumableState = getResumableState(request);
const renderState = getRenderState(request);

if (typeof href === 'string' && href) {
const bucket =
crossOrigin === 'use-credentials'
? 'credentials'
: typeof crossOrigin === 'string'
? 'anonymous'
: 'default';
const key = getResourceKey(href);
if (!resumableState.connectResources[bucket].hasOwnProperty(key)) {
resumableState.connectResources[bucket][key] = EXISTS;

const headers = renderState.headers;
let header;
if (
headers &&
headers.remainingCapacity > 0 &&
// Compute the header since we might be able to fit it in the max length
// 计算头部,因为我们可能能够将其放入最大长度
((header = getPreconnectAsHeader(href, crossOrigin)),
// We always consume the header length since once we find one header that doesn't fit
// we assume all the rest won't as well. This is to avoid getting into a situation
// where we have a very small remaining capacity but no headers will ever fit and we end
// up constantly trying to see if the next resource might make it. In the future we can
// make this behavior different between render and prerender since in the latter case
// we are less sensitive to the current requests runtime per and more sensitive to maximizing
// headers.
// 我们总是消耗头部长度,因为一旦我们发现有一个头部不适合,我们就假设其余的头部也不适
// 合。这是为了避免出现剩余容量非常小但没有任何头部能够适配的情况,从而我们不断尝试
// 看下一个资源是否能适配。将来我们可以在渲染和预渲染之间使这种行为有所不同,因为在预渲
// 染的情况下,我们对当前请求的运行时间不太敏感,更加关注最大化头部。
(headers.remainingCapacity -= header.length + 2) >= 0)
) {
// Store this in resettableState in case we are prerending and postpone in the Shell
// 将此存储在 resettableState 中,以防我们在预渲染时并在 Shell 中延迟
renderState.resets.connect[bucket][key] = EXISTS;
if (headers.preconnects) {
headers.preconnects += ', ';
}
headers.preconnects += header;
} else {
const resource: Resource = [];
pushLinkImpl(resource, {
rel: 'preconnect',
href,
crossOrigin,
} as PreconnectProps);
renderState.preconnects.add(resource);
}
}
flushResources(request);
}
}

79. 预加载

备注
function preload(href: string, as: string, options?: ?PreloadImplOptions) {
const request = resolveRequest();
if (!request) {
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
// fetching) and we don't want to warn in those cases.
// 在异步上下文中,我们有时可以从 AsyncLocalStorage 中解析资源。如果无法解析,我们也可以
// 在非异步上下文中尝试从堆栈获取资源。由于在这两种情况下我们都无法解析此调用的资源,
// 因此我们选择不做任何操作。我们可以考虑将其作为警告,但有时在渲染之外调用函数是故意的(
// 例如,为了预热数据获取),在这种情况下我们不希望发出警告。
previousDispatcher.L(/* preload */ href, as, options);
return;
}
const resumableState = getResumableState(request);
const renderState = getRenderState(request);
if (as && href) {
switch (as) {
case 'image': {
let imageSrcSet, imageSizes, fetchPriority;
if (options) {
imageSrcSet = options.imageSrcSet;
imageSizes = options.imageSizes;
fetchPriority = options.fetchPriority;
}
const key = getImageResourceKey(href, imageSrcSet, imageSizes);
if (resumableState.imageResources.hasOwnProperty(key)) {
// we can return if we already have this resource
// 如果我们已经有了这个资源,我们可以返回
return;
}
resumableState.imageResources[key] = PRELOAD_NO_CREDS;

const headers = renderState.headers;
let header: string;
if (
headers &&
headers.remainingCapacity > 0 &&
// browsers today don't support preloading responsive images from link headers so we bail out
// if the img has srcset defined
// 现在的浏览器不支持从链接头预加载响应式图片,所以我们放弃。如果图片定义了 srcset
typeof imageSrcSet !== 'string' &&
// We only include high priority images in the link header
// 计算头部,因为我们可能能够将其放入最大长度
fetchPriority === 'high' &&
// Compute the header since we might be able to fit it in the max length
((header = getPreloadAsHeader(href, as, options)),
// We always consume the header length since once we find one header that doesn't fit
// we assume all the rest won't as well. This is to avoid getting into a situation
// where we have a very small remaining capacity but no headers will ever fit and we end
// up constantly trying to see if the next resource might make it. In the future we can
// make this behavior different between render and prerender since in the latter case
// we are less sensitive to the current requests runtime per and more sensitive to maximizing
// headers.
// 我们总是消耗头部长度,因为一旦我们发现有一个头部不适合,我们就假设其余的头部也不
// 适合。这是为了避免出现剩余容量非常小但没有任何头部能够适配的情况,从而我们不断尝
// 试看下一个资源是否能适配。将来我们可以在渲染和预渲染之间使这种行为有所不同,因为
// 在预渲染的情况下,我们对当前请求的运行时间不太敏感,更加关注最大化头部使用。
(headers.remainingCapacity -= header.length + 2) >= 0)
) {
// If we postpone in the shell we will still emit a preload as a header so we
// track this to make sure we don't reset it.
// 如果我们在 shell 中推迟,我们仍然会作为头部发出一个预加载,所以我们跟踪
// 这个以确保我们不会重置它。
renderState.resets.image[key] = PRELOAD_NO_CREDS;
if (headers.highImagePreloads) {
headers.highImagePreloads += ', ';
}

headers.highImagePreloads += header;
} else {
// If we don't have headers to write to we have to encode as elements to flush in the head
// When we have imageSrcSet the browser probably cannot load the right version from headers
// (this should be verified by testing). For now we assume these need to go in the head
// as elements even if headers are available.
// 如果我们没有头部可以写入,我们必须将其编码为元素以便在头部刷新。当我们有
// imageSrcSet 时,浏览器可能无法从头部加载正确的版本(这应该通过测试来验证)。目
// 前,我们假设即使有头部可用,这些也需要作为元素放在头部。
const resource = [] as Resource;
pushLinkImpl(
resource,
Object.assign(
{
rel: 'preload',
// There is a bug in Safari where imageSrcSet is not respected on preload links
// so we omit the href here if we have imageSrcSet b/c safari will load the wrong image.
// This harms older browers that do not support imageSrcSet by making their preloads not work
// but this population is shrinking fast and is already small so we accept this tradeoff.
// Safari 中存在一个 bug,即在预加载链接上不尊重 imageSrcSet 因此如果我
// 们有 imageSrcSet,这里会省略 href,因为 Safari 会加载错误的图片。这会
// 影响不支持 imageSrcSet 的旧浏览器,使它们的预加载无法工作。但这类用户群
// 体正在快速减少,并且已经很小,所以我们接受这个权衡。
href: imageSrcSet ? undefined : href,
as,
} as PreloadAsProps,
options,
),
);
if (fetchPriority === 'high') {
renderState.highImagePreloads.add(resource);
} else {
renderState.bulkPreloads.add(resource);
// Stash the resource in case we need to promote it to higher priority
// when an img tag is rendered
// 将资源存储起来,以防我们需要在渲染 img 标签时将其提升为更高的优先级
renderState.preloads.images.set(key, resource);
}
}
break;
}
case 'style': {
const key = getResourceKey(href);
if (resumableState.styleResources.hasOwnProperty(key)) {
// we can return if we already have this resource
// 如果我们已经有了这个资源,我们可以返回
return;
}
const resource = [] as Resource;
pushLinkImpl(
resource,
Object.assign(
{ rel: 'preload', href, as } as PreloadAsProps,
options,
),
);
resumableState.styleResources[key] =
options &&
(typeof options.crossOrigin === 'string' ||
typeof options.integrity === 'string')
? [options.crossOrigin, options.integrity]
: PRELOAD_NO_CREDS;
renderState.preloads.stylesheets.set(key, resource);
renderState.bulkPreloads.add(resource);
break;
}
case 'script': {
const key = getResourceKey(href);
if (resumableState.scriptResources.hasOwnProperty(key)) {
// we can return if we already have this resource
// 如果我们已经有了这个资源,我们可以返回
return;
}
const resource = [] as Resource;
renderState.preloads.scripts.set(key, resource);
renderState.bulkPreloads.add(resource);
pushLinkImpl(
resource,
Object.assign(
{ rel: 'preload', href, as } as PreloadAsProps,
options,
),
);
resumableState.scriptResources[key] =
options &&
(typeof options.crossOrigin === 'string' ||
typeof options.integrity === 'string')
? [options.crossOrigin, options.integrity]
: PRELOAD_NO_CREDS;
break;
}
default: {
const key = getResourceKey(href);
const hasAsType = resumableState.unknownResources.hasOwnProperty(as);
let resources;
if (hasAsType) {
resources = resumableState.unknownResources[as];
if (resources.hasOwnProperty(key)) {
// we can return if we already have this resource
// 如果我们已经有了这个资源,我们可以返回
return;
}
} else {
resources = {} as ResumableState['unknownResources']['asType'];
resumableState.unknownResources[as] = resources;
}
resources[key] = PRELOAD_NO_CREDS;

const headers = renderState.headers;
let header;
if (
headers &&
headers.remainingCapacity > 0 &&
as === 'font' &&
// We compute the header here because we might be able to fit it in the max length
// 我们在这里计算头部,因为我们可能能够将其放入最大长度
((header = getPreloadAsHeader(href, as, options)),
// We always consume the header length since once we find one header that doesn't fit
// we assume all the rest won't as well. This is to avoid getting into a situation
// where we have a very small remaining capacity but no headers will ever fit and we end
// up constantly trying to see if the next resource might make it. In the future we can
// make this behavior different between render and prerender since in the latter case
// we are less sensitive to the current requests runtime per and more sensitive to maximizing
// headers.
// 我们总是消耗头部长度,因为一旦我们发现有一个头部不适合,我们就假设其余的头部也不
// 适合。这是为了避免出现剩余容量非常小但没有任何头部能够适配的情况,从而我们不断尝
// 试看下一个资源是否能适配。将来我们可以在渲染和预渲染之间。使这种行为有所不同,因
// 为在预渲染的情况下,我们对当前请求的运行时间不太敏感,更加关注最大化头部使用。
(headers.remainingCapacity -= header.length + 2) >= 0)
) {
// If we postpone in the shell we will still emit this preload so we
// track it here to prevent it from being reset.
// 如果我们在 shell 中推迟,我们仍然会发出这个预加载,因此我们在这里跟踪它以防止它
// 被重置。
renderState.resets.font[key] = PRELOAD_NO_CREDS;
if (headers.fontPreloads) {
headers.fontPreloads += ', ';
}
headers.fontPreloads += header;
} else {
// We either don't have headers or we are preloading something that does
// not warrant elevated priority so we encode as an element.
// 我们要么没有头文件,要么正在预加载某些不需要优先处理的内容,所以我们将其编码
// 为一个元素。
const resource = [] as Resource;
const props = Object.assign(
{
rel: 'preload',
href,
as,
} as PreloadAsProps,
options,
);
pushLinkImpl(resource, props);
switch (as) {
case 'font':
renderState.fontPreloads.add(resource);
break;
// intentional fall through
// 故意穿透
default:
renderState.bulkPreloads.add(resource);
}
}
}
}
// If we got this far we created a new resource
// 如果我们走到这一步,我们创建了一个新的资源
flushResources(request);
}
}

80. 预加载模块

备注
function preloadModule(
href: string,
options?: ?PreloadModuleImplOptions,
): void {
const request = resolveRequest();
if (!request) {
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
// fetching) and we don't want to warn in those cases.
// 在异步上下文中,我们有时可以从 AsyncLocalStorage 中解析资源。如果无法解析,我们也可以
// 在非异步上下文中从堆栈获取它们。由于在这两种情况下我们都无法为此调用解析资源,因此我们选
// 择不执行任何操作。我们可以考虑将其作为一个警告,但有时在渲染之外调用函数是有意的(例如,
// 为了预热数据获取),在这些情况下我们不希望发出警告。
previousDispatcher.m(/* preloadModule */ href, options);
return;
}
const resumableState = getResumableState(request);
const renderState = getRenderState(request);
if (href) {
const key = getResourceKey(href);
const as =
options && typeof options.as === 'string' ? options.as : 'script';

let resource;
switch (as) {
case 'script': {
if (resumableState.moduleScriptResources.hasOwnProperty(key)) {
// we can return if we already have this resource
// 如果我们已经有了这个资源,我们可以返回
return;
}
resource = [] as Resource;
resumableState.moduleScriptResources[key] =
options &&
(typeof options.crossOrigin === 'string' ||
typeof options.integrity === 'string')
? [options.crossOrigin, options.integrity]
: PRELOAD_NO_CREDS;
renderState.preloads.moduleScripts.set(key, resource);
break;
}
default: {
const hasAsType =
resumableState.moduleUnknownResources.hasOwnProperty(as);
let resources;
if (hasAsType) {
resources = resumableState.unknownResources[as];
if (resources.hasOwnProperty(key)) {
// we can return if we already have this resource
// 如果我们已经有了这个资源,我们可以返回
return;
}
} else {
resources = {} as ResumableState['moduleUnknownResources']['asType'];
resumableState.moduleUnknownResources[as] = resources;
}
resource = [] as Resource;
resources[key] = PRELOAD_NO_CREDS;
}
}

pushLinkImpl(
resource,
Object.assign(
{
rel: 'modulepreload',
href,
} as PreloadModuleProps,
options,
),
);
renderState.bulkPreloads.add(resource);
// If we got this far we created a new resource
// 如果我们走到这一步,我们创建了一个新的资源
flushResources(request);
}
}

81. 预初始化样式

备注
function preinitStyle(
href: string,
precedence: ?string,
options?: ?PreinitStyleOptions,
): void {
const request = resolveRequest();
if (!request) {
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
// fetching) and we don't want to warn in those cases.
// 在异步上下文中,我们有时可以从 AsyncLocalStorage 中解析资源。如果无法解析,我们也可以
// 在非异步上下文中尝试从堆栈获取资源。由于在这两种情况下我们都无法解析此调用的资源,
// 因此我们选择不做任何操作。我们可以考虑将其作为警告,但有时在渲染之外调用函数是有意的(
// 例如,为了预加载数据),在这些情况下我们不希望发出警告。
previousDispatcher.S(/* preinitStyle */ href, precedence, options);
return;
}
const resumableState = getResumableState(request);
const renderState = getRenderState(request);
if (href) {
precedence = precedence || 'default';
const key = getResourceKey(href);

let styleQueue = renderState.styles.get(precedence);
const hasKey = resumableState.styleResources.hasOwnProperty(key);
const resourceState = hasKey
? resumableState.styleResources[key]
: undefined;
if (resourceState !== EXISTS) {
// We are going to create this resource now so it is marked as Exists
// 我们现在将创建此资源,因此它被标记为已存在
resumableState.styleResources[key] = EXISTS;

// If this is the first time we've encountered this precedence we need
// to create a StyleQueue
// 如果这是我们第一次遇到这个优先级,我们需要创建一个 StyleQueue
if (!styleQueue) {
styleQueue = {
precedence: stringToChunk(escapeTextForBrowser(precedence)),
rules: [] as Array<Chunk | PrecomputedChunk>,
hrefs: [] as Array<Chunk | PrecomputedChunk>,
sheets: new Map() as Map<string, StylesheetResource>,
};
renderState.styles.set(precedence, styleQueue);
}

const resource = {
state: PENDING,
props: Object.assign(
{
rel: 'stylesheet',
href,
'data-precedence': precedence,
} as StylesheetProps,
options,
),
};

if (resourceState) {
// When resourceState is truty it is a Preload state. We cast it for clarity
// 当 resourceState 为真时,它处于预加载状态。我们进行类型转换以便更清楚
const preloadState: Preloaded | PreloadedWithCredentials =
resourceState;
if (preloadState.length === 2) {
adoptPreloadCredentials(resource.props, preloadState);
}

const preloadResource = renderState.preloads.stylesheets.get(key);
if (preloadResource && preloadResource.length > 0) {
// The Preload for this resource was created in this render pass and has not flushed yet so
// we need to clear it to avoid it flushing.
// 此资源的预加载是在此渲染过程中创建的,并且尚未刷新,因此我们需要清除它以避免刷新。
preloadResource.length = 0;
} else {
// Either the preload resource from this render already flushed in this render pass
// or the preload flushed in a prior pass (prerender). In either case we need to mark
// this resource as already having been preloaded.
// 要么是来自此渲染的预加载资源已在此渲染过程中刷新过,要么是之前的渲染过程(预渲
// 染)已经刷新过预加载。在任何情况下,我们都需要标记该资源已被预加载。
resource.state = PRELOADED;
}
} else {
// We don't need to check whether a preloadResource exists in the renderState
// because if it did exist then the resourceState would also exist and we would
// have hit the primary if condition above.
// 我们不需要检查 preloadResource 是否存在于 renderState 中。因为如果它存在,那
// 么 resourceState 也会存在,我们就会在上面的主要 if 条件中被触发。
}

// We add the newly created resource to our StyleQueue and if necessary
// track the resource with the currently rendering boundary
// 我们将新创建的资源添加到我们的 StyleQueue 中,并在必要时使用当前渲染边界跟踪该资源
styleQueue.sheets.set(key, resource);

// Notify the request that there are resources to flush even if no work is currently happening
// 通知请求,即使当前没有工作正在进行,也有资源需要刷新
flushResources(request);
}
}
}

82. 预初始化脚本

备注
function preinitScript(src: string, options?: ?PreinitScriptOptions): void {
const request = resolveRequest();
if (!request) {
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
// fetching) and we don't want to warn in those cases.
// 在异步上下文中,我们有时可以从 AsyncLocalStorage 中解析资源。如果无法解析,我们也可以
// 在非异步上下文中从堆栈获取它们。由于在这两种情况下我们都无法为此调用解析资源,因此我们选
// 择不执行任何操作。我们可以考虑将其作为警告,但有时在渲染之外调用函数是有意的(例如,为数
// 据获取预热),在这些情况下我们不希望发出警告。
previousDispatcher.X(/* preinitScript */ src, options);
return;
}
const resumableState = getResumableState(request);
const renderState = getRenderState(request);
if (src) {
const key = getResourceKey(src);

const hasKey = resumableState.scriptResources.hasOwnProperty(key);
const resourceState = hasKey
? resumableState.scriptResources[key]
: undefined;
if (resourceState !== EXISTS) {
// We are going to create this resource now so it is marked as Exists
// 我们现在将创建此资源,因此它被标记为已存在
resumableState.scriptResources[key] = EXISTS;

const props: ScriptProps = Object.assign(
{
src,
async: true,
} as ScriptProps,
options,
);
if (resourceState) {
// When resourceState is truty it is a Preload state. We cast it for clarity
// 当 resourceState 为 true 时,它是一个预加载状态。我们进行类型转换以便更清楚
const preloadState: Preloaded | PreloadedWithCredentials =
resourceState;
if (preloadState.length === 2) {
adoptPreloadCredentials(props, preloadState);
}

const preloadResource = renderState.preloads.scripts.get(key);
if (preloadResource) {
// the preload resource exists was created in this render. Now that we have
// a script resource which will emit earlier than a preload would if it
// hasn't already flushed we prevent it from flushing by zeroing the length
// 预加载资源是在这个渲染中创建的。现在我们有了一个脚本资源,如果它还没有刷新,它将
// 比预加载资源更早发出。我们通过将长度清零来防止它刷新
preloadResource.length = 0;
}
}

const resource: Resource = [];
// Add to the script flushing queue
// 添加到脚本刷新队列
renderState.scripts.add(resource);
// encode the tag as Chunks
// 将标签编码为块
pushScriptImpl(resource, props);
// Notify the request that there are resources to flush even if no work is currently happening
// 通知请求,即使当前没有工作正在进行,也有资源需要刷新
flushResources(request);
}
return;
}
}

83. 预初始化模块脚本

备注
function preinitModuleScript(
src: string,
options?: ?PreinitModuleScriptOptions,
): void {
const request = resolveRequest();
if (!request) {
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
// fetching) and we don't want to warn in those cases.
// 在异步上下文中,我们有时可以从 AsyncLocalStorage 中解析资源。如果无法解析,我们也可以在非异步上下
// 文中从堆栈获取它们。由于在这两种情况下我们都无法为此调用解析资源,因此我们选择不执行任何操作。我们可以
// 考虑将其作为警告,但有时在渲染之外调用函数是有意的(例如,为数据获取预热),在这些情况下我们不希望发出
// 警告。
previousDispatcher.M(/* preinitModuleScript */ src, options);
return;
}
const resumableState = getResumableState(request);
const renderState = getRenderState(request);
if (src) {
const key = getResourceKey(src);
const hasKey = resumableState.moduleScriptResources.hasOwnProperty(key);
const resourceState = hasKey
? resumableState.moduleScriptResources[key]
: undefined;
if (resourceState !== EXISTS) {
// We are going to create this resource now so it is marked as Exists
// 我们现在将创建此资源,因此它被标记为已存在
resumableState.moduleScriptResources[key] = EXISTS;

const props = Object.assign(
{
src,
type: 'module',
async: true,
} as ModuleScriptProps,
options,
);
if (resourceState) {
// When resourceState is truty it is a Preload state. We cast it for clarity
// 当 resourceState 为真时,它处于预加载状态。我们进行类型转换以便更清楚
const preloadState: Preloaded | PreloadedWithCredentials =
resourceState;
if (preloadState.length === 2) {
adoptPreloadCredentials(props, preloadState);
}

const preloadResource = renderState.preloads.moduleScripts.get(key);
if (preloadResource) {
// the preload resource exists was created in this render. Now that we have
// a script resource which will emit earlier than a preload would if it
// hasn't already flushed we prevent it from flushing by zeroing the length
// 预加载资源是在这个渲染中创建的。现在我们有了一个脚本资源,如果它还没有刷新,它将比预加载资源更
// 早发出。我们通过将长度清零来防止它刷新
preloadResource.length = 0;
}
}

const resource: Resource = [];
// Add to the script flushing queue
// 添加到脚本刷新队列
renderState.scripts.add(resource);
// encode the tag as Chunks
// 将标签编码为块
pushScriptImpl(resource, props);
// Notify the request that there are resources to flush even if no work is currently happening
// 通知请求,即使当前没有工作正在进行,也有资源需要刷新
flushResources(request);
}
return;
}
}

84. 预加载 Bootstrap 脚本或模块

// This function is only safe to call at Request start time since it assumes
// that each module has not already been preloaded. If we find a need to preload
// scripts at any other point in time we will need to check whether the preload
// already exists and not assume it
// 这个函数只有在请求开始时调用才安全,因为它假设每个模块尚未预加载。如果我们发现需要在其他任何时间
// 预加载脚本,我们将需要检查预加载是否已经存在,而不能假设它存在
function preloadBootstrapScriptOrModule(
resumableState: ResumableState,
renderState: RenderState,
href: string,
props: PreloadProps,
): void {
const key = getResourceKey(href);

if (__DEV__) {
if (
resumableState.scriptResources.hasOwnProperty(key) ||
resumableState.moduleScriptResources.hasOwnProperty(key)
) {
// This is coded as a React error because it should be impossible for a userspace preload to preempt this call
// 由于用户空间的预加载不可能抢占此调用,因此这被编码为 React 错误
// If a userspace preload can preempt it then this assumption is broken and we need to reconsider this strategy
// rather than instruct the user to not preload their bootstrap scripts themselves
// 如果用户空间的预加载能够抢占它,那么这个假设就被破坏了,我们需要重新考虑该策略,而不是指示用户不要
// 自己预加载他们的引导脚本
console.error(
'Internal React Error: React expected bootstrap script or module with src "%s" to not have been preloaded already. please file an issue',
href,
);
}
}

// The href used for bootstrap scripts and bootstrap modules should never be
// used to preinit the resource. If a script can be preinited then it shouldn't
// be a bootstrap script/module and if it is a bootstrap script/module then it
// must not be safe to emit early. To avoid possibly allowing for preinits of
// bootstrap scripts/modules we occlude these keys.
// 用于引导脚本和引导模块的 href 永远不应被用于预初始化资源。如果一个脚本可以被预初始化,那么它
// 不应是引导脚本/模块;如果它是引导脚本/模块,那么它绝不能安全地提前发出。为了避免可能允许对引导
// 脚本/模块进行预初始化,我们屏蔽这些键。
resumableState.scriptResources[key] = EXISTS;
resumableState.moduleScriptResources[key] = EXISTS;

const resource: Resource = [];
pushLinkImpl(resource, props);
renderState.bootstrapScripts.add(resource);
}

85. 内部预初始化脚本

function internalPreinitScript(
resumableState: ResumableState,
renderState: RenderState,
src: string,
chunks: Array<Chunk | PrecomputedChunk>,
): void {
const key = getResourceKey(src);
if (!resumableState.scriptResources.hasOwnProperty(key)) {
const resource: Resource = chunks;
resumableState.scriptResources[key] = EXISTS;
renderState.scripts.add(resource);
}
return;
}

86. 从属性预加载样式属性

function preloadAsStylePropsFromProps(href: string, props: any): PreloadProps {
return {
rel: 'preload',
as: 'style',
href: href,
crossOrigin: props.crossOrigin,
fetchPriority: props.fetchPriority,
integrity: props.integrity,
media: props.media,
hrefLang: props.hrefLang,
referrerPolicy: props.referrerPolicy,
};
}

87. 从原始属性来的样式表属性

function stylesheetPropsFromRawProps(rawProps: any): StylesheetProps {
return {
...rawProps,
'data-precedence': rawProps.precedence,
precedence: null,
};
}

88. 采用预加载凭据

function adoptPreloadCredentials(
target: StylesheetProps | ScriptProps | ModuleScriptProps,
preloadState: PreloadedWithCredentials,
): void {
if (target.crossOrigin == null) target.crossOrigin = preloadState[0];
if (target.integrity == null) target.integrity = preloadState[1];
}

89. 获取预取 DNS 作为头

function getPrefetchDNSAsHeader(href: string): string {
const escapedHref = escapeHrefForLinkHeaderURLContext(href);
return `<${escapedHref}>; rel=dns-prefetch`;
}

90. 获取预连接作为头部

function getPreconnectAsHeader(
href: string,
crossOrigin?: ?CrossOriginEnum,
): string {
const escapedHref = escapeHrefForLinkHeaderURLContext(href);
let value = `<${escapedHref}>; rel=preconnect`;
if (typeof crossOrigin === 'string') {
const escapedCrossOrigin = escapeStringForLinkHeaderQuotedParamValueContext(
crossOrigin,
'crossOrigin',
);
value += `; crossorigin="${escapedCrossOrigin}"`;
}
return value;
}

91. 获取预加载作为头部

备注
function getPreloadAsHeader(
href: string,
as: string,
params: ?PreloadImplOptions,
): string {
const escapedHref = escapeHrefForLinkHeaderURLContext(href);
const escapedAs = escapeStringForLinkHeaderQuotedParamValueContext(as, 'as');
let value = `<${escapedHref}>; rel=preload; as="${escapedAs}"`;
for (const paramName in params) {
if (hasOwnProperty.call(params, paramName)) {
const paramValue = params[paramName];
if (typeof paramValue === 'string') {
value += `; ${paramName.toLowerCase()}="${escapeStringForLinkHeaderQuotedParamValueContext(
paramValue,
paramName,
)}"`;
}
}
}
return value;
}

92. 获取样式表预加载作为头部

function getStylesheetPreloadAsHeader(stylesheet: StylesheetResource): string {
const props = stylesheet.props;
const preloadOptions: PreloadImplOptions = {
crossOrigin: props.crossOrigin,
integrity: props.integrity,
nonce: props.nonce,
type: props.type,
fetchPriority: props.fetchPriority,
referrerPolicy: props.referrerPolicy,
media: props.media,
};
return getPreloadAsHeader(props.href, 'style', preloadOptions);
}

93. 为链接头 URL 上下文转义 Href

备注
// This escaping function is only safe to use for href values being written into
// a "Link" header in between `<` and `>` characters. The primary concern with the href is
// to escape the bounding characters as well as new lines. This is unsafe to use in any other
// context
// 此转义函数仅适用于将 href 值写入 `<` 和 `>` 字符之间的 "Link" 头部。href 的主要关注点是转义边界
// 字符以及换行符。在任何其他上下文中使用都是不安全的

// 用于 Link 头 URL 上下文的 href 正则表达式
const regexForHrefInLinkHeaderURLContext = /[<>\r\n]/g;

function escapeHrefForLinkHeaderURLContext(hrefInput: string): string {
if (__DEV__) {
checkAttributeStringCoercion(hrefInput, 'href');
}
const coercedHref = '' + hrefInput;
return coercedHref.replace(
regexForHrefInLinkHeaderURLContext,
escapeHrefForLinkHeaderURLContextReplacer,
);
}

94. 为链接头 URL 上下文替换器转义 Href

function escapeHrefForLinkHeaderURLContextReplacer(match: string): string {
switch (match) {
case '<':
return '%3C';
case '>':
return '%3E';
case '\n':
return '%0A';
case '\r':
return '%0D';
default: {
throw new Error(
'escapeLinkHrefForHeaderContextReplacer encountered a match it does not know how to replace. this means the match regex and the replacement characters are no longer in sync. This is a bug in React',
);
}
}
}

95. 为链接头引用参数值上下文转义字符串

备注
// This escaping function is only safe to use for quoted param values in an HTTP header.
// 该转义函数仅在 HTTP 头部的带引号参数值中使用时是安全的。
// It is unsafe to use for any value not inside quote marks in parater value position.
// 在参数值位置中,任何不在引号内的值使用该函数都是不安全的。

// 用于链接头引用参数值的正则表达式上下文
const regexForLinkHeaderQuotedParamValueContext = /["';,\r\n]/g;

function escapeStringForLinkHeaderQuotedParamValueContext(
value: string,
name: string,
): string {
if (__DEV__) {
checkOptionStringCoercion(value, name);
}
const coerced = '' + value;
return coerced.replace(
regexForLinkHeaderQuotedParamValueContext,
escapeStringForLinkHeaderQuotedParamValueContextReplacer,
);
}

96. 用于链接头引用参数值上下文的转义字符串替换器

function escapeStringForLinkHeaderQuotedParamValueContextReplacer(
match: string,
): string {
switch (match) {
case '"':
return '%22';
case "'":
return '%27';
case ';':
return '%3B';
case ',':
return '%2C';
case '\n':
return '%0A';
case '\r':
return '%0D';
default: {
throw new Error(
'escapeStringForLinkHeaderQuotedParamValueContextReplacer encountered a match it does not know how to replace. this means the match regex and the replacement characters are no longer in sync. This is a bug in React',
);
}
}
}

97. 提升样式队列依赖

function hoistStyleQueueDependency(
this: HoistableState,
styleQueue: StyleQueue,
) {
this.styles.add(styleQueue);
}

98. 提升样式表依赖

function hoistStylesheetDependency(
this: HoistableState,
stylesheet: StylesheetResource,
) {
this.stylesheets.add(stylesheet);
}

伍三、类型

1. 一次性选项

type NonceOption =
| string
| {
script?: string;
style?: string;
};

2. 存在

// 存在
type Exists = null;
// 预加载
type Preloaded = [];
// Credentials here are things that affect whether a browser will make a request
// as well as things that affect which connection the browser will use for that request.
// 凭证在这里指的是影响浏览器是否会发出请求的因素以及影响浏览器为该请求使用哪种连接的因素。
// We want these to be aligned across preloads and resources because otherwise the preload
// will be wasted.
// 我们希望这些在预加载和资源之间保持一致,否则预加载将被浪费。
// We investigated whether referrerPolicy should be included here but from experimentation
// it seems that browsers do not treat this as part of the http cache key and does not affect
// which connection is used.
// 我们调查了是否应该在这里包含 referrerPolicy,但从实验来看浏览器似乎不将其视为 HTTP 缓存键的一部分,也不
// 影响使用哪种连接。
type PreloadedWithCredentials = [
?/* crossOrigin */ CrossOriginEnum,
?/* integrity */ string,
];

3. 插入模式

type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;

4. 视图过渡上下文

type ViewTransitionContext = {
update: 'none' | 'auto' | string;
enter: 'none' | 'auto' | string;
exit: 'none' | 'auto' | string;
share: 'none' | 'auto' | string;
name: 'auto' | string;
// 如果没有明确定义名称,可使用的名称。
autoName: string; // a name that can be used if an explicit one is not defined.
// 追踪我们已经发出的此名称的重复数量。
nameIdx: number; // keeps track of how many duplicates of this name we've emitted.
};

5. 推送视图过渡属性

备注
function pushViewTransitionAttributes(
target: Array<Chunk | PrecomputedChunk>,
formatContext: FormatContext,
): void {
if (!enableViewTransition) {
return;
}
const viewTransition = formatContext.viewTransition;
if (viewTransition === null) {
return;
}
if (viewTransition.name !== 'auto') {
pushStringAttribute(
target,
'vt-name',
viewTransition.nameIdx === 0
? viewTransition.name
: viewTransition.name + '_' + viewTransition.nameIdx,
);
// Increment the index in case we have multiple children to the same ViewTransition.
// 如果我们对同一个 ViewTransition 有多个子项,则增加索引。
// Because this is a side-effect in render, we should ideally call pushViewTransitionAttributes
// after we've suspended (like forms do), so that we don't increment each attempt.
// 因为这是渲染中的副作用,我们理想情况下应该在挂起之后调用 pushViewTransitionAttributes(就像表单
// 那样),这样我们就不会在每次尝试时都增加索引。
// TODO: Make this deterministic.
// TODO:使其具有确定性。
viewTransition.nameIdx++;
}
pushStringAttribute(target, 'vt-update', viewTransition.update);
if (viewTransition.enter !== 'none') {
pushStringAttribute(target, 'vt-enter', viewTransition.enter);
}
if (viewTransition.exit !== 'none') {
pushStringAttribute(target, 'vt-exit', viewTransition.exit);
}
if (viewTransition.share !== 'none') {
pushStringAttribute(target, 'vt-share', viewTransition.share);
}
}

6. 样式表状态

/**
* Resources
*/

type StylesheetState = 0 | 1 | 2 | 3;

7. 预连接属性

// 预连接属性
type PreconnectProps = {
rel: 'preconnect' | 'dns-prefetch';
href: string;
[string]: mixed;
};

// 作为属性预加载
type PreloadAsProps = {
rel: 'preload';
as: string;
href: ?string;
[string]: ?string;
};
// 预加载模块属性
type PreloadModuleProps = {
rel: 'modulepreload';
href: ?string;
[string]: ?string;
};
// 预加载属性
type PreloadProps = PreloadAsProps | PreloadModuleProps;

// 脚本属性
type ScriptProps = {
async: true;
src: string;
crossOrigin?: ?CrossOriginEnum;
[string]: mixed;
};
// 模块脚本属性
type ModuleScriptProps = {
async: true;
src: string;
type: 'module';
crossOrigin?: ?CrossOriginEnum;
[string]: mixed;
};

8. 样式表属性

// 样式表属性
type StylesheetProps = {
rel: 'stylesheet';
href: string;
'data-precedence': string;
crossOrigin?: ?CrossOriginEnum;
integrity?: ?string;
nonce?: ?string;
type?: ?string;
fetchPriority?: ?string;
referrerPolicy?: ?string;
media?: ?string;
[string]: mixed;
};
// 样式表资源
type StylesheetResource = {
props: StylesheetProps;
state: StylesheetState;
};