跳到主要内容

React Fiber 水合差异

一、作用

二、描述差异

export function describeDiff(rootNode: HydrationDiffNode): string {
try {
return '\n\n' + describeNode(rootNode, 0);
} catch (x) {
return '';
}
}

三、常量

1. 最大行长度

const maxRowLength = 120;

2. 理想深度

const idealDepth = 15;

3. 需要转义

const needsEscaping = /["'&<>\n\t]|^\s|\s$/;

四、工具

1. 找到显著节点

function findNotableNode(
node: HydrationDiffNode,
indent: number,
): HydrationDiffNode {
if (
node.serverProps === undefined &&
node.serverTail.length === 0 &&
node.children.length === 1 &&
node.distanceFromLeaf > 3 &&
node.distanceFromLeaf > idealDepth - indent
) {
// This is not an interesting node for contextual purposes so we can skip it.
// 这个节点在上下文中没有什么趣味,所以我们可以跳过它。
const child = node.children[0];
return findNotableNode(child, indent);
}
return node;
}

2. 缩进

function indentation(indent: number): string {
return ' ' + ' '.repeat(indent);
}

3. 已添加

function added(indent: number): string {
return '+ ' + ' '.repeat(indent);
}

4. 已移除

function removed(indent: number): string {
return '- ' + ' '.repeat(indent);
}

5.描述 Fiber 类型

备注

常量值来自于 ReactWorkTags

function describeFiberType(fiber: Fiber): null | string {
switch (fiber.tag) {
case HostHoistable:
case HostSingleton:
case HostComponent:
return fiber.type;
case LazyComponent:
return 'Lazy';
case ActivityComponent:
return 'Activity';
case SuspenseComponent:
return 'Suspense';
case SuspenseListComponent:
return 'SuspenseList';
case FunctionComponent:
case SimpleMemoComponent:
const fn = fiber.type;
return fn.displayName || fn.name || null;
case ForwardRef:
const render = fiber.type.render;
return render.displayName || render.name || null;
case ClassComponent:
const ctr = fiber.type;
return ctr.displayName || ctr.name || null;
default:
// Skip
// 跳过
return null;
}
}

6. 描述文本节点

function describeTextNode(content: string, maxLength: number): string {
if (needsEscaping.test(content)) {
const encoded = JSON.stringify(content);
if (encoded.length > maxLength - 2) {
if (maxLength < 8) {
return '{"..."}';
}
return '{' + encoded.slice(0, maxLength - 7) + '..."}';
}
return '{' + encoded + '}';
} else {
if (content.length > maxLength) {
if (maxLength < 5) {
return '{"..."}';
}
return content.slice(0, maxLength - 3) + '...';
}
return content;
}
}

7. 描述文本差异

备注

所有函数本文档提供

function describeTextDiff(
clientText: string,
serverProps: mixed,
indent: number,
): string {
const maxLength = maxRowLength - indent * 2;
if (serverProps === null) {
return added(indent) + describeTextNode(clientText, maxLength) + '\n';
} else if (typeof serverProps === 'string') {
let serverText: string = serverProps;
let firstDiff = 0;
for (
;
firstDiff < serverText.length && firstDiff < clientText.length;
firstDiff++
) {
if (
serverText.charCodeAt(firstDiff) !== clientText.charCodeAt(firstDiff)
) {
break;
}
}
if (firstDiff > maxLength - 8 && firstDiff > 10) {
// The first difference between the two strings would be cut off, so cut off in
// the beginning instead.
//
// 两个字符串之间的第一个差异会被截断,因此应该在开头截断。
clientText = '...' + clientText.slice(firstDiff - 8);
serverText = '...' + serverText.slice(firstDiff - 8);
}
return (
added(indent) +
describeTextNode(clientText, maxLength) +
'\n' +
removed(indent) +
describeTextNode(serverText, maxLength) +
'\n'
);
} else {
return indentation(indent) + describeTextNode(clientText, maxLength) + '\n';
}
}

8. 对象名

function objectName(object: mixed): string {
const name = Object.prototype.toString.call(object);
return name.replace(/^\[object (.*)\]$/, function (m, p0) {
return p0;
});
}

9. 描述值

function describeValue(value: mixed, maxLength: number): string {
switch (typeof value) {
case 'string': {
const encoded = JSON.stringify(value);
if (encoded.length > maxLength) {
if (maxLength < 5) {
return '"..."';
}
return encoded.slice(0, maxLength - 4) + '..."';
}
return encoded;
}
case 'object': {
if (value === null) {
return 'null';
}
if (isArray(value)) {
return '[...]';
}
if ((value as any).$$typeof === REACT_ELEMENT_TYPE) {
const type = getComponentNameFromType((value as any).type);
return type ? '<' + type + '>' : '<...>';
}
const name = objectName(value);
if (name === 'Object') {
let properties = '';
maxLength -= 2;
for (let propName in value) {
if (!value.hasOwnProperty(propName)) {
continue;
}
const jsonPropName = JSON.stringify(propName);
if (jsonPropName !== '"' + propName + '"') {
propName = jsonPropName;
}
maxLength -= propName.length - 2;
const propValue = describeValue(
value[propName],
maxLength < 15 ? maxLength : 15,
);
maxLength -= propValue.length;
if (maxLength < 0) {
properties += properties === '' ? '...' : ', ...';
break;
}
properties +=
(properties === '' ? '' : ',') + propName + ':' + propValue;
}
return '{' + properties + '}';
} else if (enableSrcObject && (name === 'Blob' || name === 'File')) {
return name + ':' + (value as any).type;
}
return name;
}
case 'function': {
const name = (value as any).displayName || value.name;
return name ? 'function ' + name : 'function';
}
default:
return String(value);
}
}

10. 描述属性值

function describePropValue(value: mixed, maxLength: number): string {
if (typeof value === 'string' && !needsEscaping.test(value)) {
if (value.length > maxLength - 2) {
if (maxLength < 5) {
return '"..."';
}
return '"' + value.slice(0, maxLength - 5) + '..."';
}
return '"' + value + '"';
}
return '{' + describeValue(value, maxLength - 2) + '}';
}

11. 描述已折叠的元素

function describeCollapsedElement(
type: string,
props: { [propName: string]: mixed },
indent: number,
): string {
// This function tries to fit the props into a single line for non-essential elements.
// We also ignore children because we're not going deeper.
//
// 这个函数尝试将属性放入单行,以适用于非关键元素。
// 我们也会忽略子元素,因为我们不会进一步深入。

let maxLength = maxRowLength - indent * 2 - type.length - 2;

let content = '';

for (const propName in props) {
if (!props.hasOwnProperty(propName)) {
continue;
}
if (propName === 'children') {
// Ignored.
// 忽略
continue;
}
const propValue = describePropValue(props[propName], 15);
maxLength -= propName.length + propValue.length + 2;
if (maxLength < 0) {
content += ' ...';
break;
}
content += ' ' + propName + '=' + propValue;
}

return indentation(indent) + '<' + type + content + '>\n';
}

12. 描述属性差异

function describePropertiesDiff(
// clientObject: {+[propName: string]: mixed},
clientObject: { [propName: string]: mixed },
// serverObject: { +[propName: string]: mixed },
serverObject: { [propName: string]: mixed },
indent: number,
): string {
let properties = '';
const remainingServerProperties = assign({}, serverObject);
for (const propName in clientObject) {
if (!clientObject.hasOwnProperty(propName)) {
continue;
}
delete remainingServerProperties[propName];
const maxLength = maxRowLength - indent * 2 - propName.length - 2;
const clientValue = clientObject[propName];
const clientPropValue = describeValue(clientValue, maxLength);
if (serverObject.hasOwnProperty(propName)) {
const serverValue = serverObject[propName];
const serverPropValue = describeValue(serverValue, maxLength);
properties += added(indent) + propName + ': ' + clientPropValue + '\n';
properties += removed(indent) + propName + ': ' + serverPropValue + '\n';
} else {
properties += added(indent) + propName + ': ' + clientPropValue + '\n';
}
}
for (const propName in remainingServerProperties) {
if (!remainingServerProperties.hasOwnProperty(propName)) {
continue;
}
const maxLength = maxRowLength - indent * 2 - propName.length - 2;
const serverValue = remainingServerProperties[propName];
const serverPropValue = describeValue(serverValue, maxLength);
properties += removed(indent) + propName + ': ' + serverPropValue + '\n';
}
return properties;
}

13. 描述元素差异


function describeElementDiff(
type: string,
// clientProps: {+[propName: string]: mixed},
clientProps: {[propName: string]: mixed},
// serverProps: {+[propName: string]: mixed},
serverProps: {[propName: string]: mixed},
indent: number,
): string {
let content = '';

// Maps any previously unmatched lower case server prop name to its full prop name
// 将之前未匹配的小写服务器属性名称映射到其完整属性名称
const serverPropNames: Map<string, string> = new Map();

for (const propName in serverProps) {
if (!serverProps.hasOwnProperty(propName)) {
continue;
}
serverPropNames.set(propName.toLowerCase(), propName);
}

if (serverPropNames.size === 1 && serverPropNames.has('children')) {
content += describeExpandedElement(type, clientProps, indentation(indent));
} else {
for (const propName in clientProps) {
if (!clientProps.hasOwnProperty(propName)) {
continue;
}
if (propName === 'children') {
// Handled below.
// 已处理如下。
continue;
}
const maxLength = maxRowLength - (indent + 1) * 2 - propName.length - 1;
const serverPropName = serverPropNames.get(propName.toLowerCase());
if (serverPropName !== undefined) {
serverPropNames.delete(propName.toLowerCase());
// There's a diff here.
// There's a diff here.
const clientValue = clientProps[propName];
const serverValue = serverProps[serverPropName];
const clientPropValue = describePropValue(clientValue, maxLength);
const serverPropValue = describePropValue(serverValue, maxLength);
if (
typeof clientValue === 'object' &&
clientValue !== null &&
typeof serverValue === 'object' &&
serverValue !== null &&
objectName(clientValue) === 'Object' &&
objectName(serverValue) === 'Object' &&
// Only do the diff if the object has a lot of keys or was shortened.
// 只有在对象有很多键或被缩短时才进行差异比较。
(Object.keys(clientValue).length > 2 ||
Object.keys(serverValue).length > 2 ||
clientPropValue.indexOf('...') > -1 ||
serverPropValue.indexOf('...') > -1)
) {
// We're comparing two plain objects. We can diff the nested objects instead.
// 我们正在比较两个普通对象。我们可以改为比较嵌套对象。
content +=
indentation(indent + 1) +
propName +
'={{\n' +
describePropertiesDiff(clientValue, serverValue, indent + 2) +
indentation(indent + 1) +
'}}\n';
} else {
content +=
added(indent + 1) + propName + '=' + clientPropValue + '\n';
content +=
removed(indent + 1) + propName + '=' + serverPropValue + '\n';
}
} else {
// Considered equal.
// 被认为是相等的。
content +=
indentation(indent + 1) +
propName +
'=' +
describePropValue(clientProps[propName], maxLength) +
'\n';
}
}

serverPropNames.forEach(propName => {
if (propName === 'children') {
// Handled below.
// 已处理如下。
return;
}
const maxLength = maxRowLength - (indent + 1) * 2 - propName.length - 1;
content +=
removed(indent + 1) +
propName +
'=' +
describePropValue(serverProps[propName], maxLength) +
'\n';
});

if (content === '') {
// No properties
// 没有属性
content = indentation(indent) + '<' + type + '>\n';
} else {
// Had properties
// 拥有的属性
content =
indentation(indent) +
'<' +
type +
'\n' +
content +
indentation(indent) +
'>\n';
}
}

14. 描述兄弟 Fiber

function describeSiblingFiber(fiber: Fiber, indent: number): string {
const type = describeFiberType(fiber);
if (type === null) {
// Skip this type of fiber. We currently treat this as a fragment
// so it's just part of the parent's children.
//
// 跳过这种类型的 fiber。我们目前将其视为一个片段
// 所以它只是父元素子节点的一部分。
let flatContent = '';
let childFiber = fiber.child;
while (childFiber) {
flatContent += describeSiblingFiber(childFiber, indent);
childFiber = childFiber.sibling;
}
return flatContent;
}
return indentation(indent) + '<' + type + '>' + '\n';
}

15. 描述元素

function describeNode(node: HydrationDiffNode, indent: number): string {
const skipToNode = findNotableNode(node, indent);
if (
skipToNode !== node &&
(node.children.length !== 1 || node.children[0] !== skipToNode)
) {
return indentation(indent) + '...\n' + describeNode(skipToNode, indent + 1);
}

// Prefix with any server components for context
// 为上下文添加任何服务器组件前缀
let parentContent = '';
const debugInfo = node.fiber._debugInfo;
if (debugInfo) {
for (let i = 0; i < debugInfo.length; i++) {
const serverComponentName = debugInfo[i].name;
if (typeof serverComponentName === 'string') {
parentContent +=
indentation(indent) + '<' + serverComponentName + '>' + '\n';
indent++;
}
}
}

// Self
// 自身
let selfContent = '';

// We use the pending props since we might be generating a diff before the complete phase
// when something throws.
//
// 我们使用待处理的 props,因为我们可能在完整阶段之前生成一个差异
// 当某些东西抛出时。
const clientProps = node.fiber.pendingProps;

if (node.fiber.tag === HostText) {
// Text Node
// 文本节点
selfContent = describeTextDiff(clientProps, node.serverProps, indent);
indent++;
} else {
const type = describeFiberType(node.fiber);
if (type !== null) {
// Element Node
// 元素节点
if (node.serverProps === undefined) {
// Just a reference node for context.
// 仅作为上下文的参考节点。
selfContent = describeCollapsedElement(type, clientProps, indent);
indent++;
} else if (node.serverProps === null) {
selfContent = describeExpandedElement(type, clientProps, added(indent));
indent++;
} else if (typeof node.serverProps === 'string') {
if (__DEV__) {
console.error(
'Should not have matched a non HostText fiber to a Text node. This is a bug in React.',
);
}
} else {
selfContent = describeElementDiff(
type,
clientProps,
node.serverProps,
indent,
);
indent++;
}
}
}

// Compute children
// 计算子节点
let childContent = '';
let childFiber = node.fiber.child;
let diffIdx = 0;
while (childFiber && diffIdx < node.children.length) {
const childNode = node.children[diffIdx];
if (childNode.fiber === childFiber) {
// This was a match in the diff.
// 这是 diff 中的一次匹配。
childContent += describeNode(childNode, indent);
diffIdx++;
} else {
// This is an unrelated previous sibling.
// 这是一个无关的前一个兄弟节点。
childContent += describeSiblingFiber(childFiber, indent);
}
childFiber = childFiber.sibling;
}

if (childFiber && node.children.length > 0) {
// If we had any further siblings after the last mismatch, we can't be sure if it's
// actually a valid match since it might not have found a match. So we exclude next
// siblings to avoid confusion.
//
// 如果在最后一次不匹配之后还有兄弟节点,我们无法确定它是否是一个有效的匹配,因为它可能没有找到
// 匹配项。所以我们排除下一个兄弟节点以避免混淆。
childContent += indentation(indent) + '...' + '\n';
}

// Deleted tail nodes
// 删除尾节点
const serverTail = node.serverTail;
if (node.serverProps === null) {
indent--;
}
for (let i = 0; i < serverTail.length; i++) {
const tailNode = serverTail[i];
if (typeof tailNode === 'string') {
// Removed text node
// 移除文本节点
childContent +=
removed(indent) +
describeTextNode(tailNode, maxRowLength - indent * 2) +
'\n';
} else {
// Removed element
// 移除元素
childContent += describeExpandedElement(
tailNode.type,
tailNode.props,
removed(indent),
);
}
}

return parentContent + selfContent + childContent;
}