跳到主要内容

React DOM fizz instruction set shared

内联脚本和外部运行时指令集之间的共享实现和常量。

一、作用

二、显示已完成的边界

// TODO: Symbols that are referenced outside this module use dynamic accessor
// notation instead of dot notation to prevent Closure's advanced compilation
// mode from renaming. We could use extern files instead, but I couldn't get it
// working. Closure converts it to a dot access anyway, though, so it's not an
// urgent issue.
// TODO:在此模块外使用的符号使用动态访问符表示法,而不是点表示法,以防止 Closure 的高级编译模式重命名。我们可以使用 extern 文件,但我没能让它工作。不过,无论如何 Closure 会将其转换为点访问符,所以这不是紧急问题。

export function revealCompletedBoundaries(batch) {
window['$RT'] = performance.now();
for (let i = 0; i < batch.length; i += 2) {
const suspenseIdNode = batch[i];
const contentNode = batch[i + 1];
if (contentNode.parentNode === null) {
// If the client has failed hydration we may have already deleted the streaming
// segments. The server may also have emitted a complete instruction but cancelled
// the segment. Regardless we can ignore this case.
// 如果客户端的水合失败,我们可能已经删除了流式段。服务器也可能已经发出了完整的指令但取消了该
// 段。不管怎样,我们可以忽略这种情况。
} else {
// We can detach the content now.
// Completions of boundaries within this contentNode will now find the boundary
// in its designated place.
// 我们现在可以分离内容。在此 contentNode 内的边界完成现在将会在其指定位置找到边界。
contentNode.parentNode.removeChild(contentNode);
}
// Clear all the existing children. This is complicated because
// there can be embedded Suspense boundaries in the fallback.
// This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.
// 清除所有现有的子元素。这很复杂,因为在回退中可能存在嵌套的 Suspense 边界。这类似于
// ReactFiberConfigDOM 中的 clearSuspenseBoundary。
// TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
// TODO:如果我们从不在回退树中发出 suspense 边界,就可以避免这样做。
// They never hydrate anyway. However, currently we support incrementally loading the fallback.
// 反正它们从不会被水合。然而,目前我们支持增量加载回退。
const parentInstance = suspenseIdNode.parentNode;
if (!parentInstance) {
// We may have client-rendered this boundary already. Skip it.
// 我们可能已经对这个边界进行了客户端渲染。跳过它。
continue;
}

// Find the boundary around the fallback. This is always the previous node.
// 查找回退的边界。这总是前一个节点。
const suspenseNode = suspenseIdNode.previousSibling;

let node = suspenseIdNode;
let depth = 0;
do {
if (node && node.nodeType === COMMENT_NODE) {
const data = node.data;
if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
if (depth === 0) {
break;
} else {
depth--;
}
} else if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_QUEUED_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA ||
data === ACTIVITY_START_DATA
) {
depth++;
}
}

const nextNode = node.nextSibling;
parentInstance.removeChild(node);
node = nextNode;
} while (node);

const endOfBoundary = node;

// Insert all the children from the contentNode between the start and end of suspense boundary.
// 将 contentNode 的所有子节点插入到 suspense 边界的开始和结束之间。
while (contentNode.firstChild) {
parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
}

suspenseNode.data = SUSPENSE_START_DATA;
if (suspenseNode['_reactRetry']) {
requestAnimationFrame(suspenseNode['_reactRetry']);
}
}
batch.length = 0;
}

三、使用视图过渡显示已完成的边界

export function revealCompletedBoundariesWithViewTransitions(
revealBoundaries,
batch,
) {
let shouldStartViewTransition = false;
let autoNameIdx = 0;
const restoreQueue = [];
function applyViewTransitionName(element, classAttributeName) {
const className = element.getAttribute(classAttributeName);
if (!className) {
return;
}
// Add any elements we apply a name to a queue to be reverted when we start.
// 添加我们为队列应用名称的任何元素,以便在开始时恢复。
const elementStyle = element.style;
restoreQueue.push(
element,
elementStyle['viewTransitionName'],
elementStyle['viewTransitionClass'],
);
if (className !== 'auto') {
elementStyle['viewTransitionClass'] = className;
}
let name = element.getAttribute('vt-name');
if (!name) {
// Auto-generate a name for this one.
// 自动为这个生成一个名称。
// TODO: We don't have a prefix to pick from here but maybe we don't need it
// since it's only applicable temporarily during this specific animation.
// 待办事项:我们这里没有可以选择的前缀,但也许我们不需要它。因为它仅在这个特定动画期间临时适用
const idPrefix = '';
name = '_' + idPrefix + 'T_' + autoNameIdx++ + '_';
}
// If the name isn't valid CSS identifier, base64 encode the name instead.
// 如果名称不是有效的 CSS 标识符,则改为对名称进行 base64 编码。
// This doesn't let you select it in custom CSS selectors but it does work in current
// browsers.
// 这不会让你在自定义 CSS 选择器中选择它,但在当前浏览器中是有效的。
const escapedName =
CSS.escape(name) !== name ? 'r-' + btoa(name).replace(/=/g, '') : name;
elementStyle['viewTransitionName'] = escapedName;
shouldStartViewTransition = true;
}
try {
const existingTransition = document['__reactViewTransition'];
if (existingTransition) {
// Retry after the previous ViewTransition finishes.
// 在上一次视图过渡完成后重试。
existingTransition.finished.finally(window['$RV'].bind(null, batch));
return;
}
// First collect all entering names that might form pairs exiting names.
// 首先收集所有可能形成配对的进入名称和退出名称。
const appearingViewTransitions = new Map();
for (let i = 1; i < batch.length; i += 2) {
const contentNode = batch[i];
const appearingElements = contentNode.querySelectorAll('[vt-share]');
for (let j = 0; j < appearingElements.length; j++) {
const appearingElement = appearingElements[j];
appearingViewTransitions.set(
appearingElement.getAttribute('vt-name'),
appearingElement,
);
}
}
const suspenseyImages = [];
// Next we'll find the nodes that we're going to animate and apply names to them..
// 接下来我们将找到要动画的节点并为它们命名..
for (let i = 0; i < batch.length; i += 2) {
const suspenseIdNode = batch[i];
const parentInstance = suspenseIdNode.parentNode;
if (!parentInstance) {
// We may have client-rendered this boundary already. Skip it.
// 我们可能已经对这个边界进行了客户端渲染。跳过它。
continue;
}
const parentRect = parentInstance.getBoundingClientRect();
if (
!parentRect.left &&
!parentRect.top &&
!parentRect.width &&
!parentRect.height
) {
// If the parent instance is display: none then we don't animate this boundary.
// 如果父实例是 display: none,那么我们就不对这个边界执行动画。
// This can happen when this boundary is actually a child of a different boundary that
// isn't yet revealed or is about to be revealed, but in that case that boundary
// should do the exit/enter and not this one. Conveniently this also lets us skip
// this if it's just in a hidden tree in general.
// 当这个边界实际上是另一个边界的子元素,而那个边界尚未显示或即将显示时,可能会发生这种情
// 况,但在这种情况下,退出/进入动画应该由那个边界处理,而不是这个边界。方便的是,如果它只
// 是一般地处于隐藏的树中,我们也可以跳过它。
// TODO: Should we skip it if it's out of viewport? It's possible that it gets
// brought into the viewport by changing size.
// TODO: 如果它在视口之外,我们是否应该跳过?可能会通过改变大小将其带入视口。
// TODO: There's a another case where an inner boundary is inside a fallback that
// is about to be deleted. In that case we should not run exit animations on the inner.
// TODO: 还有一种情况是内部边界位于即将被删除的备用内容中。在这种情况下,我们不应该对内部
// 执行退出动画。
continue;
}

// Apply exit animations to the immediate elements inside the fallback.
// 将退出动画应用到回退内容中的直接元素。
let node = suspenseIdNode;
let depth = 0;
while (node) {
if (node.nodeType === COMMENT_NODE) {
const data = node.data;
if (data === SUSPENSE_END_DATA) {
if (depth === 0) {
break;
} else {
depth--;
}
} else if (
data === SUSPENSE_START_DATA ||
data === SUSPENSE_PENDING_START_DATA ||
data === SUSPENSE_QUEUED_START_DATA ||
data === SUSPENSE_FALLBACK_START_DATA
) {
depth++;
}
} else if (node.nodeType === ELEMENT_NODE) {
const exitElement = node;
const exitName = exitElement.getAttribute('vt-name');
const pairedElement = appearingViewTransitions.get(exitName);
applyViewTransitionName(
exitElement,
pairedElement ? 'vt-share' : 'vt-exit',
);
if (pairedElement) {
// Activate the other side as well.
// 也激活另一侧。
applyViewTransitionName(pairedElement, 'vt-share');
// 标记已声明
appearingViewTransitions.set(exitName, null); // mark claimed
}
// Next we'll look inside this element for pairs to trigger "share".
// 接下来我们将查看此元素内部的成对项以触发“分享”。
const disappearingElements =
exitElement.querySelectorAll('[vt-share]');
for (let j = 0; j < disappearingElements.length; j++) {
const disappearingElement = disappearingElements[j];
const name = disappearingElement.getAttribute('vt-name');
const appearingElement = appearingViewTransitions.get(name);
if (appearingElement) {
applyViewTransitionName(disappearingElement, 'vt-share');
applyViewTransitionName(appearingElement, 'vt-share');
// 标记已声明
appearingViewTransitions.set(name, null); // mark claimed
}
}
}
node = node.nextSibling;
}

// Apply enter animations to the new nodes about to be inserted.
// 对即将插入的新节点应用进入动画。
const contentNode = batch[i + 1];
let enterElement = contentNode.firstElementChild;
while (enterElement) {
const paired =
appearingViewTransitions.get(enterElement.getAttribute('vt-name')) ===
null;
if (!paired) {
applyViewTransitionName(enterElement, 'vt-enter');
}
enterElement = enterElement.nextElementSibling;
}

// Apply update animations to any parents and siblings that might be affected.
// 对可能受到影响的任何父元素和兄弟元素应用更新动画。
let ancestorElement = parentInstance;
do {
let childElement = ancestorElement.firstElementChild;
while (childElement) {
// TODO: Bail out if we can
// TODO: 如果可以的话就退出
const updateClassName = childElement.getAttribute('vt-update');
if (
updateClassName &&
updateClassName !== 'none' &&
!restoreQueue.includes(childElement)
) {
// If we have already handled this element as part of another exit/enter/share, don't override.
// 如果我们已经在处理另一个退出/进入/共享时处理了这个元素,就不要覆盖。
applyViewTransitionName(childElement, 'vt-update');
}
childElement = childElement.nextElementSibling;
}
} while (
(ancestorElement = ancestorElement.parentNode) &&
ancestorElement.nodeType === ELEMENT_NODE &&
ancestorElement.getAttribute('vt-update') !== 'none'
);

// Find the appearing Suspensey Images inside the new content.
// 在新内容中查找出现的挂起图片。
const appearingImages = contentNode.querySelectorAll(
'img[src]:not([loading="lazy"])',
);
// TODO: Consider marking shouldStartViewTransition if we found any images.
// TODO:如果我们发现任何图像,考虑标记 shouldStartViewTransition。
// But only once we can disable the root animation for that case.
// 但只有在我们可以为该情况禁用根动画时才执行一次。
suspenseyImages.push.apply(suspenseyImages, appearingImages);
}
if (shouldStartViewTransition) {
const transition = (document['__reactViewTransition'] = document[
'startViewTransition'
]({
update: () => {
revealBoundaries(batch);
const blockingPromises = [
// Force layout to trigger font loading, we stash the actual value to trick minifiers.
// 强制布局以触发字体加载,我们存储实际值以欺骗压缩工具。
document.documentElement.clientHeight,
// Block on fonts finishing loading before revealing these boundaries.
// 阻塞直到字体加载完毕再显示这些边界。
document.fonts.ready,
];
for (let i = 0; i < suspenseyImages.length; i++) {
const suspenseyImage = suspenseyImages[i];
if (!suspenseyImage.complete) {
const rect = suspenseyImage.getBoundingClientRect();
const inViewport =
rect.bottom > 0 &&
rect.right > 0 &&
rect.top < window.innerHeight &&
rect.left < window.innerWidth;
if (inViewport) {
// TODO: Use decode() instead of the load event here once the fix in
// TODO: 一旦修复,在这里使用 decode() 而不是 load 事件
// https://issues.chromium.org/issues/420748301 has propagated fully.
const loadingImage = new Promise(resolve => {
suspenseyImage.addEventListener('load', resolve);
suspenseyImage.addEventListener('error', resolve);
});
blockingPromises.push(loadingImage);
}
}
}
return Promise.race([
Promise.all(blockingPromises),
new Promise(resolve => {
const currentTime = performance.now();
const msUntilTimeout =
// If the throttle would make us miss the target metric, then shorten the throttle.
// performance.now()'s zero value is assumed to be the start time of the metric.
// 如果节流会让我们错过目标指标,那么缩短节流。
// 假设 performance.now() 的零值是指标的开始时间。
currentTime < TARGET_VANITY_METRIC &&
currentTime > TARGET_VANITY_METRIC - FALLBACK_THROTTLE_MS
? TARGET_VANITY_METRIC - currentTime
: // Otherwise it's throttled starting from last commit time.
// 否则它会从上一次提交时间开始被限流。
SUSPENSEY_FONT_AND_IMAGE_TIMEOUT;
setTimeout(resolve, msUntilTimeout);
}),
]);
},
// 待办事项:为 Suspense reveals 添加一个硬编码类型。
types: [], // TODO: Add a hard coded type for Suspense reveals.
}));
transition.ready.finally(() => {
// Restore all the names/classes that we applied to what they were before.
// 恢复我们应用到它们上的所有名称/类,回到之前的状态。
// We do it in reverse order in case there were duplicates so the first one wins.
// 我们按相反的顺序进行,以防有重复项,这样第一个就会胜出。
for (let i = restoreQueue.length - 3; i >= 0; i -= 3) {
const element = restoreQueue[i];
const elementStyle = element.style;
const previousName = restoreQueue[i + 1];
elementStyle['viewTransitionName'] = previousName;
const previousClassName = restoreQueue[i + 1];
elementStyle['viewTransitionClass'] = previousClassName;
if (element.getAttribute('style') === '') {
element.removeAttribute('style');
}
}
});
transition.finished.finally(() => {
if (document['__reactViewTransition'] === transition) {
document['__reactViewTransition'] = null;
}
});
// Queue any future completions into its own batch since they won't have been
// snapshotted by this one.
// 将任何未来的完成加入它自己的批次,因为它们不会被这个批次快照。
window['$RB'] = [];
return;
}
// Fall through to reveal.
// 跌落以显示。
} catch (x) {
// Fall through to reveal.
// 跌落以显示。
}
// ViewTransitions v2 not supported or no ViewTransitions found. Reveal immediately.
// ViewTransitions v2 不支持或未找到 ViewTransitions。立即显示。
revealBoundaries(batch);
}

四、客户端渲染边界

export function clientRenderBoundary(
suspenseBoundaryID,
errorDigest,
errorMsg,
errorStack,
errorComponentStack,
) {
// Find the fallback's first element.
// 找到备用方案的第一个元素。
const suspenseIdNode = document.getElementById(suspenseBoundaryID);
if (!suspenseIdNode) {
// The user must have already navigated away from this tree.
// E.g. because the parent was hydrated.
// 用户必须已经离开了这棵树。例如,因为父组件已被水合(hydrated)。
return;
}
// Find the boundary around the fallback. This is always the previous node.
// 查找回退的边界。这总是前一个节点。
const suspenseNode = suspenseIdNode.previousSibling;
// Tag it to be client rendered.
// 将其标记为客户端渲染。
suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
// assign error metadata to first sibling
// 将错误元数据分配给第一个兄弟节点
const dataset = suspenseIdNode.dataset;
if (errorDigest) dataset['dgst'] = errorDigest;
if (errorMsg) dataset['msg'] = errorMsg;
if (errorStack) dataset['stck'] = errorStack;
if (errorComponentStack) dataset['cstck'] = errorComponentStack;
// Tell React to retry it if the parent already hydrated.
// 如果父组件已经完成 hydration,告诉 React 重试它。
if (suspenseNode['_reactRetry']) {
suspenseNode['_reactRetry']();
}
}

五、完成边界

export function completeBoundary(suspenseBoundaryID, contentID) {
const contentNodeOuter = document.getElementById(contentID);
if (!contentNodeOuter) {
// If the client has failed hydration we may have already deleted the streaming
// segments. The server may also have emitted a complete instruction but cancelled
// the segment. Regardless we can ignore this case.
// 如果客户端的水合失败,我们可能已经删除了流式段。服务器也可能已经发出了完整的指令但取消了该
// 段。不管怎样,我们可以忽略这种情况。
return;
}

// Find the fallback's first element.
// 找到备用方案的第一个元素。
const suspenseIdNodeOuter = document.getElementById(suspenseBoundaryID);
if (!suspenseIdNodeOuter) {
// We'll never reveal this boundary so we can remove its content immediately.
// Otherwise we'll leave it in until we reveal it.
// This is important in case this specific boundary contains other boundaries
// that may get completed before we reveal this one.
// 我们永远不会透露这个边界,所以我们可以立即删除其内容。否则我们会一直保留它,直到我们揭示它。
// 这一点很重要,因为这个特定的边界可能包含其他边界。这些边界可能在我们揭示这个边界之前就已经完成。
contentNodeOuter.parentNode.removeChild(contentNodeOuter);

// The user must have already navigated away from this tree.
// E.g. because the parent was hydrated. That's fine there's nothing to do
// but we have to make sure that we already deleted the container node.
// 用户必须已经离开了这棵树。例如,因为父节点已被水合(hydrated)。没关系,没有什么需要做的,
// 但我们必须确保已经删除了容器节点。
return;
}

// Mark this Suspense boundary as queued so we know not to client render it
// at the end of document load.
// 将此挂起边界标记为已排队,以便我们知道在文档加载结束时不进行客户端渲染。
const suspenseNodeOuter = suspenseIdNodeOuter.previousSibling;
suspenseNodeOuter.data = SUSPENSE_QUEUED_START_DATA;
// Queue this boundary for the next batch
// 将此边界排入下一批处理中
window['$RB'].push(suspenseIdNodeOuter, contentNodeOuter);

if (window['$RB'].length === 2) {
// This is the first time we've pushed to the batch. We need to schedule a callback
// to flush the batch. This is delayed by the throttle heuristic.
// 这是我们第一次推送到批处理。我们需要安排一个回调来刷新批处理。这会因节流启发式而延迟。
if (typeof window['$RT'] !== 'number') {
// If we haven't had our rAF callback yet, schedule everything for the first paint.
// 如果我们还没有进行 rAF 回调,请将所有内容安排在第一次绘制时。
requestAnimationFrame(window['$RV'].bind(null, window['$RB']));
} else {
const currentTime = performance.now();
const msUntilTimeout =
// If the throttle would make us miss the target metric, then shorten the throttle.
// performance.now()'s zero value is assumed to be the start time of the metric.
// 如果节流会让我们错过目标指标,则缩短节流。假设 performance.now() 的零值是指标的开始时间。
currentTime < TARGET_VANITY_METRIC &&
currentTime > TARGET_VANITY_METRIC - FALLBACK_THROTTLE_MS
? TARGET_VANITY_METRIC - currentTime
: // Otherwise it's throttled starting from last commit time.
// 否则它会从上一次提交时间开始被限流。
window['$RT'] + FALLBACK_THROTTLE_MS - currentTime;
// We always schedule the flush in a timer even if it's very low or negative to allow
// for multiple completeBoundary calls that are already queued to have a chance to
// make the batch.
// 我们总是将刷新安排在计时器中,即使它非常低或为负数,也要允许已经排队的多个
// completeBoundary 调用有机会创建批次。
setTimeout(window['$RV'].bind(null, window['$RB']), msUntilTimeout);
}
}
}

六、完成带样式的边界

export function completeBoundaryWithStyles(
suspenseBoundaryID,
contentID,
stylesheetDescriptors,
) {
const precedences = new Map();
const thisDocument = document;
let lastResource, node;

// Seed the precedence list with existing resources and collect hoistable style tags
// 使用现有资源初始化优先级列表并收集可提升的样式标签
const nodes = thisDocument.querySelectorAll(
'link[data-precedence],style[data-precedence]',
);
const styleTagsToHoist = [];
for (let i = 0; (node = nodes[i++]); ) {
if (node.getAttribute('media') === 'not all') {
styleTagsToHoist.push(node);
} else {
if (node.tagName === 'LINK') {
window['$RM'].set(node.getAttribute('href'), node);
}
precedences.set(node.dataset['precedence'], (lastResource = node));
}
}

let i = 0;
const dependencies = [];
let href, precedence, attr, loadingState, resourceEl, media;

function cleanupWith(cb) {
this['_p'] = null;
cb();
}

// Sheets Mode
// 工作表模式
let sheetMode = true;
while (true) {
if (sheetMode) {
// Sheet Mode iterates over the stylesheet arguments and constructs them if new or checks them for
// dependency if they already existed
// 工作表模式遍历样式表参数,并在参数是新的情况下构建它们,或在已存在时检查它们的依赖关系
const stylesheetDescriptor = stylesheetDescriptors[i++];
if (!stylesheetDescriptor) {
// enter <style> Mode
// 进入 <style> 模式
sheetMode = false;
i = 0;
continue;
}

let avoidInsert = false;
let j = 0;
href = stylesheetDescriptor[j++];

if ((resourceEl = window['$RM'].get(href))) {
// We have an already inserted stylesheet.
// 我们已经插入了一个样式表。
loadingState = resourceEl['_p'];
avoidInsert = true;
} else {
// We haven't already processed this href so we need to construct a stylesheet and hoist it
// 我们还没有处理过这个 href,所以我们需要构建一个样式表并提升它
// We construct it here and attach a loadingState. We also check whether it matches
// media before we include it in the dependency array.
// 我们在这里构建它并附加一个 loadingState。我们还会在将其包括在依赖数组中之前检查它是否
// 匹配 media。
resourceEl = thisDocument.createElement('link');
resourceEl.href = href;
resourceEl.rel = 'stylesheet';
resourceEl.dataset['precedence'] = precedence =
stylesheetDescriptor[j++];
while ((attr = stylesheetDescriptor[j++])) {
resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
}
loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {
resourceEl.onload = cleanupWith.bind(resourceEl, resolve);
resourceEl.onerror = cleanupWith.bind(resourceEl, reject);
});
// Save this resource element so we can bailout if it is used again
// 保存此资源元素,以便如果再次使用可以退出
window['$RM'].set(href, resourceEl);
}
media = resourceEl.getAttribute('media');
if (loadingState && (!media || window['matchMedia'](media).matches)) {
dependencies.push(loadingState);
}
if (avoidInsert) {
// We have a link that is already in the document. We don't want to fall through to the insert path
// 我们已经在文档中有一个链接。我们不想掉到插入路径
continue;
}
} else {
// <style> mode iterates over not-yet-hoisted <style> tags with data-precedence and hoists them.
// <style> 模式遍历尚未提升的具有 data-precedence 的 <style> 标签并将其提升。
resourceEl = styleTagsToHoist[i++];
if (!resourceEl) {
// we are done with all style tags
// 我们已经处理完所有样式标签
break;
}

precedence = resourceEl.getAttribute('data-precedence');
resourceEl.removeAttribute('media');
}

// resourceEl is either a newly constructed <link rel="stylesheet" ...> or a <style> tag requiring hoisting
// resourceEl 要么是新构建的 <link rel="stylesheet" ...>,要么是需要提升的 <style> 标签
const prior = precedences.get(precedence) || lastResource;
if (prior === lastResource) {
lastResource = resourceEl;
}
precedences.set(precedence, resourceEl);

// Finally, we insert the newly constructed instance at an appropriate location
// in the Document.
// 最后,我们将新构建的实例插入到文档中的适当位置
if (prior) {
prior.parentNode.insertBefore(resourceEl, prior.nextSibling);
} else {
const head = thisDocument.head;
head.insertBefore(resourceEl, head.firstChild);
}
}

const suspenseIdNodeOuter = document.getElementById(suspenseBoundaryID);
if (suspenseIdNodeOuter) {
// Mark this Suspense boundary as queued so we know not to client render it
// at the end of document load.
// 将此挂起边界标记为已排队,以便我们知道在文档加载结束时不进行客户端渲染。
const suspenseNodeOuter = suspenseIdNodeOuter.previousSibling;
suspenseNodeOuter.data = SUSPENSE_QUEUED_START_DATA;
}

Promise.all(dependencies).then(
window['$RC'].bind(null, suspenseBoundaryID, contentID),
window['$RX'].bind(null, suspenseBoundaryID, 'CSS failed to load'),
);
}

七、完成段落

export function completeSegment(containerID, placeholderID) {
const segmentContainer = document.getElementById(containerID);
const placeholderNode = document.getElementById(placeholderID);
// We always expect both nodes to exist here because, while we might
// have navigated away from the main tree, we still expect the detached
// tree to exist.
// 我们总是期望这里的两个节点都存在,因为虽然我们可能已经离开了主树,我们仍然期望分离的树存在。
segmentContainer.parentNode.removeChild(segmentContainer);
while (segmentContainer.firstChild) {
placeholderNode.parentNode.insertBefore(
segmentContainer.firstChild,
placeholderNode,
);
}
placeholderNode.parentNode.removeChild(placeholderNode);
}

八、监听表单提交以重放

export function listenToFormSubmissionsForReplaying() {
// A global replay queue ensures actions are replayed in order.
// This event listener should be above the React one. That way when
// we preventDefault in React's handling we also prevent this event
// from queing it. Since React listens to the root and the top most
// container you can use is the document, the window is fine.
// 全局重放队列确保操作按顺序重放。
// 这个事件监听器应放在 React 的监听器之上。这样,当
// 我们在 React 的处理过程中调用 preventDefault 时,也会阻止该事件
// 被加入队列。由于 React 监听根节点,你可以使用的最顶层容器
// 是 document,window 也可以。
addEventListener('submit', event => {
if (event.defaultPrevented) {
// We let earlier events to prevent the action from submitting.
// 我们让早期事件阻止该操作提交。
return;
}
const form = event.target;
const submitter = event['submitter'];
let action = form.action;
let formDataSubmitter = submitter;
if (submitter) {
const submitterAction = submitter.getAttribute('formAction');
if (submitterAction != null) {
// The submitter overrides the action.
// 提交者覆盖了操作。
action = submitterAction;
// If the submitter overrides the action, and it passes the test below,
// that means that it was a function action which conceptually has no name.
// Therefore, we exclude the submitter from the formdata.
// 如果提交者覆盖了操作,并且通过了下面的测试,那意味着这是一个在概念上没有名字的函数操
// 作。因此,我们将提交者从表单数据中排除。
formDataSubmitter = null;
}
}
if (action !== EXPECTED_FORM_ACTION_URL) {
// The form is a regular form action, we can bail.
// 该表单是一个常规表单操作,我们可以放弃。
return;
}

// Prevent native navigation.
// 阻止本地导航。
// This will also prevent other React's on the same page from listening.
// 这也会阻止页面上其他 React 组件的监听。
event.preventDefault();

// Take a snapshot of the FormData at the time of the event.
// 在事件发生时获取表单数据的快照。
const formData = new FormData(form, formDataSubmitter);

// Queue for replaying later. This field could potentially be shared with multiple
// Reacts on the same page since each one will preventDefault for the next one.
// 用于稍后重放的队列。此字段可能会与同一页面上的多个 React 共享,因为每个 React 都会对下一
// 个调用 preventDefault。
// This means that this protocol is shared with any React version that shares the same
// javascript: URL placeholder value. So we might not be the first to declare it.
// 这意味着该协议会与任何使用相同 javascript: URL 占位符值的 React 版本共享。因此我们可能
// 不是第一个声明它的。
// We attach it to the form's root node, which is the shared environment context
// where we preserve sequencing and where we'll pick it up from during hydration.
// If there's no ownerDocument, then this is the document.
// 我们将它附加到表单的根节点,这是共享的环境上下文,我们在其中保持顺序,并将在 hydration 期
// 间从中读取。如果没有 ownerDocument,那么这就是文档。
const root = form.ownerDocument || form;
(root['$$reactFormReplay'] = root['$$reactFormReplay'] || []).push(
form,
submitter,
formData,
);
});
}

九、常量

1. 元素节点

备注

源码中 6 - 24 行

// 元素节点
const ELEMENT_NODE = 1;
// 注释节点
const COMMENT_NODE = 8;
// 活动开始数据
const ACTIVITY_START_DATA = '&';
// 活动结束数据
const ACTIVITY_END_DATA = '/&';
// 挂起开始数据
const SUSPENSE_START_DATA = '$';
// 挂起结束数据
const SUSPENSE_END_DATA = '/$';
// 挂起未决的启动数据
const SUSPENSE_PENDING_START_DATA = '$?';
// 挂起排队开始数据
const SUSPENSE_QUEUED_START_DATA = '$~';
// 挂起回退开始数据
const SUSPENSE_FALLBACK_START_DATA = '$!';

// 回退节流毫秒
const FALLBACK_THROTTLE_MS = 300;

// 挂起字体和图像超时
const SUSPENSEY_FONT_AND_IMAGE_TIMEOUT = 500;

// If you have a target goal in mind for a metric to hit, you don't want the
// only reason you miss it by a little bit to be throttling heuristics.
// This tries to avoid throttling if avoiding it would let you hit this metric.
// This is derived from trying to hit an LCP of 2.5 seconds with some head room.
// 如果你有一个目标指标要达到,你不会希望仅仅因为限流启发式而稍微达不到。这个尝试在避免限流的情况
// 下,如果避免它可以让你达到这个指标。这是基于尝试以一些裕量达到 2.5 秒的 LCP 而来的。

// 目标虚荣指标
const TARGET_VANITY_METRIC = 2300;

2. 预期的表单操作网址

备注

源码中 593 - 598 行

// This is the exact URL string we expect that Fizz renders if we provide a function action.
// We use this for hydration warnings. It needs to be in sync with Fizz. Maybe makes sense
// as a shared module for that reason.
// 这是我们期望 Fizz 在我们提供函数操作时渲染的精确 URL 字符串。我们将其用于 hydration 警告。它
// 需要与 Fizz 保持同步。出于这个原因,也许作为一个共享模块是有意义的。
const EXPECTED_FORM_ACTION_URL =
"javascript:throw new Error('React form unexpectedly submitted.')";