validate DOM nesting
一、作用
二、已更新的Ancestor信息开发
function updatedAncestorInfoDev(
oldInfo: null | AncestorInfoDev,
tag: string,
): AncestorInfoDev {
if (__DEV__) {
const ancestorInfo = { ...(oldInfo || emptyAncestorInfoDev) };
const info = { tag };
if (inScopeTags.indexOf(tag) !== -1) {
ancestorInfo.aTagInScope = null;
ancestorInfo.buttonTagInScope = null;
ancestorInfo.nobrTagInScope = null;
}
if (buttonScopeTags.indexOf(tag) !== -1) {
ancestorInfo.pTagInButtonScope = null;
}
if (
specialTags.indexOf(tag) !== -1 &&
tag !== 'address' &&
tag !== 'div' &&
tag !== 'p'
) {
// See rules for 'li', 'dd', 'dt' start tags in
// 查看 'li'、'dd'、'dt' 起始标签的规则
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
ancestorInfo.listItemTagAutoclosing = null;
ancestorInfo.dlItemTagAutoclosing = null;
}
ancestorInfo.current = info;
if (tag === 'form') {
ancestorInfo.formTag = info;
}
if (tag === 'a') {
ancestorInfo.aTagInScope = info;
}
if (tag === 'button') {
ancestorInfo.buttonTagInScope = info;
}
if (tag === 'nobr') {
ancestorInfo.nobrTagInScope = info;
}
if (tag === 'p') {
ancestorInfo.pTagInButtonScope = info;
}
if (tag === 'li') {
ancestorInfo.listItemTagAutoclosing = info;
}
if (tag === 'dd' || tag === 'dt') {
ancestorInfo.dlItemTagAutoclosing = info;
}
if (tag === '#document' || tag === 'html') {
ancestorInfo.containerTagInScope = null;
} else if (!ancestorInfo.containerTagInScope) {
ancestorInfo.containerTagInScope = info;
}
if (
oldInfo === null &&
(tag === '#document' || tag === 'html' || tag === 'body')
) {
// While <head> is also a singleton we don't want to support semantics where
// you can escape the head by rendering a body singleton so we treat it like a normal scope
//
// 虽然 <head> 也是一个单例,但我们不希望支持可以通过渲染 body 单例来绕过 head 的语义
// 因此我们将其视为一个普通的作用域
ancestorInfo.implicitRootScope = true;
} else if (ancestorInfo.implicitRootScope === true) {
ancestorInfo.implicitRootScope = false;
}
return ancestorInfo;
} else {
return null as any;
}
}
三、验证DOM嵌套
备注
current由 ReactCurrentFiber#current 提供runWithFiberInDEV()由 ReactCurrentFiber#runWithFiberInDEV 实现
function validateDOMNesting(
childTag: string,
ancestorInfo: AncestorInfoDev,
): boolean {
if (__DEV__) {
ancestorInfo = ancestorInfo || emptyAncestorInfoDev;
const parentInfo = ancestorInfo.current;
const parentTag = parentInfo && parentInfo.tag;
const invalidParent = isTagValidWithParent(
childTag,
parentTag,
ancestorInfo.implicitRootScope,
)
? null
: parentInfo;
const invalidAncestor = invalidParent
? null
: findInvalidAncestorForTag(childTag, ancestorInfo);
const invalidParentOrAncestor = invalidParent || invalidAncestor;
if (!invalidParentOrAncestor) {
return true;
}
const ancestorTag = invalidParentOrAncestor.tag;
const warnKey =
String(!!invalidParent) + '|' + childTag + '|' + ancestorTag;
if (didWarn[warnKey]) {
return false;
}
didWarn[warnKey] = true;
const child = current;
const ancestor = child ? findAncestor(child.return, ancestorTag) : null;
const ancestorDescription =
child !== null && ancestor !== null
? describeAncestors(ancestor, child, null)
: '';
const tagDisplayName = '<' + childTag + '>';
if (invalidParent) {
let info = '';
if (ancestorTag === 'table' && childTag === 'tr') {
info +=
' Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated by ' +
'the browser.';
}
console.error(
'In HTML, %s cannot be a child of <%s>.%s\n' +
'This will cause a hydration error.%s',
tagDisplayName,
ancestorTag,
info,
ancestorDescription,
);
} else {
console.error(
'In HTML, %s cannot be a descendant of <%s>.\n' +
'This will cause a hydration error.%s',
tagDisplayName,
ancestorTag,
ancestorDescription,
);
}
if (child) {
// For debugging purposes find the nearest ancestor that caused the issue.
// The stack trace of this ancestor can be useful to find the cause.
// If the parent is a direct parent in the same owner, we don't bother.
//
// 为调试目的,找到导致问题的最近祖先。该祖先的堆栈跟踪可以帮助查找原因。如果父节点是
// 同一个所有者中的直接父节点,我们就不管它。
const parent = child.return;
if (
ancestor !== null &&
parent !== null &&
(ancestor !== parent || parent._debugOwner !== child._debugOwner)
) {
runWithFiberInDEV(ancestor, () => {
console.error(
// We repeat some context because this log might be taken out of context
// such as in React DevTools or grouped server logs.
// 我们重复一些上下文,因为此日志可能会被断章取义。例如:在 React 开发工具或分组的
// 服务器日志中。
'<%s> cannot contain a nested %s.\n' +
'See this log for the ancestor stack trace.',
ancestorTag,
tagDisplayName,
);
});
}
}
return false;
}
return true;
}
四、验证文本嵌套
备注
current由 ReactCurrentFiber#current 提供
function validateTextNesting(
childText: string,
parentTag: string,
implicitRootScope: boolean,
): boolean {
if (__DEV__) {
if (implicitRootScope || isTagValidWithParent('#text', parentTag, false)) {
return true;
}
const warnKey = '#text|' + parentTag;
if (didWarn[warnKey]) {
return false;
}
didWarn[warnKey] = true;
const child = current;
const ancestor = child ? findAncestor(child, parentTag) : null;
const ancestorDescription =
child !== null && ancestor !== null
? describeAncestors(
ancestor,
child,
child.tag !== HostText ? { children: null } : null,
)
: '';
if (/\S/.test(childText)) {
console.error(
'In HTML, text nodes cannot be a child of <%s>.\n' +
'This will cause a hydration error.%s',
parentTag,
ancestorDescription,
);
} else {
console.error(
'In HTML, whitespace text nodes cannot be a child of <%s>. ' +
"Make sure you don't have any extra whitespace between tags on " +
'each line of your source code.\n' +
'This will cause a hydration error.%s',
parentTag,
ancestorDescription,
);
}
return false;
}
return true;
}
五、常量
1. 特殊标签
// This validation code was written based on the HTML5 parsing spec:
// https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
//
// Note: this does not catch all invalid nesting, nor does it try to (as it's
// not clear what practical benefit doing so provides); instead, we warn only
// for cases where the parser will give a parse tree differing from what React
// intended. For example, <b><div></div></b> is invalid but we don't warn
// because it still parses correctly; we do warn for other cases like nested
// <p> tags where the beginning of the second element implicitly closes the
// first, causing a confusing mess.
//
// 注意:这不能捕捉到所有无效的嵌套,也不打算做到这一点(因为不清楚这样做有什么实际好处);
// 相反,我们只在解析器生成的解析树与 React 预期的不同情况下发出警告。例如,
// `<b><div></div></b>` 是无效的,但我们不会发出警告,因为它仍然可以正确解析;
// 我们会对其他情况发出警告,比如嵌套的 `<p>` 标签,其中第二个元素的开头会隐式关闭
// 第一个元素,从而产生混乱。
// https://html.spec.whatwg.org/multipage/syntax.html#special
const specialTags = [
'address',
'applet',
'area',
'article',
'aside',
'base',
'basefont',
'bgsound',
'blockquote',
'body',
'br',
'button',
'caption',
'center',
'col',
'colgroup',
'dd',
'details',
'dir',
'div',
'dl',
'dt',
'embed',
'fieldset',
'figcaption',
'figure',
'footer',
'form',
'frame',
'frameset',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'head',
'header',
'hgroup',
'hr',
'html',
'iframe',
'img',
'input',
'isindex',
'li',
'link',
'listing',
'main',
'marquee',
'menu',
'menuitem',
'meta',
'nav',
'noembed',
'noframes',
'noscript',
'object',
'ol',
'p',
'param',
'plaintext',
'pre',
'script',
'section',
'select',
'source',
'style',
'summary',
'table',
'tbody',
'td',
'template',
'textarea',
'tfoot',
'th',
'thead',
'title',
'tr',
'track',
'ul',
'wbr',
'xmp',
];
2. 范围内标签
// https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-scope
const inScopeTags = [
'applet',
'caption',
'html',
'table',
'td',
'th',
'marquee',
'object',
'template',
// https://html.spec.whatwg.org/multipage/syntax.html#html-integration-point
// TODO: Distinguish by namespace here -- for <title>, including it here
// errs on the side of fewer warnings
// TODO: 在这里按命名空间区分 —— 对于 <title>,包括在这里
// 偏向于少发出警告
'foreignObject',
'desc',
'title',
];
3. 按钮范围标签
// https://html.spec.whatwg.org/multipage/syntax.html#has-an-element-in-button-scope
const buttonScopeTags = __DEV__ ? inScopeTags.concat(['button']) : [];
4. 隐含结束标签
// https://html.spec.whatwg.org/multipage/syntax.html#generate-implied-end-tags
const impliedEndTags = [
'dd',
'dt',
'li',
'option',
'optgroup',
'p',
'rp',
'rt',
];
5. 空祖先信息开发
const emptyAncestorInfoDev: AncestorInfoDev = {
current: null,
formTag: null,
aTagInScope: null,
buttonTagInScope: null,
nobrTagInScope: null,
pTagInButtonScope: null,
listItemTagAutoclosing: null,
dlItemTagAutoclosing: null,
containerTagInScope: null,
implicitRootScope: false,
};
6. 已警告
备注
源码 532 行
const didWarn: { [string]: boolean } = {};
六、工具
1. 描述祖先
备注
describeDiff()由 ReactFiberHydrationDiffs#describeDiff 实现
function describeAncestors(
ancestor: Fiber,
child: Fiber,
props: null | { children: null },
): string {
let fiber: null | Fiber = child;
let node: null | HydrationDiffNode = null;
let distanceFromLeaf = 0;
while (fiber) {
if (fiber === ancestor) {
distanceFromLeaf = 0;
}
node = {
fiber: fiber,
children: node !== null ? [node] : [],
serverProps:
fiber === child ? props : fiber === ancestor ? null : undefined,
serverTail: [],
distanceFromLeaf: distanceFromLeaf,
};
distanceFromLeaf++;
fiber = fiber.return;
}
if (node !== null) {
// Describe the node using the hydration diff logic.
// Replace + with - to mark ancestor and child. It's kind of arbitrary.
//
// 使用 hydration 差异逻辑描述节点。用 - 替换 +- 来标记祖先和子节点。这有点随意。
return describeDiff(node).replaceAll(/^[+-]/gm, '>');
}
return '';
}
2. 标签在父级中是否有效
/**
* Returns whether
* 返回是否
*/
function isTagValidWithParent(
tag: string,
parentTag: ?string,
implicitRootScope: boolean,
): boolean {
// First, let's check if we're in an unusual parsing mode...
// 首先,让我们检查一下我们是否处于异常解析模式...
switch (parentTag) {
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inselect
case 'select':
return (
tag === 'hr' ||
tag === 'option' ||
tag === 'optgroup' ||
tag === 'script' ||
tag === 'template' ||
tag === '#text'
);
case 'optgroup':
return tag === 'option' || tag === '#text';
// Strictly speaking, seeing an <option> doesn't mean we're in a <select>
// but
// 严格来说,看到一个 <option> 并不意味着我们在一个 <select> 中。但是
case 'option':
return tag === '#text';
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intd
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incaption
// No special behavior since these rules fall back to "in body" mode for
// all except special table nodes which cause bad parsing behavior anyway.
// 没有特殊行为,因为这些规则对于所有节点都会回退到“在正文中”模式,除了会导致解析错误的
// 特殊表格节点之外。
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intr
case 'tr':
return (
tag === 'th' ||
tag === 'td' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intbody
case 'tbody':
case 'thead':
case 'tfoot':
return (
tag === 'tr' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-incolgroup
case 'colgroup':
return tag === 'col' || tag === 'template';
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-intable
case 'table':
return (
tag === 'caption' ||
tag === 'colgroup' ||
tag === 'tbody' ||
tag === 'tfoot' ||
tag === 'thead' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inhead
case 'head':
return (
tag === 'base' ||
tag === 'basefont' ||
tag === 'bgsound' ||
tag === 'link' ||
tag === 'meta' ||
tag === 'title' ||
tag === 'noscript' ||
tag === 'noframes' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
// https://html.spec.whatwg.org/multipage/semantics.html#the-html-element
case 'html':
if (implicitRootScope) {
// When our parent tag is html and we're in the root scope we will actually
// insert most tags into the body so we need to fall through to validating
// the specific tag with "in body" parsing mode below
// 当我们的父标签是 html 并且我们在根作用域时,实际上我们会将大多数标签插入到 body 中
// 因此我们需要继续使用下面的“在 body 中”解析模式来验证特定标签
break;
}
return tag === 'head' || tag === 'body' || tag === 'frameset';
case 'frameset':
return tag === 'frame';
case '#document':
if (implicitRootScope) {
// When our parent is the Document and we're in the root scope we will actually
// insert most tags into the body so we need to fall through to validating
// the specific tag with "in body" parsing mode below
// 当我们的父节点是文档并且我们在根作用域时,我们实际上会将大多数标签插入到 body 中,
// 所以我们需要继续进行验证,使用下面的“在 body 中”解析模式来验证特定标签
break;
}
return tag === 'html';
}
// Probably in the "in body" parsing mode, so we outlaw only tag combos
// where the parsing rules cause implicit opens or closes to be added.
// 可能是在“正文内”解析模式下,所以我们只禁止那些解析规则会导致隐式打开或关闭的标签组合。
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
switch (tag) {
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return (
parentTag !== 'h1' &&
parentTag !== 'h2' &&
parentTag !== 'h3' &&
parentTag !== 'h4' &&
parentTag !== 'h5' &&
parentTag !== 'h6'
);
case 'rp':
case 'rt':
return impliedEndTags.indexOf(parentTag) === -1;
case 'caption':
case 'col':
case 'colgroup':
case 'frameset':
case 'frame':
case 'tbody':
case 'td':
case 'tfoot':
case 'th':
case 'thead':
case 'tr':
// These tags are only valid with a few parents that have special child
// parsing rules -- if we're down here, then none of those matched and
// so we allow it only if we don't know what the parent is, as all other
// cases are invalid.
//
// 这些标签仅在一些具有特殊子元素解析规则的父元素下有效——如果我们运行到这里,说明
// 没有匹配任何这些父元素,因此我们只在不知道父元素是什么时允许它,因为所有其他情
// 况都是无效的。
return parentTag == null;
case 'head':
// We support rendering <head> in the root when the container is
// #document, <html>, or <body>.
// 当容器是 #document、<html> 或 <body> 时,我们支持在根节点渲染 <head>
return implicitRootScope || parentTag === null;
case 'html':
// We support rendering <html> in the root when the container is
// #document
// 当容器是 #document 时,我们支持在根节点渲染 <html>
return (
(implicitRootScope && parentTag === '#document') || parentTag === null
);
case 'body':
// We support rendering <body> in the root when the container is
// #document or <html>
// 当容器是 #document 或 <html> 时,我们支持在根部渲染 <body>
return (
(implicitRootScope &&
(parentTag === '#document' || parentTag === 'html')) ||
parentTag === null
);
}
return true;
}
3. 查找标签的无效祖先
/**
* Returns whether
* 返回是否
*/
function findInvalidAncestorForTag(
tag: string,
ancestorInfo: AncestorInfoDev,
): ?Info {
switch (tag) {
case 'address':
case 'article':
case 'aside':
case 'blockquote':
case 'center':
case 'details':
case 'dialog':
case 'dir':
case 'div':
case 'dl':
case 'fieldset':
case 'figcaption':
case 'figure':
case 'footer':
case 'header':
case 'hgroup':
case 'main':
case 'menu':
case 'nav':
case 'ol':
case 'p':
case 'section':
case 'summary':
case 'ul':
case 'pre':
case 'listing':
case 'table':
case 'hr':
case 'xmp':
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return ancestorInfo.pTagInButtonScope;
case 'form':
return ancestorInfo.formTag || ancestorInfo.pTagInButtonScope;
case 'li':
return ancestorInfo.listItemTagAutoclosing;
case 'dd':
case 'dt':
return ancestorInfo.dlItemTagAutoclosing;
case 'button':
return ancestorInfo.buttonTagInScope;
case 'a':
// Spec says something about storing a list of markers, but it sounds
// equivalent to this check.
// 规范中提到关于存储标记列表的内容,但听起来和这个检查是等效的。
return ancestorInfo.aTagInScope;
case 'nobr':
return ancestorInfo.nobrTagInScope;
}
return null;
}
4. 查找祖先
function findAncestor(parent: null | Fiber, tagName: string): null | Fiber {
while (parent) {
switch (parent.tag) {
case HostComponent:
case HostHoistable:
case HostSingleton:
if (parent.type === tagName) {
return parent;
}
}
parent = parent.return;
}
return null;
}
七、类型
备注
该段代码在源码中的 57 - 74 行。
type Info = { tag: string };
export type AncestorInfoDev = {
current: ?Info;
formTag: ?Info;
aTagInScope: ?Info;
buttonTagInScope: ?Info;
nobrTagInScope: ?Info;
pTagInButtonScope: ?Info;
listItemTagAutoclosing: ?Info;
dlItemTagAutoclosing: ?Info;
// <head> or <body>
containerTagInScope: ?Info;
implicitRootScope: boolean;
};