React DOM component
一、作用
二、在非交互元素上捕获点击
export function trapClickOnNonInteractiveElement(node: HTMLElement) {
// Mobile Safari does not fire properly bubble click events on
// non-interactive elements, which means delegated click listeners do not
// fire. The workaround for this bug involves attaching an empty click
// listener on the target node.
// 移动端 Safari 无法在非交互元素上正确触发冒泡点击事件,这意味着委托的点击监听器无法
// 触发。解决此问题的办法是在目标节点上附加一个空的点击监听器。
// https://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
// Just set it using the onclick property so that we don't have to manage any
// bookkeeping for it. Not sure if we need to clear it when the listener is
// removed.
// TODO: Only do this for the relevant Safaris maybe?
// 只需使用 onclick 属性设置它,这样我们就不必管理任何记录。 不确定在移除监听器时是否
// 需要清除它。
// 待办事项:也许只对相关的 Safari 做这个?
node.onclick = noop;
}
三、设置初始属性
备注
listenToNonDelegatedEvent()由 DOMPluginEventSystem#listenToNonDelegatedEvent 实现checkControlledValueProps()由 ReactControlledValuePropTypes#checkControlledValueProps 实现validateInputProps()由 ReactDOMInput#validateInputProps 实现initSelect()由 ReactDOMSelect#initSelect 实现initTextarea()由 ReactDOMTextarea#initTextarea 实现
export function setInitialProperties(
domElement: Element,
tag: string,
props: Object,
): void {
if (__DEV__) {
validatePropertiesInDevelopment(tag, props);
}
// TODO: Make sure that we check isMounted before firing any of these events.
// 待办事项:在触发任何这些事件之前,确保我们检查 isMounted。
switch (tag) {
case 'div':
case 'span':
case 'svg':
case 'path':
case 'a':
case 'g':
case 'p':
case 'li': {
// Fast track the most common tag types
// 快速跟踪最常见的标签类型
break;
}
// img tags previously were implemented as void elements with non delegated events however Safari (and possibly Firefox)
// begin fetching the image as soon as the `src` or `srcSet` property is set and if we set these before other properties
// that can modify the request (such as crossorigin) or the resource fetch (such as sizes) then the browser will load
// the wrong thing or load more than one thing. This implementation ensures src and srcSet are set on the instance last
// img 标签以前被实现为不带委托事件的空元素,然而 Safari(可能还有 Firefox)
// 会在 `src` 或 `srcSet` 属性被设置后立即开始获取图片,如果我们在其他可能修改
// 请求的属性(如 crossorigin)或资源获取的属性(如 sizes)之前设置了这些属性,
// 浏览器就可能加载错误的内容或加载多次。
// 这个实现确保在实例上最后设置 src 和 srcSet
case 'img': {
listenToNonDelegatedEvent('error', domElement);
listenToNonDelegatedEvent('load', domElement);
// Mostly a port of Void Element logic with special casing to ensure srcset and src are set last
// 大部分是 Void Element 逻辑的移植,带有特殊处理以确保最后设置 srcset 和 src
let hasSrc = false;
let hasSrcSet = false;
for (const propKey in props) {
if (!props.hasOwnProperty(propKey)) {
continue;
}
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'src':
hasSrc = true;
break;
case 'srcSet':
hasSrcSet = true;
break;
case 'children':
case 'dangerouslySetInnerHTML': {
// TODO: Can we make this a DEV warning to avoid this deny list?
// TODO: 我们能否将其改为开发警告,以避免这个拒绝列表?
throw new Error(
`${tag} is a void element tag and must neither have \`children\` nor ` +
'use `dangerouslySetInnerHTML`.',
);
}
// defaultChecked and defaultValue are ignored by setProp
// setProp 会忽略 defaultChecked 和 defaultValue
default: {
setProp(domElement, tag, propKey, propValue, props, null);
}
}
}
if (hasSrcSet) {
setProp(domElement, tag, 'srcSet', props.srcSet, props, null);
}
if (hasSrc) {
setProp(domElement, tag, 'src', props.src, props, null);
}
return;
}
case 'input': {
if (__DEV__) {
checkControlledValueProps('input', props);
}
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
// 我们监听这个事件,以确保模拟的冒泡监听器仍然会触发无效事件。
listenToNonDelegatedEvent('invalid', domElement);
let name = null;
let type = null;
let value = null;
let defaultValue = null;
let checked = null;
let defaultChecked = null;
for (const propKey in props) {
if (!props.hasOwnProperty(propKey)) {
continue;
}
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'name': {
name = propValue;
break;
}
case 'type': {
type = propValue;
break;
}
case 'checked': {
checked = propValue;
break;
}
case 'defaultChecked': {
defaultChecked = propValue;
break;
}
case 'value': {
value = propValue;
break;
}
case 'defaultValue': {
defaultValue = propValue;
break;
}
case 'children':
case 'dangerouslySetInnerHTML': {
if (propValue != null) {
throw new Error(
`${tag} is a void element tag and must neither have \`children\` nor ` +
'use `dangerouslySetInnerHTML`.',
);
}
break;
}
default: {
setProp(domElement, tag, propKey, propValue, props, null);
}
}
}
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
// 待办事项:确保我们检查此组件是否仍未卸载,或者进行任何必要的清理,因为我们再也
// 不停止追踪了。
validateInputProps(domElement, props);
initInput(
domElement,
value,
defaultValue,
checked,
defaultChecked,
type,
name,
false,
);
return;
}
case 'select': {
if (__DEV__) {
checkControlledValueProps('select', props);
}
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
// 我们监听这个事件,以确保模拟的冒泡监听器仍然会触发无效事件。
listenToNonDelegatedEvent('invalid', domElement);
let value = null;
let defaultValue = null;
let multiple = null;
for (const propKey in props) {
if (!props.hasOwnProperty(propKey)) {
continue;
}
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'value': {
value = propValue;
// This is handled by initSelect below.
// 这由下面的 initSelect 处理。
break;
}
case 'defaultValue': {
defaultValue = propValue;
// This is handled by initSelect below.
// 这由下面的 initSelect 处理。
break;
}
case 'multiple': {
multiple = propValue;
// TODO: We don't actually have to fall through here because we set it
// in initSelect anyway. We can remove the special case in setProp.
// 待办事项:实际上我们不必在这里继续执行下去,因为我们无论如何都会在
// initSelect 中设置它。我们可以在 setProp 中移除这个特殊情况。
}
// Fallthrough
// 贯穿
default: {
setProp(domElement, tag, propKey, propValue, props, null);
}
}
}
validateSelectProps(domElement, props);
initSelect(domElement, value, defaultValue, multiple);
return;
}
case 'textarea': {
if (__DEV__) {
checkControlledValueProps('textarea', props);
}
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
// 我们监听这个事件,以确保模拟的冒泡监听器仍然会触发无效事件。
listenToNonDelegatedEvent('invalid', domElement);
let value = null;
let defaultValue = null;
let children = null;
for (const propKey in props) {
if (!props.hasOwnProperty(propKey)) {
continue;
}
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'value': {
value = propValue;
// This is handled by initTextarea below.
// 这由下面的 initTextarea 处理。
break;
}
case 'defaultValue': {
defaultValue = propValue;
break;
}
case 'children': {
children = propValue;
// Handled by initTextarea above.
// 由上面的 initTextarea 处理。
break;
}
case 'dangerouslySetInnerHTML': {
if (propValue != null) {
// TODO: Do we really need a special error message for this. It's also pretty blunt.
// 待办:我们真的需要为此提供一个特殊的错误信息吗?这也显得相当直白。
throw new Error(
'`dangerouslySetInnerHTML` does not make sense on <textarea>.',
);
}
break;
}
default: {
setProp(domElement, tag, propKey, propValue, props, null);
}
}
}
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
// 待办:确保检查此组件是否仍未卸载,或进行任何必要的清理,因为我们不再停止跟踪。
validateTextareaProps(domElement, props);
initTextarea(domElement, value, defaultValue, children);
return;
}
case 'option': {
validateOptionProps(domElement, props);
for (const propKey in props) {
if (!props.hasOwnProperty(propKey)) {
continue;
}
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'selected': {
// TODO: Remove support for selected on option.
// 待办:移除对选中选项的支持。
(domElement as any).selected =
propValue &&
typeof propValue !== 'function' &&
typeof propValue !== 'symbol';
break;
}
default: {
setProp(domElement, tag, propKey, propValue, props, null);
}
}
}
return;
}
case 'dialog': {
listenToNonDelegatedEvent('beforetoggle', domElement);
listenToNonDelegatedEvent('toggle', domElement);
listenToNonDelegatedEvent('cancel', domElement);
listenToNonDelegatedEvent('close', domElement);
break;
}
case 'iframe':
case 'object': {
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the load event.
// 我们监听这个事件,以确保模拟的冒泡监听器仍然会触发 load 事件。
listenToNonDelegatedEvent('load', domElement);
break;
}
case 'video':
case 'audio': {
// We listen to these events in case to ensure emulated bubble
// listeners still fire for all the media events.
// 我们监听这些事件,以确保模拟的冒泡监听器仍然会触发所有媒体事件。
for (let i = 0; i < mediaEventTypes.length; i++) {
listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
}
break;
}
case 'image': {
// We listen to these events in case to ensure emulated bubble
// listeners still fire for error and load events.
// 我们监听这些事件,以确保模拟的冒泡监听器仍然会触发错误和加载事件。
listenToNonDelegatedEvent('error', domElement);
listenToNonDelegatedEvent('load', domElement);
break;
}
case 'details': {
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the toggle event.
// 我们监听这个事件,以确保模拟的冒泡监听器仍然会触发切换事件。
listenToNonDelegatedEvent('toggle', domElement);
break;
}
case 'embed':
case 'source':
case 'link': {
// These are void elements that also need delegated events.
// 这些是也需要委托事件的空元素。
listenToNonDelegatedEvent('error', domElement);
listenToNonDelegatedEvent('load', domElement);
// We fallthrough to the return of the void elements
// 我们继续执行到 void 元素的返回
}
case 'area':
case 'base':
case 'br':
case 'col':
case 'hr':
case 'keygen':
case 'meta':
case 'param':
case 'track':
case 'wbr':
case 'menuitem': {
// Void elements
// 空元素
for (const propKey in props) {
if (!props.hasOwnProperty(propKey)) {
continue;
}
const propValue = props[propKey];
if (propValue == null) {
continue;
}
switch (propKey) {
case 'children':
case 'dangerouslySetInnerHTML': {
// TODO: Can we make this a DEV warning to avoid this deny list?
// TODO: 我们能否将其改为开发警告,以避免这个拒绝列表?
throw new Error(
`${tag} is a void element tag and must neither have \`children\` nor ` +
'use `dangerouslySetInnerHTML`.',
);
}
// defaultChecked and defaultValue are ignored by setProp
// defaultChecked 和 defaultValue 会被 setProp 忽略
default: {
setProp(domElement, tag, propKey, propValue, props, null);
}
}
}
return;
}
default: {
if (isCustomElement(tag, props)) {
for (const propKey in props) {
if (!props.hasOwnProperty(propKey)) {
continue;
}
const propValue = props[propKey];
if (propValue === undefined) {
continue;
}
setPropOnCustomElement(
domElement,
tag,
propKey,
propValue,
props,
undefined,
);
}
return;
}
}
}
for (const propKey in props) {
if (!props.hasOwnProperty(propKey)) {
continue;
}
const propValue = props[propKey];
if (propValue == null) {
continue;
}
setProp(domElement, tag, propKey, propValue, props, null);
}
}
四、更新属性
备注
trackHostMutation()由 ReactFiberMutationTracking#trackHostMutation 实现updateInput()由 ReactDOMInput#updateInput 实现updateSelect()由 ReactDOMSelect#updateSelect 实现updateTextarea()由 ReactDOMTextarea#updateTextarea 实现isCustomElement()由 isCustomElement#isCustomElement 实现
export function updateProperties(
domElement: Element,
tag: string,
lastProps: Object,
nextProps: Object,
): void {
if (__DEV__) {
validatePropertiesInDevelopment(tag, nextProps);
}
switch (tag) {
case 'div':
case 'span':
case 'svg':
case 'path':
case 'a':
case 'g':
case 'p':
case 'li': {
// Fast track the most common tag types
// 快速跟踪最常见的标签类型
break;
}
case 'input': {
let name = null;
let type = null;
let value = null;
let defaultValue = null;
let lastDefaultValue = null;
let checked = null;
let defaultChecked = null;
for (const propKey in lastProps) {
const lastProp = lastProps[propKey];
if (lastProps.hasOwnProperty(propKey) && lastProp != null) {
switch (propKey) {
case 'checked': {
break;
}
case 'value': {
// This is handled by updateWrapper below.
// 这由下面的 updateWrapper 处理。
break;
}
case 'defaultValue': {
lastDefaultValue = lastProp;
}
// defaultChecked and defaultValue are ignored by setProp
// setProp 会忽略 defaultChecked 和 defaultValue
// Fallthrough
// 继承
default: {
if (!nextProps.hasOwnProperty(propKey))
setProp(domElement, tag, propKey, null, nextProps, lastProp);
}
}
}
}
for (const propKey in nextProps) {
const nextProp = nextProps[propKey];
const lastProp = lastProps[propKey];
if (
nextProps.hasOwnProperty(propKey) &&
(nextProp != null || lastProp != null)
) {
switch (propKey) {
case 'type': {
if (nextProp !== lastProp) {
trackHostMutation();
}
type = nextProp;
break;
}
case 'name': {
if (nextProp !== lastProp) {
trackHostMutation();
}
name = nextProp;
break;
}
case 'checked': {
if (nextProp !== lastProp) {
trackHostMutation();
}
checked = nextProp;
break;
}
case 'defaultChecked': {
if (nextProp !== lastProp) {
trackHostMutation();
}
defaultChecked = nextProp;
break;
}
case 'value': {
if (nextProp !== lastProp) {
trackHostMutation();
}
value = nextProp;
break;
}
case 'defaultValue': {
if (nextProp !== lastProp) {
trackHostMutation();
}
defaultValue = nextProp;
break;
}
case 'children':
case 'dangerouslySetInnerHTML': {
if (nextProp != null) {
throw new Error(
`${tag} is a void element tag and must neither have \`children\` nor ` +
'use `dangerouslySetInnerHTML`.',
);
}
break;
}
default: {
if (nextProp !== lastProp)
setProp(
domElement,
tag,
propKey,
nextProp,
nextProps,
lastProp,
);
}
}
}
}
if (__DEV__) {
const wasControlled =
lastProps.type === 'checkbox' || lastProps.type === 'radio'
? lastProps.checked != null
: lastProps.value != null;
const isControlled =
nextProps.type === 'checkbox' || nextProps.type === 'radio'
? nextProps.checked != null
: nextProps.value != null;
if (
!wasControlled &&
isControlled &&
!didWarnUncontrolledToControlled
) {
console.error(
'A component is changing an uncontrolled input to be controlled. ' +
'This is likely caused by the value changing from undefined to ' +
'a defined value, which should not happen. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://react.dev/link/controlled-components',
);
didWarnUncontrolledToControlled = true;
}
if (
wasControlled &&
!isControlled &&
!didWarnControlledToUncontrolled
) {
console.error(
'A component is changing a controlled input to be uncontrolled. ' +
'This is likely caused by the value changing from a defined to ' +
'undefined, which should not happen. ' +
'Decide between using a controlled or uncontrolled input ' +
'element for the lifetime of the component. More info: https://react.dev/link/controlled-components',
);
didWarnControlledToUncontrolled = true;
}
}
// Update the wrapper around inputs *after* updating props. This has to
// happen after updating the rest of props. Otherwise HTML5 input validations
// raise warnings and prevent the new value from being assigned.
// 在更新 props 之后更新输入的包装器。这必须在更新其余 props 之后进行。
// 否则,HTML5 输入验证会发出警告并阻止新值被分配。
updateInput(
domElement,
value,
defaultValue,
lastDefaultValue,
checked,
defaultChecked,
type,
name,
);
return;
}
case 'select': {
let value = null;
let defaultValue = null;
let multiple = null;
let wasMultiple = null;
for (const propKey in lastProps) {
const lastProp = lastProps[propKey];
if (lastProps.hasOwnProperty(propKey) && lastProp != null) {
switch (propKey) {
case 'value': {
// This is handled by updateWrapper below.
// 这由下面的 updateWrapper 处理。
break;
}
// defaultValue are ignored by setProp
// setProp 会忽略 defaultValue
case 'multiple': {
wasMultiple = lastProp;
// TODO: Move special case in here from setProp.
// TODO: 将特殊情况从 setProp 移到这里。
}
// Fallthrough
// 穿透
default: {
if (!nextProps.hasOwnProperty(propKey)) {
setProp(domElement, tag, propKey, null, nextProps, lastProp);
}
}
}
}
}
for (const propKey in nextProps) {
const nextProp = nextProps[propKey];
const lastProp = lastProps[propKey];
if (
nextProps.hasOwnProperty(propKey) &&
(nextProp != null || lastProp != null)
) {
switch (propKey) {
case 'value': {
if (nextProp !== lastProp) {
trackHostMutation();
}
value = nextProp;
// This is handled by updateSelect below.
// 这由下面的 updateSelect 处理。
break;
}
case 'defaultValue': {
if (nextProp !== lastProp) {
trackHostMutation();
}
defaultValue = nextProp;
break;
}
case 'multiple': {
if (nextProp !== lastProp) {
trackHostMutation();
}
multiple = nextProp;
// TODO: Just move the special case in here from setProp.
// TODO: 只是把 setProp 中的特殊情况移动到这里。
}
// Fallthrough
// 贯穿
default: {
if (nextProp !== lastProp)
setProp(
domElement,
tag,
propKey,
nextProp,
nextProps,
lastProp,
);
}
}
}
}
// <select> value update needs to occur after <option> children
// reconciliation
// <select> 的值更新需要在 <option> 子元素协调之后进行
updateSelect(domElement, value, defaultValue, multiple, wasMultiple);
return;
}
case 'textarea': {
let value = null;
let defaultValue = null;
for (const propKey in lastProps) {
const lastProp = lastProps[propKey];
if (
lastProps.hasOwnProperty(propKey) &&
lastProp != null &&
!nextProps.hasOwnProperty(propKey)
) {
switch (propKey) {
case 'value': {
// This is handled by updateTextarea below.
// 这由下面的 updateTextarea 处理。
break;
}
case 'children': {
// TODO: This doesn't actually do anything if it updates.
// 待办事项:如果它更新的话,这实际上不会做任何事。
break;
}
// defaultValue is ignored by setProp
// setProp 会忽略 defaultValue
default: {
setProp(domElement, tag, propKey, null, nextProps, lastProp);
}
}
}
}
for (const propKey in nextProps) {
const nextProp = nextProps[propKey];
const lastProp = lastProps[propKey];
if (
nextProps.hasOwnProperty(propKey) &&
(nextProp != null || lastProp != null)
) {
switch (propKey) {
case 'value': {
if (nextProp !== lastProp) {
trackHostMutation();
}
value = nextProp;
// This is handled by updateTextarea below.
// 这由下面的 updateTextarea 处理。
break;
}
case 'defaultValue': {
if (nextProp !== lastProp) {
trackHostMutation();
}
defaultValue = nextProp;
break;
}
case 'children': {
// TODO: This doesn't actually do anything if it updates.
// 待办事项:如果它更新的话,这实际上不会做任何事。
break;
}
case 'dangerouslySetInnerHTML': {
if (nextProp != null) {
// TODO: Do we really need a special error message for this. It's also pretty blunt.
// 待办:我们真的需要为此提供一个特殊的错误信息吗?这也显得相当直白。
throw new Error(
'`dangerouslySetInnerHTML` does not make sense on <textarea>.',
);
}
break;
}
default: {
if (nextProp !== lastProp)
setProp(
domElement,
tag,
propKey,
nextProp,
nextProps,
lastProp,
);
}
}
}
}
updateTextarea(domElement, value, defaultValue);
return;
}
case 'option': {
for (const propKey in lastProps) {
const lastProp = lastProps[propKey];
if (
lastProps.hasOwnProperty(propKey) &&
lastProp != null &&
!nextProps.hasOwnProperty(propKey)
) {
switch (propKey) {
case 'selected': {
// TODO: Remove support for selected on option.
// 待办:移除对选中选项的支持。
(domElement as any).selected = false;
break;
}
default: {
setProp(domElement, tag, propKey, null, nextProps, lastProp);
}
}
}
}
for (const propKey in nextProps) {
const nextProp = nextProps[propKey];
const lastProp = lastProps[propKey];
if (
nextProps.hasOwnProperty(propKey) &&
nextProp !== lastProp &&
(nextProp != null || lastProp != null)
) {
switch (propKey) {
case 'selected': {
if (nextProp !== lastProp) {
trackHostMutation();
}
// TODO: Remove support for selected on option.
// 待办:移除对选中选项的支持。
(domElement as any).selected =
nextProp &&
typeof nextProp !== 'function' &&
typeof nextProp !== 'symbol';
break;
}
default: {
setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
}
}
}
}
return;
}
case 'img':
case 'link':
case 'area':
case 'base':
case 'br':
case 'col':
case 'embed':
case 'hr':
case 'keygen':
case 'meta':
case 'param':
case 'source':
case 'track':
case 'wbr':
case 'menuitem': {
// Void elements
// 空元素
for (const propKey in lastProps) {
const lastProp = lastProps[propKey];
if (
lastProps.hasOwnProperty(propKey) &&
lastProp != null &&
!nextProps.hasOwnProperty(propKey)
) {
setProp(domElement, tag, propKey, null, nextProps, lastProp);
}
}
for (const propKey in nextProps) {
const nextProp = nextProps[propKey];
const lastProp = lastProps[propKey];
if (
nextProps.hasOwnProperty(propKey) &&
nextProp !== lastProp &&
(nextProp != null || lastProp != null)
) {
switch (propKey) {
case 'children':
case 'dangerouslySetInnerHTML': {
if (nextProp != null) {
// TODO: Can we make this a DEV warning to avoid this deny list?
// TODO: 我们能否将其设为开发警告以避免这个拒绝列表?
throw new Error(
`${tag} is a void element tag and must neither have \`children\` nor ` +
'use `dangerouslySetInnerHTML`.',
);
}
break;
}
// defaultChecked and defaultValue are ignored by setProp
// setProp 会忽略 defaultChecked 和 defaultValue
default: {
setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
}
}
}
}
return;
}
default: {
if (isCustomElement(tag, nextProps)) {
for (const propKey in lastProps) {
const lastProp = lastProps[propKey];
if (
lastProps.hasOwnProperty(propKey) &&
lastProp !== undefined &&
!nextProps.hasOwnProperty(propKey)
) {
setPropOnCustomElement(
domElement,
tag,
propKey,
undefined,
nextProps,
lastProp,
);
}
}
for (const propKey in nextProps) {
const nextProp = nextProps[propKey];
const lastProp = lastProps[propKey];
if (
nextProps.hasOwnProperty(propKey) &&
nextProp !== lastProp &&
(nextProp !== undefined || lastProp !== undefined)
) {
setPropOnCustomElement(
domElement,
tag,
propKey,
nextProp,
nextProps,
lastProp,
);
}
}
return;
}
}
}
for (const propKey in lastProps) {
const lastProp = lastProps[propKey];
if (
lastProps.hasOwnProperty(propKey) &&
lastProp != null &&
!nextProps.hasOwnProperty(propKey)
) {
setProp(domElement, tag, propKey, null, nextProps, lastProp);
}
}
for (const propKey in nextProps) {
const nextProp = nextProps[propKey];
const lastProp = lastProps[propKey];
if (
nextProps.hasOwnProperty(propKey) &&
nextProp !== lastProp &&
(nextProp != null || lastProp != null)
) {
setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
}
}
}
五、从元素获取属性
export function getPropsFromElement(domElement: Element): Object {
const serverDifferences: { [propName: string]: mixed } = {};
const attributes = domElement.attributes;
for (let i = 0; i < attributes.length; i++) {
const attr = attributes[i];
serverDifferences[getPropNameFromAttributeName(attr.name)] =
attr.name.toLowerCase() === 'style'
? getStylesObjectFromElement(domElement)
: attr.value;
}
return serverDifferences;
}
六、水合属性
备注
listenToNonDelegatedEvent()由 DOMPluginEventSystem#listenToNonDelegatedEvent 实现initInput()由 ReactDOMInput#initInput 实现validateOptionProps()由 ReactDOMOption#validateOptionProps 实现checkControlledValueProps()由 ReactControlledValuePropTypes#checkControlledValueProps 实现validateSelectProps()由 ReactDOMSelect#validateSelectProps 实现validateTextareaProps()由 ReactDOMTextarea#validateTextareaProps 实现mediaEventTypes由 DOMPluginEventSystem#mediaEventTypes 提供enableHydrationChangeEvent由 ReactFeatureFlags#enableHydrationChangeEvent 提供
export function hydrateProperties(
domElement: Element,
tag: string,
props: Object,
hostContext: HostContext,
): boolean {
if (__DEV__) {
validatePropertiesInDevelopment(tag, props);
}
// TODO: Make sure that we check isMounted before firing any of these events.
// 待办:确保在触发任何这些事件之前检查 isMounted。
switch (tag) {
case 'dialog':
listenToNonDelegatedEvent('cancel', domElement);
listenToNonDelegatedEvent('close', domElement);
break;
case 'iframe':
case 'object':
case 'embed':
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the load event.
// 我们监听这个事件,以确保模拟的冒泡监听器仍然会触发 load 事件。
listenToNonDelegatedEvent('load', domElement);
break;
case 'video':
case 'audio':
// We listen to these events in case to ensure emulated bubble
// listeners still fire for all the media events.
// 我们监听这些事件,以确保模拟的冒泡监听器仍然会触发所有媒体事件。
for (let i = 0; i < mediaEventTypes.length; i++) {
listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
}
break;
case 'source':
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the error event.
// 我们监听这个事件,以确保模拟的冒泡监听器仍然会触发错误事件。
listenToNonDelegatedEvent('error', domElement);
break;
case 'img':
case 'image':
case 'link':
// We listen to these events in case to ensure emulated bubble
// listeners still fire for error and load events.
// 我们监听这些事件,以确保模拟的冒泡监听器仍然会触发错误和加载事件。
listenToNonDelegatedEvent('error', domElement);
listenToNonDelegatedEvent('load', domElement);
break;
case 'details':
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the toggle event.
// 我们监听这个事件,以确保模拟的冒泡监听器仍然会触发切换事件。
listenToNonDelegatedEvent('toggle', domElement);
break;
case 'input':
if (__DEV__) {
checkControlledValueProps('input', props);
}
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
// 我们监听这个事件,以确保模拟的冒泡监听器仍然会触发无效事件。
listenToNonDelegatedEvent('invalid', domElement);
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
// 待办:确保检查此组件是否仍未卸载,或进行任何必要的清理,因为我们不再停止跟踪。
validateInputProps(domElement, props);
// For input and textarea we current always set the value property at
// post mount to force it to diverge from attributes. However, for
// option and select we don't quite do the same thing and select
// is not resilient to the DOM state changing so we don't do that here.
// 对于 input 和 textarea,我们当前总是在挂载后设置 value 属性,以强制其与属性
// 值不同。然而,对于 option 和 select,我们并没有完全这样做,而且 select 对
// DOM 状态的变化不够稳健,所以在这里没有这么做。
// TODO: Consider not doing this for input and textarea.
// 待办事项:考虑是否不对 input 和 textarea 这么做。
if (!enableHydrationChangeEvent) {
initInput(
domElement,
props.value,
props.defaultValue,
props.checked,
props.defaultChecked,
props.type,
props.name,
true,
);
}
break;
case 'option':
validateOptionProps(domElement, props);
break;
case 'select':
if (__DEV__) {
checkControlledValueProps('select', props);
}
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
// 我们监听这个事件,以确保模拟的冒泡监听器仍然会触发无效事件。
listenToNonDelegatedEvent('invalid', domElement);
validateSelectProps(domElement, props);
break;
case 'textarea':
if (__DEV__) {
checkControlledValueProps('textarea', props);
}
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
// 我们监听这个事件,以确保模拟的冒泡监听器仍然会触发无效事件。
listenToNonDelegatedEvent('invalid', domElement);
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
// 待办事项:确保我们检查此组件是否仍未卸载,或者进行任何必要的清理,因为我们再也
// 不停止追踪了。
validateTextareaProps(domElement, props);
if (!enableHydrationChangeEvent) {
initTextarea(
domElement,
props.value,
props.defaultValue,
props.children,
);
}
break;
}
const children = props.children;
// For text content children we compare against textContent. This
// might match additional HTML that is hidden when we read it using
// textContent. E.g. "foo" will match "f<span>oo</span>" but that still
// satisfies our requirement. Our requirement is not to produce perfect
// HTML and attributes. Ideally we should preserve structure but it's
// ok not to if the visible content is still enough to indicate what
// even listeners these nodes might be wired up to.
// TODO: Warn if there is more than a single textNode as a child.
// TODO: Should we use domElement.firstChild.nodeValue to compare?
//
// 对于文本内容的子节点,我们使用 textContent 进行比较。这可能会匹配在使用
// textContent 读取时被隐藏的额外 HTML。例如,“foo”会匹配 “f<span>oo</span>”,
// 但这仍然满足我们的要求。我们的要求不是生成完美的 HTML 和属性。理想情况下,我们应该
// 保留结构,但如果可见内容足以表明这些节点可能绑定了哪些事件监听器,偶尔不保留也没
// 关系。
// TODO: 如果子节点中有多个 textNode,要发出警告。
// TODO: 我们是否应该使用 domElement.firstChild.nodeValue 进行比较?
if (
typeof children === 'string' ||
typeof children === 'number' ||
typeof children === 'bigint'
) {
if (
domElement.textContent !== '' + children &&
props.suppressHydrationWarning !== true &&
!checkForUnmatchedText(domElement.textContent, children)
) {
return false;
}
}
if (props.popover != null) {
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the toggle event.
// 我们监听这个事件,以确保模拟的冒泡监听器仍然会触发切换事件。
listenToNonDelegatedEvent('beforetoggle', domElement);
listenToNonDelegatedEvent('toggle', domElement);
}
if (props.onScroll != null) {
listenToNonDelegatedEvent('scroll', domElement);
}
if (props.onScrollEnd != null) {
listenToNonDelegatedEvent('scrollend', domElement);
if (enableScrollEndPolyfill) {
// For use by the polyfill.
// 供填充功能使用。
listenToNonDelegatedEvent('scroll', domElement);
}
}
if (props.onClick != null) {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
// 待办:这种类型转换对于 SVG、MathML 或自定义元素可能不可靠。
trapClickOnNonInteractiveElement(domElement as any as HTMLElement);
}
return true;
}
七、差异化水合属性
备注
isCustomElement()由 isCustomElement#isCustomElement 实现enableViewTransition由 ReactFeatureFlags#enableViewTransition 提供
export function diffHydratedProperties(
domElement: Element,
tag: string,
props: Object,
hostContext: HostContext,
): null | Object {
const serverDifferences: { [propName: string]: mixed } = {};
if (__DEV__) {
const extraAttributes: Set<string> = new Set();
const attributes = domElement.attributes;
for (let i = 0; i < attributes.length; i++) {
const name = attributes[i].name.toLowerCase();
switch (name) {
// Controlled attributes are not validated
// 受控属性未经过验证
// TODO: Only ignore them on controlled tags.
// TODO: 仅在受控标签上忽略它们。
case 'value':
break;
case 'checked':
break;
case 'selected':
break;
case 'vt-name':
case 'vt-update':
case 'vt-enter':
case 'vt-exit':
case 'vt-share':
if (enableViewTransition) {
// View Transition annotations are expected from the Server Runtime.
// However, if they're also specified on the client and don't match
// that's an error.
// 视图转换注解应由服务器运行时提供。但是,如果客户端也指定了注解且不匹配
// 那就是一个错误。
break;
}
// Fallthrough
// 贯穿
default:
// Intentionally use the original name.
// 故意使用原名称。
// See discussion in https://github.com/facebook/react/pull/10676.
extraAttributes.add(attributes[i].name);
}
}
if (isCustomElement(tag, props)) {
diffHydratedCustomComponent(
domElement,
tag,
props,
hostContext,
extraAttributes,
serverDifferences,
);
} else {
diffHydratedGenericElement(
domElement,
tag,
props,
hostContext,
extraAttributes,
serverDifferences,
);
}
if (extraAttributes.size > 0 && props.suppressHydrationWarning !== true) {
warnForExtraAttributes(domElement, extraAttributes, serverDifferences);
}
}
if (Object.keys(serverDifferences).length === 0) {
return null;
}
return serverDifferences;
}
八、补水文字
export function hydrateText(
textNode: Text,
text: string,
parentProps: null | Object,
): boolean {
const isDifferent = textNode.nodeValue !== text;
if (
isDifferent &&
(parentProps === null || parentProps.suppressHydrationWarning !== true) &&
!checkForUnmatchedText(textNode.nodeValue, text)
) {
return false;
}
return true;
}
九、差异水化文本
export function diffHydratedText(textNode: Text, text: string): null | string {
if (textNode.nodeValue === text) {
return null;
}
const normalizedClientText = normalizeMarkupForTextOrAttribute(text);
const normalizedServerText = normalizeMarkupForTextOrAttribute(
textNode.nodeValue,
);
if (normalizedServerText === normalizedClientText) {
return null;
}
return textNode.nodeValue;
}
十、恢复受控状态
备注
restoreControlledInputState()由 ReactDOMInput#restoreControlledInputState 实现restoreControlledTextareaState()由 ReactDOMTextarea#restoreControlledTextareaState 实现restoreControlledSelectState()由 ReactDOMSelect#restoreControlledSelectState 实现
export function restoreControlledState(
domElement: Element,
tag: string,
props: Object,
): void {
switch (tag) {
case 'input':
restoreControlledInputState(domElement, props);
return;
case 'textarea':
restoreControlledTextareaState(domElement, props);
return;
case 'select':
restoreControlledSelectState(domElement, props);
return;
}
}
十一、常量
1. 标准化换行符正则
// HTML parsing normalizes CR and CRLF to LF.
// It also can turn \u0000 into \uFFFD inside attributes.
// HTML 解析会将 CR 和 CRLF 规范化为 LF。
// 它还可以将属性中的 \u0000 转换为 \uFFFD。
// https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream
// If we have a mismatch, it might be caused by that.
// We will still patch up in this case but not fire the warning.
// 如果我们有不匹配,可能就是由此引起的。
// 在这种情况下我们仍会进行修补,但不会发出警告。
// 标准化换行符正则
const NORMALIZE_NEWLINES_REGEX = /\r\n?/g;
// 规范空值和替换正则表达式
const NORMALIZE_NULL_AND_REPLACEMENT_REGEX = /\u0000|\uFFFD/g;
2. XLink 命名空间
// XLink 命名空间
const xlinkNamespace = 'http://www.w3.org/1999/xlink';
// xml 命名空间
const xmlNamespace = 'http://www.w3.org/XML/1998/namespace';
3. 预期的表单操作网址
// 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.')";
十二、变量
1. 已警告从受控变为非受控
// 已警告从受控变为非受控
let didWarnControlledToUncontrolled = false;
// 已警告从非受控到受控
let didWarnUncontrolledToControlled = false;
// 已警告表单操作类型
let didWarnFormActionType = false;
// 已警告表单操作名称
let didWarnFormActionName = false;
// 已警告表单操作目标
let didWarnFormActionTarget = false;
// 已警告表单操作方法
let didWarnFormActionMethod = false;
// 已针对新布尔属性的空值发出警告
let didWarnForNewBooleanPropsWithEmptyValue: { [string]: boolean };
// 已警告弹出目标对象
let didWarnPopoverTargetObject = false;
if (__DEV__) {
didWarnForNewBooleanPropsWithEmptyValue = {};
}
十三、工具
1. 在开发中验证属性
function validatePropertiesInDevelopment(type: string, props: any) {
if (__DEV__) {
validateARIAProperties(type, props);
validateInputProperties(type, props);
validateUnknownProperties(type, props, {
registrationNameDependencies,
possibleRegistrationNames,
});
if (
props.contentEditable &&
!props.suppressContentEditableWarning &&
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.',
);
}
}
}
2. 在开发中验证表单操作
function validateFormActionInDevelopment(
tag: string,
key: string,
value: mixed,
props: any,
) {
if (__DEV__) {
if (value == null) {
return;
}
if (tag === 'form') {
if (key === 'formAction') {
console.error(
'You can only pass the formAction prop to <input> or <button>. Use the action prop on <form>.',
);
} else if (typeof value === 'function') {
if (
(props.encType != null || props.method != 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 (props.target != 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.',
);
}
}
} else if (tag === 'input' || tag === 'button') {
if (key === 'action') {
console.error(
'You can only pass the action prop to <form>. Use the formAction prop on <input> or <button>.',
);
} else if (
tag === 'input' &&
props.type !== 'submit' &&
props.type !== 'image' &&
!didWarnFormActionType
) {
didWarnFormActionType = true;
console.error(
'An input can only specify a formAction along with type="submit" or type="image".',
);
} else if (
tag === 'button' &&
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.',
);
} else if (typeof value === 'function') {
// Function form actions cannot control the form properties
// 表单函数操作不能控制表单属性
if (props.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 (
(props.formEncType != null || props.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 (props.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.',
);
}
}
} else {
if (key === 'action') {
console.error('You can only pass the action prop to <form>.');
} else {
console.error(
'You can only pass the formAction prop to <input> or <button>.',
);
}
}
}
}
3. 警告属性差异
function warnForPropDifference(
propName: string,
serverValue: mixed,
clientValue: mixed,
serverDifferences: { [propName: string]: mixed },
): void {
if (__DEV__) {
if (serverValue === clientValue) {
return;
}
const normalizedClientValue =
normalizeMarkupForTextOrAttribute(clientValue);
const normalizedServerValue =
normalizeMarkupForTextOrAttribute(serverValue);
if (normalizedServerValue === normalizedClientValue) {
return;
}
serverDifferences[propName] = serverValue;
}
}
4. 有视图过渡
function hasViewTransition(htmlElement: HTMLElement): boolean {
return !!(
htmlElement.getAttribute('vt-share') ||
htmlElement.getAttribute('vt-exit') ||
htmlElement.getAttribute('vt-enter') ||
htmlElement.getAttribute('vt-update')
);
}
5. 预期视图转换名称
function isExpectedViewTransitionName(htmlElement: HTMLElement): boolean {
if (!hasViewTransition(htmlElement)) {
// We didn't expect to see a view transition name applied.
// 我们没想到会看到视图过渡名称被应用。
return false;
}
const expectedVtName = htmlElement.getAttribute('vt-name');
const actualVtName: string = (htmlElement.style as any)[
'view-transition-name'
];
if (expectedVtName) {
return expectedVtName === actualVtName;
} else {
// Auto-generated name.
// TODO: If Fizz starts applying a prefix to this name, we need to consider that.
// 自动生成的名称。
// 待办事项:如果 Fizz 开始在此名称前添加前缀,我们需要考虑这一点。
return actualVtName.startsWith('_T_');
}
}
6. 警告额外属性
function warnForExtraAttributes(
domElement: Element,
attributeNames: Set<string>,
serverDifferences: { [propName: string]: mixed },
) {
if (__DEV__) {
attributeNames.forEach(function (attributeName) {
if (attributeName === 'style') {
if (domElement.getAttribute(attributeName) === '') {
// Skip empty style. It's fine.
// 跳过空样式。没问题。
return;
}
const htmlElement = domElement as any as HTMLElement;
const style = htmlElement.style;
const isOnlyVTStyles =
(style.length === 1 && style[0] === 'view-transition-name') ||
(style.length === 2 &&
style[0] === 'view-transition-class' &&
style[1] === 'view-transition-name');
if (isOnlyVTStyles && isExpectedViewTransitionName(htmlElement)) {
// If the only extra style was the view-transition-name that we applied from the Fizz
// runtime, then we should ignore it.
// 如果唯一额外的样式是我们从 Fizz 运行时应用的 view-transition-name,那么我们应该忽略它。
} else {
serverDifferences.style = getStylesObjectFromElement(domElement);
}
} else {
serverDifferences[getPropNameFromAttributeName(attributeName)] =
domElement.getAttribute(attributeName);
}
});
}
}
7. 警告无效的事件监听器
function warnForInvalidEventListener(registrationName: string, listener: any) {
if (__DEV__) {
if (listener === false) {
console.error(
'Expected `%s` listener to be a function, instead got `false`.\n\n' +
'If you used to conditionally omit it with %s={condition && value}, ' +
'pass %s={condition ? value : undefined} instead.',
registrationName,
registrationName,
registrationName,
);
} else {
console.error(
'Expected `%s` listener to be a function, instead got a value of `%s` type.',
registrationName,
typeof listener,
);
}
}
}
8. 规范化HTML
// Parse the HTML and read it back to normalize the HTML string so that it
// can be used for comparison.
// 解析 HTML 并读取它,以规范化 HTML 字符串,以便用于比较。
function normalizeHTML(parent: Element, html: string) {
if (__DEV__) {
// We could have created a separate document here to avoid
// re-initializing custom elements if they exist. But this breaks
// how <noscript> is being handled. So we use the same document.
//
// 我们本可以在这里创建一个独立的文档,以避免在自定义元素存在时重新初始化它们。但这
// 会破坏 <noscript> 的处理方式。因此我们使用相同的文档。
//
// See the discussion in https://github.com/facebook/react/pull/11157.
const testElement =
parent.namespaceURI === MATH_NAMESPACE ||
parent.namespaceURI === SVG_NAMESPACE
? parent.ownerDocument.createElementNS(
parent.namespaceURI as any,
parent.tagName,
)
: parent.ownerDocument.createElement(parent.tagName);
testElement.innerHTML = html;
return testElement.innerHTML;
}
}
9. 规范化文本或属性的标记
function normalizeMarkupForTextOrAttribute(markup: mixed): string {
if (__DEV__) {
checkHtmlStringCoercion(markup);
}
const markupString = typeof markup === 'string' ? markup : '' + (markup: any);
return markupString
.replace(NORMALIZE_NEWLINES_REGEX, '\n')
.replace(NORMALIZE_NULL_AND_REPLACEMENT_REGEX, '');
}
10. 检查未匹配的文本
function checkForUnmatchedText(
serverText: string,
clientText: string | number | bigint,
) {
const normalizedClientText = normalizeMarkupForTextOrAttribute(clientText);
const normalizedServerText = normalizeMarkupForTextOrAttribute(serverText);
if (normalizedServerText === normalizedClientText) {
return true;
}
return false;
}
11. 设置属性
备注
validateTextNesting()由 validateDOMNesting#validateTextNesting 实现setTextContent()由 setTextContent#setTextContent 实现setValueForKnownAttribute()由 DOMPropertyOperations#setValueForKnownAttribute 实现setValueForStyles()由 CSSPropertyOperations#setValueForStyles 实现checkAttributeStringCoercion()由 CheckStringCoercion#checkAttributeStringCoercion 实现listenToNonDelegatedEvent()由 DOMPluginEventSystem#listenToNonDelegatedEvent 实现setValueForAttribute()由 DOMPropertyOperations#setValueForAttribute 实现setValueForNamespacedAttribute()由 DOMPropertyOperations#setValueForNamespacedAttribute 实现registrationNameDependencies()由 EventRegistry#registrationNameDependencies 实现trackHostMutation()由 ReactFiberMutationTracking#trackHostMutation 实现enableSrcObject由 ReactFeatureFlags#enableSrcObject 提供enableTrustedTypesIntegration由 ReactFeatureFlags#enableTrustedTypesIntegration 提供
function setProp(
domElement: Element,
tag: string,
key: string,
value: mixed,
props: any,
prevValue: mixed,
): void {
switch (key) {
case 'children': {
if (typeof value === 'string') {
if (__DEV__) {
validateTextNesting(value, tag, false);
}
// Avoid setting initial textContent when the text is empty. In IE11 setting
// textContent on a <textarea> will cause the placeholder to not
// show within the <textarea> until it has been focused and blurred again.
// 当文本为空时,避免设置初始的 textContent。在 IE11 中,
// 如果在 <textarea> 上设置 textContent,会导致占位符在 <textarea> 中无法
// 显示,直到再次聚焦并失焦后才会显示。
// https://github.com/facebook/react/issues/6731#issuecomment-254874553
const canSetTextContent =
tag !== 'body' && (tag !== 'textarea' || value !== '');
if (canSetTextContent) {
setTextContent(domElement, value);
}
} else if (typeof value === 'number' || typeof value === 'bigint') {
if (__DEV__) {
validateTextNesting('' + value, tag, false);
}
const canSetTextContent = tag !== 'body';
if (canSetTextContent) {
setTextContent(domElement, '' + value);
}
} else {
return;
}
break;
}
// These are very common props and therefore are in the beginning of the 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.
// 这些是非常常见的属性,因此出现在 switch 的开头。
// TODO: aria-label 是一个非常常见的属性,但它允许布尔值,因此不像其他属性
// 但理想情况下也应该加入到这个列表中。
case 'className':
setValueForKnownAttribute(domElement, 'class', value);
break;
case 'tabIndex':
// This has to be case sensitive in SVG.
// 在 SVG 中,这必须区分大小写。
setValueForKnownAttribute(domElement, 'tabindex', value);
break;
case 'dir':
case 'role':
case 'viewBox':
case 'width':
case 'height': {
setValueForKnownAttribute(domElement, key, value);
break;
}
case 'style': {
setValueForStyles(domElement, value, prevValue);
return;
}
// These attributes accept URLs. These must not allow javascript: URLS.
// 这些属性接受 URL。这些属性不能允许 javascript: URL。
case 'data':
if (tag !== 'object') {
setValueForKnownAttribute(domElement, 'data', value);
break;
}
// fallthrough
// 穿透
case 'src': {
if (enableSrcObject && typeof value === 'object' && value !== null) {
// Some tags support object sources like Blob, File, MediaSource and MediaStream.
// 一些标签支持像 Blob、File、MediaSource 和 MediaStream 这样的对象作为
// 来源。
if (tag === 'img' || tag === 'video' || tag === 'audio') {
try {
setSrcObject(domElement, tag, value);
break;
} catch (x) {
// If URL.createObjectURL() errors, it was probably some other object type
// that should be toString:ed instead, so we just fall-through to the normal
// path.
// 如果 URL.createObjectURL() 出错,可能是其他类型的对象。应该调用
// toString 方法,因此我们就直接执行正常的路径。
}
} else {
if (__DEV__) {
try {
// This should always error.
// 这应该总是出错。
URL.revokeObjectURL(URL.createObjectURL(value as any));
if (tag === 'source') {
console.error(
'Passing Blob, MediaSource or MediaStream to <source src> is not supported. ' +
'Pass it directly to <img src>, <video src> or <audio src> instead.',
);
} else {
console.error(
'Passing Blob, MediaSource or MediaStream to <%s src> is not supported.',
tag,
);
}
} catch (x) {}
}
}
}
// Fallthrough
// 贯穿
}
case 'href': {
if (
value === '' &&
// <a href=""> is fine for "reload" links.
// <a href=""> 对于“重新加载”链接是可以的。
!(tag === 'a' && key === 'href')
) {
if (__DEV__) {
if (key === '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.',
key,
key,
);
} 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.',
key,
key,
);
}
}
domElement.removeAttribute(key);
break;
}
if (
value == null ||
typeof value === 'function' ||
typeof value === 'symbol' ||
typeof value === 'boolean'
) {
domElement.removeAttribute(key);
break;
}
// `setAttribute` with objects becomes only `[object]` in IE8/9,
// ('' + value) makes it output the correct toString()-value.
// 在 IE8/9 中,使用对象的 `setAttribute` 只会变成 `[object]`,
// ('' value) 让它输出正确的 toString() 值。
if (__DEV__) {
checkAttributeStringCoercion(value, key);
}
const sanitizedValue = sanitizeURL(
enableTrustedTypesIntegration ? value : '' + (value as any),
) as any;
domElement.setAttribute(key, sanitizedValue);
break;
}
case 'action':
case 'formAction': {
// TODO: Consider moving these special cases to the form, input and button tags.
// 待办事项:考虑将这些特殊情况移到表单、输入和按钮标签中。
if (__DEV__) {
validateFormActionInDevelopment(tag, key, value, props);
}
if (typeof value === 'function') {
// Set a javascript URL that doesn't do anything. We don't expect this to be invoked
// because we'll preventDefault, 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。我们不期望它被调用。因为我们会调
// 用 preventDefault,但如果表单被手动提交或者在 React 捕获事件之前有人调用
// 了 stopPropagation,这种情况仍可能发生。如果使用 CSP 阻止 javascript:
// URL 也没关系。只是不会显示这个错误信息,但 URL 仍会被记录。
domElement.setAttribute(
key,
"javascript:throw new Error('" +
'A React form was unexpectedly submitted. If you called form.submit() manually, ' +
"consider using form.requestSubmit() instead. If you\\'re trying to use " +
'event.stopPropagation() in a submit event handler, consider also calling ' +
'event.preventDefault().' +
"')",
);
break;
} else if (typeof prevValue === 'function') {
// When we're switching off a Server Action that was originally hydrated.
// The server control these fields during SSR that are now trailing.
// The regular diffing doesn't apply since we compare against the previous props.
// Instead, we need to force them to be set to whatever they should be now.
// This would be a lot cleaner if we did this whole fork in the per-tag approach.
// 当我们关闭最初被水合的服务器动作时。服务器在 SSR 期间控制这些现在滞后的字段
// 常规的差异比较不适用,因为我们是与之前的属性进行比较。相反,我们需要强制将它们
// 设置为当前应该的值。如果我们在每个标签的方法中完成整个分支处理,这将更清晰。
if (key === 'formAction') {
if (tag !== 'input') {
// Setting the name here isn't completely safe for inputs if this is switching
// to become a radio button. In that case we let the tag based override take
// control.
// 如果此处切换成单选按钮,为输入设置名称并不完全安全。在那种情况下,我们让
// 基于标签的覆盖来控制。
setProp(domElement, tag, 'name', props.name, props, null);
}
setProp(
domElement,
tag,
'formEncType',
props.formEncType,
props,
null,
);
setProp(domElement, tag, 'formMethod', props.formMethod, props, null);
setProp(domElement, tag, 'formTarget', props.formTarget, props, null);
} else {
setProp(domElement, tag, 'encType', props.encType, props, null);
setProp(domElement, tag, 'method', props.method, props, null);
setProp(domElement, tag, 'target', props.target, props, null);
}
}
if (
value == null ||
typeof value === 'symbol' ||
typeof value === 'boolean'
) {
domElement.removeAttribute(key);
break;
}
// `setAttribute` with objects becomes only `[object]` in IE8/9,
// ('' + value) makes it output the correct toString()-value.
// 在 IE8/9 中,使用对象的 `setAttribute` 只会变成 `[object]`,
// ('' value) 让它输出正确的 toString() 值。
if (__DEV__) {
checkAttributeStringCoercion(value, key);
}
const sanitizedValue = sanitizeURL(
enableTrustedTypesIntegration ? value : '' + (value as any),
) as any;
domElement.setAttribute(key, sanitizedValue);
break;
}
case 'onClick': {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
// TODO: 对于 SVG、MathML 或自定义元素,这种类型转换可能不可靠。
if (value != null) {
if (__DEV__ && typeof value !== 'function') {
warnForInvalidEventListener(key, value);
}
trapClickOnNonInteractiveElement(domElement as any as HTMLElement);
}
return;
}
case 'onScroll': {
if (value != null) {
if (__DEV__ && typeof value !== 'function') {
warnForInvalidEventListener(key, value);
}
listenToNonDelegatedEvent('scroll', domElement);
}
return;
}
case 'onScrollEnd': {
if (value != null) {
if (__DEV__ && typeof value !== 'function') {
warnForInvalidEventListener(key, value);
}
listenToNonDelegatedEvent('scrollend', domElement);
if (enableScrollEndPolyfill) {
// For use by the polyfill.
// 供填充使用。
listenToNonDelegatedEvent('scroll', domElement);
}
}
return;
}
case 'dangerouslySetInnerHTML': {
if (value != null) {
if (typeof value !== 'object' || !('__html' in value)) {
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 nextHtml: any = value.__html;
if (nextHtml != null) {
if (props.children != null) {
throw new Error(
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
);
}
domElement.innerHTML = nextHtml;
}
}
break;
}
// Note: `option.selected` is not updated if `select.multiple` is
// disabled with `removeAttribute`. We have special logic for handling this.
// 注意:如果使用 `removeAttribute` 禁用 `select.multiple`,`option.
// selected` 不会更新。我们有专门的逻辑来处理这种情况。
case 'multiple': {
(domElement as any).multiple =
value && typeof value !== 'function' && typeof value !== 'symbol';
break;
}
case 'muted': {
(domElement as any).muted =
value && typeof value !== 'function' && typeof value !== 'symbol';
break;
}
case 'suppressContentEditableWarning':
case 'suppressHydrationWarning':
// 保留
case 'defaultValue': // Reserved
case 'defaultChecked':
case 'innerHTML':
case 'ref': {
// TODO: `ref` is pretty common, should we move it up?
// Noop
// 待办:`ref` 很常见,我们是否应该把它提前?
// 无操作
break;
}
case 'autoFocus': {
// We polyfill it separately on the client during commit.
// We could have excluded it in the property list instead of
// adding a special case here, but then it wouldn't be emitted
// on server rendering (but we *do* want to emit it in SSR).
// 我们在提交期间在客户端单独做 polyfill。我们本可以在属性列表中将其排除,而不是
// 在这里添加特殊情况,但那样它在服务器渲染时就不会被输出了(但我们*确实*希望在
// SSR 中输出它)。
break;
}
case 'xlinkHref': {
if (
value == null ||
typeof value === 'function' ||
typeof value === 'boolean' ||
typeof value === 'symbol'
) {
domElement.removeAttribute('xlink:href');
break;
}
// `setAttribute` with objects becomes only `[object]` in IE8/9,
// ('' + value) makes it output the correct toString()-value.
// 在 IE8/9 中,使用对象的 `setAttribute` 只会变成 `[object]`,
// ('' value) 让它输出正确的 toString() 值。
if (__DEV__) {
checkAttributeStringCoercion(value, key);
}
const sanitizedValue = sanitizeURL(
enableTrustedTypesIntegration ? value : '' + (value as any),
) as any;
domElement.setAttributeNS(xlinkNamespace, 'xlink:href', sanitizedValue);
break;
}
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).
// The SVG attributes are case-sensitive. Since the HTML attributes are
// insensitive they also work even though we canonically use lower case.
//
// 布尔字符串
// 这些是接受 “true” 和 “false” 的“枚举”属性。在 React 中,我们允许用户传递
// `true` 和 `false`,尽管从技术上讲它们不是布尔属性(它们会被强制转换为字符
// 串)。 SVG 属性是区分大小写的。由于 HTML 属性是不区分大小写的。即使我们规范上
// 使用小写字母,它们也能正常工作。
if (
value != null &&
typeof value !== 'function' &&
typeof value !== 'symbol'
) {
if (__DEV__) {
checkAttributeStringCoercion(value, key);
}
domElement.setAttribute(
key,
enableTrustedTypesIntegration ? (value as any) : '' + (value as any),
);
} else {
domElement.removeAttribute(key);
}
break;
}
// Boolean
// 布尔值
case 'inert': {
if (__DEV__) {
if (value === '' && !didWarnForNewBooleanPropsWithEmptyValue[key]) {
didWarnForNewBooleanPropsWithEmptyValue[key] = 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.',
key,
);
}
}
}
// 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': {
if (value && typeof value !== 'function' && typeof value !== 'symbol') {
domElement.setAttribute(key, '');
} else {
domElement.removeAttribute(key);
}
break;
}
// Overloaded Boolean
// 重载布尔值
case 'capture':
case 'download': {
// An attribute that can be used as a flag as well as with a value.
// When true, it should be present (set either to an empty string or its name).
// When false, it should be omitted.
// For any other value, should be present with that value.
// 一个属性,可以用作标志,也可以带有值。当为 true 时,应存在(要么设置为空字符
// 串,要么设置为其名称)。当为 false 时,应省略。对于任何其他值,应存在并带有该
// 值。
if (value === true) {
domElement.setAttribute(key, '');
} else if (
value !== false &&
value != null &&
typeof value !== 'function' &&
typeof value !== 'symbol'
) {
if (__DEV__) {
checkAttributeStringCoercion(value, key);
}
domElement.setAttribute(key, value as any);
} else {
domElement.removeAttribute(key);
}
break;
}
case 'cols':
case 'rows':
case 'size':
case 'span': {
// These are HTML attributes that must be positive numbers.
// 这些是必须为正数的 HTML 属性。
if (
value != null &&
typeof value !== 'function' &&
typeof value !== 'symbol' &&
!isNaN(value) &&
(value as any) >= 1
) {
if (__DEV__) {
checkAttributeStringCoercion(value, key);
}
domElement.setAttribute(key, value as any);
} else {
domElement.removeAttribute(key);
}
break;
}
case 'rowSpan':
case 'start': {
// These are HTML attributes that must be numbers.
// 这些是必须为数字的 HTML 属性。
if (
value != null &&
typeof value !== 'function' &&
typeof value !== 'symbol' &&
!isNaN(value)
) {
if (__DEV__) {
checkAttributeStringCoercion(value, key);
}
domElement.setAttribute(key, value as any);
} else {
domElement.removeAttribute(key);
}
break;
}
case 'popover':
listenToNonDelegatedEvent('beforetoggle', domElement);
listenToNonDelegatedEvent('toggle', domElement);
setValueForAttribute(domElement, 'popover', value);
break;
case 'xlinkActuate':
setValueForNamespacedAttribute(
domElement,
xlinkNamespace,
'xlink:actuate',
value,
);
break;
case 'xlinkArcrole':
setValueForNamespacedAttribute(
domElement,
xlinkNamespace,
'xlink:arcrole',
value,
);
break;
case 'xlinkRole':
setValueForNamespacedAttribute(
domElement,
xlinkNamespace,
'xlink:role',
value,
);
break;
case 'xlinkShow':
setValueForNamespacedAttribute(
domElement,
xlinkNamespace,
'xlink:show',
value,
);
break;
case 'xlinkTitle':
setValueForNamespacedAttribute(
domElement,
xlinkNamespace,
'xlink:title',
value,
);
break;
case 'xlinkType':
setValueForNamespacedAttribute(
domElement,
xlinkNamespace,
'xlink:type',
value,
);
break;
case 'xmlBase':
setValueForNamespacedAttribute(
domElement,
xmlNamespace,
'xml:base',
value,
);
break;
case 'xmlLang':
setValueForNamespacedAttribute(
domElement,
xmlNamespace,
'xml:lang',
value,
);
break;
case 'xmlSpace':
setValueForNamespacedAttribute(
domElement,
xmlNamespace,
'xml:space',
value,
);
break;
// Properties that should not be allowed on custom elements.
// 不应允许在自定义元素上使用的属性。
case 'is': {
if (__DEV__) {
if (prevValue != null) {
console.error(
'Cannot update the "is" prop after it has been initialized.',
);
}
}
// TODO: We shouldn't actually set this attribute, because we've already
// passed it to createElement. We don't also need the attribute.
// However, our tests currently query for it so it's plausible someone
// else does too so it's break.
// 待办事项:我们实际上不应该设置这个属性,因为我们已经将它传递给了
// createElement。我们不需要再次设置该属性。但是,我们的测试当前会查询它,所以
// 很可能其他人也会使用它,因此这样做会出错。
setValueForAttribute(domElement, 'is', value);
break;
}
case 'innerText':
case 'textContent':
return;
case 'popoverTarget':
if (__DEV__) {
if (
!didWarnPopoverTargetObject &&
value != null &&
typeof value === 'object'
) {
didWarnPopoverTargetObject = true;
console.error(
'The `popoverTarget` prop expects the ID of an Element as a string. Received %s instead.',
value,
);
}
}
// Fall through
// 贯穿
default: {
if (
key.length > 2 &&
(key[0] === 'o' || key[0] === 'O') &&
(key[1] === 'n' || key[1] === 'N')
) {
if (
__DEV__ &&
registrationNameDependencies.hasOwnProperty(key) &&
value != null &&
typeof value !== 'function'
) {
warnForInvalidEventListener(key, value);
}
// Updating events doesn't affect the visuals.
// 更新事件不会影响视觉效果。
return;
} else {
const attributeName = getAttributeAlias(key);
setValueForAttribute(domElement, attributeName, value);
}
}
}
// To avoid marking things as host mutations we do early returns above.
// 为了避免将内容标记为宿主突变,我们在上面提前返回。
trackHostMutation();
}
12. 在自定义元素上设置属性
备注
setValueForStyles()由 CSSPropertyOperations#setValueForStyles 实现setTextContent()由 setTextContent#setTextContent 实现listenToNonDelegatedEvent()由 DOMPluginEventSystem#listenToNonDelegatedEvent 实现setValueForPropertyOnCustomComponent()由 DOMPropertyOperations#setValueForPropertyOnCustomComponent 实现trackHostMutation()由 ReactFiberMutationTracking#trackHostMutation 实现
function setPropOnCustomElement(
domElement: Element,
tag: string,
key: string,
value: mixed,
props: any,
prevValue: mixed,
): void {
switch (key) {
case 'style': {
setValueForStyles(domElement, value, prevValue);
return;
}
case 'dangerouslySetInnerHTML': {
if (value != null) {
if (typeof value !== 'object' || !('__html' in value)) {
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 nextHtml: any = value.__html;
if (nextHtml != null) {
if (props.children != null) {
throw new Error(
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
);
}
domElement.innerHTML = nextHtml;
}
}
break;
}
case 'children': {
if (typeof value === 'string') {
setTextContent(domElement, value);
} else if (typeof value === 'number' || typeof value === 'bigint') {
setTextContent(domElement, '' + value);
} else {
return;
}
break;
}
case 'onScroll': {
if (value != null) {
if (__DEV__ && typeof value !== 'function') {
warnForInvalidEventListener(key, value);
}
listenToNonDelegatedEvent('scroll', domElement);
}
return;
}
case 'onScrollEnd': {
if (value != null) {
if (__DEV__ && typeof value !== 'function') {
warnForInvalidEventListener(key, value);
}
listenToNonDelegatedEvent('scrollend', domElement);
if (enableScrollEndPolyfill) {
// For use by the polyfill.
// 供填充功能使用。
listenToNonDelegatedEvent('scroll', domElement);
}
}
return;
}
case 'onClick': {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
// TODO:对于 SVG、MathML 或自定义元素,这种类型转换可能不安全。
if (value != null) {
if (__DEV__ && typeof value !== 'function') {
warnForInvalidEventListener(key, value);
}
trapClickOnNonInteractiveElement(domElement as any as HTMLElement);
}
return;
}
case 'suppressContentEditableWarning':
case 'suppressHydrationWarning':
case 'innerHTML':
case 'ref': {
// Noop
// 空
return;
}
// 属性
case 'innerText': // Properties
case 'textContent':
return;
// Fall through
// 贯穿
default: {
if (registrationNameDependencies.hasOwnProperty(key)) {
if (__DEV__ && value != null && typeof value !== 'function') {
warnForInvalidEventListener(key, value);
}
return;
} else {
setValueForPropertyOnCustomComponent(domElement, key, value);
// We track mutations inside this call.
// 我们在此调用中跟踪变更。
return;
}
}
}
// To avoid marking things as host mutations we do early returns above.
// 为了避免将某些操作标记为宿主突变,我们在上面进行了提前返回。
trackHostMutation();
}
13. 从属性名获取属性名
function getPropNameFromAttributeName(attrName: string): string {
switch (attrName) {
case 'class':
return 'className';
case 'for':
return 'htmlFor';
// TODO: The rest of the aliases.
// 待办:其余的别名。
default:
return attrName;
}
}
14. 从元素获取样式对象
function getStylesObjectFromElement(domElement: Element): {
[styleName: string]: string;
} {
const serverValueInObjectForm: { [prop: string]: string } = {};
const htmlElement: HTMLElement = domElement as any;
const style = htmlElement.style;
for (let i = 0; i < style.length; i++) {
const styleName: string = style[i];
// TODO: We should use the original prop value here if it is equivalent.
// TODO: We could use the original client capitalization if the equivalent
// other capitalization exists in the DOM.
// 待办:如果等价,我们应该在这里使用原始的 prop 值。
// 待办:如果 DOM 中存在等价的其他大小写,我们可以使用原始客户端大小写。
if (
styleName === 'view-transition-name' &&
isExpectedViewTransitionName(htmlElement)
) {
// This is a view transition name added by the Fizz runtime, not the user's props.
// 这是由 Fizz 运行时添加的视图过渡名称,而不是用户的属性。
} else {
serverValueInObjectForm[styleName] = style.getPropertyValue(styleName);
}
}
return serverValueInObjectForm;
}
15. diff已水化样式
备注
createDangerousStringForStyles()由 CSSPropertyOperations#createDangerousStringForStyles 实现
function diffHydratedStyles(
domElement: Element,
value: mixed,
serverDifferences: { [propName: string]: mixed },
): void {
if (value != null && typeof value !== 'object') {
if (__DEV__) {
console.error(
'The `style` prop expects a mapping from style properties to values, ' +
"not a string. For example, style={{marginRight: spacing + 'em'}} when " +
'using JSX.',
);
}
return;
}
// First we compare the string form and see if it's equivalent.
// This lets us bail out on anything that used to pass in this form.
// It also lets us compare anything that's not parsed by this browser.
// 首先我们比较字符串形式,看它是否相等。这使我们能够排除任何以前以这种形式通过的情况。
// 它还允许我们比较任何该浏览器未解析的内容。
const clientValue = createDangerousStringForStyles(value);
const serverValue = domElement.getAttribute('style');
if (serverValue === clientValue) {
return;
}
const normalizedClientValue = normalizeMarkupForTextOrAttribute(clientValue);
const normalizedServerValue = normalizeMarkupForTextOrAttribute(serverValue);
if (normalizedServerValue === normalizedClientValue) {
return;
}
if (
// Trailing semi-colon means this was regenerated.
// 行尾分号表示这是重新生成的。
normalizedServerValue[normalizedServerValue.length - 1] === ';' &&
// TODO: Should we just ignore any style if the style as been manipulated?
// 待办:如果样式已被修改,我们是否应该直接忽略它?
hasViewTransition(domElement as any)
) {
// If this had a view transition we might have applied a view transition
// name/class and removed it. If that happens, the style attribute gets
// regenerated from the style object. This means we've lost the format
// that we sent from the server and is unable to diff it. We just treat
// it as passing even if it should be a mismatch in this edge case.
// 如果这里有一个视图过渡,我们可能会应用一个视图过渡名称/类并将其移除。如果发生这种
// 情况,style 属性将会从 style 对象重新生成。这意味着我们已经丢失了从服务器发送过
// 来的格式,并且无法进行差异比较。在这种极端情况下,即使本应不匹配,我们也只是将其视
// 为通过。
return;
}
// Otherwise, we create the object from the DOM for the diff view.
// 否则,我们从 DOM 创建对象用于差异视图。
serverDifferences.style = getStylesObjectFromElement(domElement);
}
16. 填充属性
备注
checkAttributeStringCoercion()由 CheckStringCoercion#checkAttributeStringCoercion 实现
function hydrateAttribute(
domElement: Element,
propKey: string,
attributeName: string,
value: any,
extraAttributes: Set<string>,
serverDifferences: { [propName: string]: mixed },
): void {
extraAttributes.delete(attributeName);
const serverValue = domElement.getAttribute(attributeName);
if (serverValue === null) {
switch (typeof value) {
case 'undefined':
case 'function':
case 'symbol':
case 'boolean':
return;
}
} else {
if (value == null) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
// 我们本来不应该有这个属性,但我们有了,所以读取它以显示错误信息。
} else {
switch (typeof value) {
case 'function':
case 'symbol':
case 'boolean':
break;
default: {
if (__DEV__) {
checkAttributeStringCoercion(value, propKey);
}
if (serverValue === '' + value) {
return;
}
}
}
}
}
warnForPropDifference(propKey, serverValue, value, serverDifferences);
}
17. 填充布尔属性
function hydrateBooleanAttribute(
domElement: Element,
propKey: string,
attributeName: string,
value: any,
extraAttributes: Set<string>,
serverDifferences: { [propName: string]: mixed },
): void {
extraAttributes.delete(attributeName);
const serverValue = domElement.getAttribute(attributeName);
if (serverValue === null) {
switch (typeof value) {
case 'function':
case 'symbol':
return;
}
if (!value) {
return;
}
} else {
switch (typeof value) {
case 'function':
case 'symbol':
break;
default: {
if (value) {
// If this was a boolean, it doesn't matter what the value is
// the fact that we have it is the same as the expected.
// As long as it's positive.
// 如果这是一个布尔值,值本身无关紧要。关键是我们拥有它,这与预期相同。
// 只要它为真即可。
return;
}
}
}
}
warnForPropDifference(propKey, serverValue, value, serverDifferences);
}
18. 水合过载布尔属性
备注
checkAttributeStringCoercion()由 CheckStringCoercion#checkAttributeStringCoercion 实现
function hydrateOverloadedBooleanAttribute(
domElement: Element,
propKey: string,
attributeName: string,
value: any,
extraAttributes: Set<string>,
serverDifferences: { [propName: string]: mixed },
): void {
extraAttributes.delete(attributeName);
const serverValue = domElement.getAttribute(attributeName);
if (serverValue === null) {
switch (typeof value) {
case 'undefined':
case 'function':
case 'symbol':
return;
default:
if (value === false) {
return;
}
}
} else {
if (value == null) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
// 我们本不应该有这个属性,但它存在,所以读取它以获取错误信息。
} else {
switch (typeof value) {
case 'function':
case 'symbol':
break;
case 'boolean':
if (value === true && serverValue === '') {
return;
}
break;
default: {
if (__DEV__) {
checkAttributeStringCoercion(value, propKey);
}
if (serverValue === '' + value) {
return;
}
}
}
}
}
warnForPropDifference(propKey, serverValue, value, serverDifferences);
}
19. 水合布尔属性
备注
checkAttributeStringCoercion()由 CheckStringCoercion#checkAttributeStringCoercion 实现
function hydrateBooleanishAttribute(
domElement: Element,
propKey: string,
attributeName: string,
value: any,
extraAttributes: Set<string>,
serverDifferences: { [propName: string]: mixed },
): void {
extraAttributes.delete(attributeName);
const serverValue = domElement.getAttribute(attributeName);
if (serverValue === null) {
switch (typeof value) {
case 'undefined':
case 'function':
case 'symbol':
return;
}
} else {
if (value == null) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
// 我们本不应该有这个属性,但它存在,所以读取它的错误信息。
} else {
switch (typeof value) {
case 'function':
case 'symbol':
break;
default: {
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
if (serverValue === '' + (value as any)) {
return;
}
}
}
}
}
warnForPropDifference(propKey, serverValue, value, serverDifferences);
}
20. 填充数值属性
备注
checkAttributeStringCoercion()由 CheckStringCoercion#checkAttributeStringCoercion 实现
function hydrateNumericAttribute(
domElement: Element,
propKey: string,
attributeName: string,
value: any,
extraAttributes: Set<string>,
serverDifferences: { [propName: string]: mixed },
): void {
extraAttributes.delete(attributeName);
const serverValue = domElement.getAttribute(attributeName);
if (serverValue === null) {
switch (typeof value) {
case 'undefined':
case 'function':
case 'symbol':
case 'boolean':
return;
default:
if (isNaN(value)) {
return;
}
}
} else {
if (value == null) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
// 我们本来不应该有这个属性,但我们有了,所以读取它以显示错误信息。
} else {
switch (typeof value) {
case 'function':
case 'symbol':
case 'boolean':
break;
default: {
if (isNaN(value)) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
// 我们本来不应该有这个属性,但我们有了,所以读取它以显示错误信息。
break;
}
if (__DEV__) {
checkAttributeStringCoercion(value, propKey);
}
if (serverValue === '' + value) {
return;
}
}
}
}
}
warnForPropDifference(propKey, serverValue, value, serverDifferences);
}
21. 填充正数属性
备注
checkAttributeStringCoercion()由 CheckStringCoercion#checkAttributeStringCoercion 实现
function hydratePositiveNumericAttribute(
domElement: Element,
propKey: string,
attributeName: string,
value: any,
extraAttributes: Set<string>,
serverDifferences: { [propName: string]: mixed },
): void {
extraAttributes.delete(attributeName);
const serverValue = domElement.getAttribute(attributeName);
if (serverValue === null) {
switch (typeof value) {
case 'undefined':
case 'function':
case 'symbol':
case 'boolean':
return;
default:
if (isNaN(value) || value < 1) {
return;
}
}
} else {
if (value == null) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
// 我们本不应该有这个属性,但它存在,所以读取它的错误信息。
} else {
switch (typeof value) {
case 'function':
case 'symbol':
case 'boolean':
break;
default: {
if (isNaN(value) || value < 1) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
// 我们本不应该有这个属性,但它存在,所以读取它以获取错误信息。
break;
}
if (__DEV__) {
checkAttributeStringCoercion(value, propKey);
}
if (serverValue === '' + value) {
return;
}
}
}
}
}
warnForPropDifference(propKey, serverValue, value, serverDifferences);
}
22. 水合已清理的属性
备注
checkAttributeStringCoercion()由 CheckStringCoercion#checkAttributeStringCoercion 实现
function hydrateSanitizedAttribute(
domElement: Element,
propKey: string,
attributeName: string,
value: any,
extraAttributes: Set<string>,
serverDifferences: { [propName: string]: mixed },
): void {
extraAttributes.delete(attributeName);
const serverValue = domElement.getAttribute(attributeName);
if (serverValue === null) {
switch (typeof value) {
case 'undefined':
case 'function':
case 'symbol':
case 'boolean':
return;
}
} else {
if (value == null) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
// 我们本不应该有这个属性,但它存在,所以读取它的错误信息。
} else {
switch (typeof value) {
case 'function':
case 'symbol':
case 'boolean':
break;
default: {
if (__DEV__) {
checkAttributeStringCoercion(value, propKey);
}
const sanitizedValue = sanitizeURL('' + value);
if (serverValue === sanitizedValue) {
return;
}
}
}
}
}
warnForPropDifference(propKey, serverValue, value, serverDifferences);
}
23. 填充源对象属性
function hydrateSrcObjectAttribute(
domElement: Element,
value: Blob,
extraAttributes: Set<string>,
serverDifferences: { [propName: string]: mixed },
): void {
const attributeName = 'src';
extraAttributes.delete(attributeName);
const serverValue = domElement.getAttribute(attributeName);
if (serverValue != null && value != null) {
const size = value.size;
const type = value.type;
if (typeof size === 'number' && typeof type === 'string') {
if (serverValue.indexOf('data:' + type + ';base64,') === 0) {
// For Blobs we don't bother reading the actual data but just diff by checking if
// the byte length size of the Blob maches the length of the data url.
// 对于 Blob,我们不去读取实际数据,只是通过检查 Blob 的字节长度是否与数据 URL 的长度匹配来进行差异比较。
const prefixLength = 5 + type.length + 8;
let byteLength = ((serverValue.length - prefixLength) / 4) * 3;
if (serverValue[serverValue.length - 1] === '=') {
byteLength--;
}
if (serverValue[serverValue.length - 2] === '=') {
byteLength--;
}
if (byteLength === size) {
return;
}
}
}
}
warnForPropDifference('src', serverValue, value, serverDifferences);
}
24. 已水化自定义组件差异
备注
getValueForAttributeOnCustomComponent()由 DOMPropertyOperations#getValueForAttributeOnCustomComponent 实现getValueForAttributeOnCustomComponent()由 DOMPropertyOperations#getValueForAttributeOnCustomComponent 实现registrationNameDependencies由 EventRegistry#registrationNameDependencies 提供HostContextNamespaceNone由 ReactFiberConfigDOM 提供
function diffHydratedCustomComponent(
domElement: Element,
tag: string,
props: Object,
hostContext: HostContext,
extraAttributes: Set<string>,
serverDifferences: { [propName: string]: mixed },
) {
for (const propKey in props) {
if (!props.hasOwnProperty(propKey)) {
continue;
}
const value = props[propKey];
if (value == null) {
continue;
}
if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (typeof value !== 'function') {
warnForInvalidEventListener(propKey, value);
}
continue;
}
if (props.suppressHydrationWarning === true) {
// Don't bother comparing. We're ignoring all these warnings.
// 不用比较。我们会忽略所有这些警告。
continue;
}
// Validate that the properties correspond to their expected values.
// 验证属性是否对应其预期值。
switch (propKey) {
case 'children': {
if (typeof value === 'string' || typeof value === 'number') {
warnForPropDifference(
'children',
domElement.textContent,
value,
serverDifferences,
);
}
continue;
}
// Checked above already
// 已经在上面检查过了
case 'suppressContentEditableWarning':
case 'suppressHydrationWarning':
case 'defaultValue':
case 'defaultChecked':
case 'innerHTML':
case 'ref':
// Noop
// 无操作
continue;
case 'dangerouslySetInnerHTML':
const serverHTML = domElement.innerHTML;
const nextHtml = value ? value.__html : undefined;
if (nextHtml != null) {
const expectedHTML = normalizeHTML(domElement, nextHtml);
warnForPropDifference(
propKey,
serverHTML,
expectedHTML,
serverDifferences,
);
}
continue;
case 'style':
extraAttributes.delete(propKey);
diffHydratedStyles(domElement, value, serverDifferences);
continue;
case 'offsetParent':
case 'offsetTop':
case 'offsetLeft':
case 'offsetWidth':
case 'offsetHeight':
case 'isContentEditable':
case 'outerText':
case 'outerHTML':
extraAttributes.delete(propKey.toLowerCase());
if (__DEV__) {
console.error(
'Assignment to read-only property will result in a no-op: `%s`',
propKey,
);
}
continue;
// Fall through
case 'className':
// className is a special cased property on the server to render as an attribute.
// className 是服务器上的一个特殊属性,用于渲染为一个属性。
extraAttributes.delete('class');
const serverValue = getValueForAttributeOnCustomComponent(
domElement,
'class',
value,
);
warnForPropDifference(
'className',
serverValue,
value,
serverDifferences,
);
continue;
default: {
// This is a DEV-only path
// 这是仅用于开发的路径
const hostContextDev: HostContextDev = hostContext as any;
const hostContextProd = hostContextDev.context;
if (
hostContextProd === HostContextNamespaceNone &&
tag !== 'svg' &&
tag !== 'math'
) {
extraAttributes.delete(propKey.toLowerCase());
} else {
extraAttributes.delete(propKey);
}
const valueOnCustomComponent = getValueForAttributeOnCustomComponent(
domElement,
propKey,
value,
);
warnForPropDifference(
propKey,
valueOnCustomComponent,
value,
serverDifferences,
);
}
}
}
}
25. 已水化泛型元素差异
备注
registrationNameDependencies()由 EventRegistry#registrationNameDependencies 实现enableSrcObject()由 ReactFeatureFlags#enableSrcObject 实现getAttributeAlias()由 getAttributeAlias#getAttributeAlias 实现getValueForAttribute()由 DOMPropertyOperations#getValueForAttribute 实现HostContextNamespaceNone由 ReactFiberConfigDOM 提供
function diffHydratedGenericElement(
domElement: Element,
tag: string,
props: Object,
hostContext: HostContext,
extraAttributes: Set<string>,
serverDifferences: { [propName: string]: mixed },
) {
for (const propKey in props) {
if (!props.hasOwnProperty(propKey)) {
continue;
}
const value = props[propKey];
if (value == null) {
continue;
}
if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (typeof value !== 'function') {
warnForInvalidEventListener(propKey, value);
}
continue;
}
if (props.suppressHydrationWarning === true) {
// Don't bother comparing. We're ignoring all these warnings.
// 不用比较。我们会忽略所有这些警告。
continue;
}
// Validate that the properties correspond to their expected values.
// 验证属性是否对应其预期值。
switch (propKey) {
case 'children': {
if (typeof value === 'string' || typeof value === 'number') {
warnForPropDifference(
'children',
domElement.textContent,
value,
serverDifferences,
);
}
continue;
}
// Checked above already
// 已经在上面检查过了
case 'suppressContentEditableWarning':
case 'suppressHydrationWarning':
// 受控属性不会被验证
case 'value': // Controlled attributes are not validated
// 待办:仅在受控标签上忽略它们。
case 'checked': // TODO: Only ignore them on controlled tags.
case 'selected':
case 'defaultValue':
case 'defaultChecked':
case 'innerHTML':
case 'ref':
// Noop
// 无操作
continue;
case 'dangerouslySetInnerHTML':
const serverHTML = domElement.innerHTML;
const nextHtml = value ? value.__html : undefined;
if (nextHtml != null) {
const expectedHTML = normalizeHTML(domElement, nextHtml);
if (serverHTML !== expectedHTML) {
serverDifferences[propKey] = {
__html: serverHTML,
};
}
}
continue;
case 'className':
hydrateAttribute(
domElement,
propKey,
'class',
value,
extraAttributes,
serverDifferences,
);
continue;
case 'tabIndex':
hydrateAttribute(
domElement,
propKey,
'tabindex',
value,
extraAttributes,
serverDifferences,
);
continue;
case 'style':
extraAttributes.delete(propKey);
diffHydratedStyles(domElement, value, serverDifferences);
continue;
case 'multiple': {
extraAttributes.delete(propKey);
const serverValue = (domElement as any).multiple;
warnForPropDifference(propKey, serverValue, value, serverDifferences);
continue;
}
case 'muted': {
extraAttributes.delete(propKey);
const serverValue = (domElement as any).muted;
warnForPropDifference(propKey, serverValue, value, serverDifferences);
continue;
}
case 'autoFocus': {
extraAttributes.delete('autofocus');
const serverValue = (domElement as any).autofocus;
warnForPropDifference(propKey, serverValue, value, serverDifferences);
continue;
}
case 'data':
if (tag !== 'object') {
extraAttributes.delete(propKey);
const serverValue = (domElement as any).getAttribute('data');
warnForPropDifference(propKey, serverValue, value, serverDifferences);
continue;
}
// fallthrough
// 穿透
case 'src': {
if (enableSrcObject && typeof value === 'object' && value !== null) {
// Some tags support object sources like Blob, File, MediaSource and MediaStream.
// 一些标签支持像 Blob、File、MediaSource 和 MediaStream 这样的对象来源。
if (tag === 'img' || tag === 'video' || tag === 'audio') {
try {
// Test if this is a compatible object
// 测试这是否是兼容对象
URL.revokeObjectURL(URL.createObjectURL(value as any));
hydrateSrcObjectAttribute(
domElement,
value,
extraAttributes,
serverDifferences,
);
continue;
} catch (x) {
// If not, just fall through to the normal toString flow.
// 如果没有,就直接进入正常的 toString 流程。
}
} else {
if (__DEV__) {
try {
// This should always error.
// 这应该总是出错。
URL.revokeObjectURL(URL.createObjectURL(value as any));
if (tag === 'source') {
console.error(
'Passing Blob, MediaSource or MediaStream to <source src> is not supported. ' +
'Pass it directly to <img src>, <video src> or <audio src> instead.',
);
} else {
console.error(
'Passing Blob, MediaSource or MediaStream to <%s src> is not supported.',
tag,
);
}
} catch (x) {}
}
}
}
// Fallthrough
// 贯穿
}
case 'href':
if (
value === '' &&
// <a href=""> is fine for "reload" links.
// <a href=""> 对于“重新加载”链接是可以的。
!(tag === 'a' && propKey === 'href') &&
!(tag === 'object' && propKey === 'data')
) {
if (__DEV__) {
if (propKey === '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.',
propKey,
propKey,
);
} 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.',
propKey,
propKey,
);
}
}
continue;
}
hydrateSanitizedAttribute(
domElement,
propKey,
propKey,
value,
extraAttributes,
serverDifferences,
);
continue;
case 'action':
case 'formAction': {
const serverValue = domElement.getAttribute(propKey);
if (typeof value === 'function') {
extraAttributes.delete(propKey.toLowerCase());
// The server can set these extra properties to implement actions.
// So we remove them from the extra attributes warnings.
// 服务器可以设置这些额外属性来实现某些操作。因此我们从额外属性警告中移除
// 它们。
if (propKey === 'formAction') {
extraAttributes.delete('name');
extraAttributes.delete('formenctype');
extraAttributes.delete('formmethod');
extraAttributes.delete('formtarget');
} else {
extraAttributes.delete('enctype');
extraAttributes.delete('method');
extraAttributes.delete('target');
}
// Ideally we should be able to warn if the server value was not a function
// however since the function can return any of these attributes any way it
// wants as a custom progressive enhancement, there's nothing to compare to.
// We can check if the function has the $FORM_ACTION property on the client
// and if it's not, warn, but that's an unnecessary constraint that they
// have to have the extra extension that doesn't do anything on the client.
// 理想情况下,如果服务器的值不是一个函数,我们应该能够发出警告。但是由于函数
// 可以以任何方式返回这些属性中的任意一个,作为自定义的渐进增强,没有可比较的
// 对象。我们可以在客户端检查函数是否具有 $FORM_ACTION 属性,如果没有,则发
// 出警告,但这是一个不必要的约束,他们必须拥有一个在客户端没有任何作用的额外
// 扩展。
continue;
} else if (serverValue === EXPECTED_FORM_ACTION_URL) {
extraAttributes.delete(propKey.toLowerCase());
warnForPropDifference(propKey, 'function', value, serverDifferences);
continue;
}
hydrateSanitizedAttribute(
domElement,
propKey,
propKey.toLowerCase(),
value,
extraAttributes,
serverDifferences,
);
continue;
}
case 'xlinkHref':
hydrateSanitizedAttribute(
domElement,
propKey,
'xlink:href',
value,
extraAttributes,
serverDifferences,
);
continue;
case 'contentEditable': {
// Lower-case Booleanish String
// 小写布尔值风格字符串
hydrateBooleanishAttribute(
domElement,
propKey,
'contenteditable',
value,
extraAttributes,
serverDifferences,
);
continue;
}
case 'spellCheck': {
// Lower-case Booleanish String
// 小写布尔值风格字符串
hydrateBooleanishAttribute(
domElement,
propKey,
'spellcheck',
value,
extraAttributes,
serverDifferences,
);
continue;
}
case 'draggable':
case 'autoReverse':
case 'externalResourcesRequired':
case 'focusable':
case 'preserveAlpha': {
// Case-sensitive Booleanish String
// 区分大小写的布尔字符串
hydrateBooleanishAttribute(
domElement,
propKey,
propKey,
value,
extraAttributes,
serverDifferences,
);
continue;
}
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': {
// Some of these need to be lower case to remove them from the extraAttributes list.
// 其中一些需要小写,以便将它们从 extraAttributes 列表中移除。
hydrateBooleanAttribute(
domElement,
propKey,
propKey.toLowerCase(),
value,
extraAttributes,
serverDifferences,
);
continue;
}
case 'capture':
case 'download': {
hydrateOverloadedBooleanAttribute(
domElement,
propKey,
propKey,
value,
extraAttributes,
serverDifferences,
);
continue;
}
case 'cols':
case 'rows':
case 'size':
case 'span': {
hydratePositiveNumericAttribute(
domElement,
propKey,
propKey,
value,
extraAttributes,
serverDifferences,
);
continue;
}
case 'rowSpan': {
hydrateNumericAttribute(
domElement,
propKey,
'rowspan',
value,
extraAttributes,
serverDifferences,
);
continue;
}
case 'start': {
hydrateNumericAttribute(
domElement,
propKey,
propKey,
value,
extraAttributes,
serverDifferences,
);
continue;
}
case 'xHeight':
hydrateAttribute(
domElement,
propKey,
'x-height',
value,
extraAttributes,
serverDifferences,
);
continue;
case 'xlinkActuate':
hydrateAttribute(
domElement,
propKey,
'xlink:actuate',
value,
extraAttributes,
serverDifferences,
);
continue;
case 'xlinkArcrole':
hydrateAttribute(
domElement,
propKey,
'xlink:arcrole',
value,
extraAttributes,
serverDifferences,
);
continue;
case 'xlinkRole':
hydrateAttribute(
domElement,
propKey,
'xlink:role',
value,
extraAttributes,
serverDifferences,
);
continue;
case 'xlinkShow':
hydrateAttribute(
domElement,
propKey,
'xlink:show',
value,
extraAttributes,
serverDifferences,
);
continue;
case 'xlinkTitle':
hydrateAttribute(
domElement,
propKey,
'xlink:title',
value,
extraAttributes,
serverDifferences,
);
continue;
case 'xlinkType':
hydrateAttribute(
domElement,
propKey,
'xlink:type',
value,
extraAttributes,
serverDifferences,
);
continue;
case 'xmlBase':
hydrateAttribute(
domElement,
propKey,
'xml:base',
value,
extraAttributes,
serverDifferences,
);
continue;
case 'xmlLang':
hydrateAttribute(
domElement,
propKey,
'xml:lang',
value,
extraAttributes,
serverDifferences,
);
continue;
case 'xmlSpace':
hydrateAttribute(
domElement,
propKey,
'xml:space',
value,
extraAttributes,
serverDifferences,
);
continue;
case 'inert':
if (__DEV__) {
if (
value === '' &&
!didWarnForNewBooleanPropsWithEmptyValue[propKey]
) {
didWarnForNewBooleanPropsWithEmptyValue[propKey] = 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.',
propKey,
);
}
}
hydrateBooleanAttribute(
domElement,
propKey,
propKey,
value,
extraAttributes,
serverDifferences,
);
continue;
default: {
if (
// shouldIgnoreAttribute
// We have already filtered out null/undefined and reserved words.
// 应忽略的属性,我们已经过滤掉了 null/undefined 和保留字。
propKey.length > 2 &&
(propKey[0] === 'o' || propKey[0] === 'O') &&
(propKey[1] === 'n' || propKey[1] === 'N')
) {
continue;
}
const attributeName = getAttributeAlias(propKey);
let isMismatchDueToBadCasing = false;
// This is a DEV-only path
// 这是仅用于开发的路径
const hostContextDev: HostContextDev = hostContext as any;
const hostContextProd = hostContextDev.context;
if (
hostContextProd === HostContextNamespaceNone &&
tag !== 'svg' &&
tag !== 'math'
) {
extraAttributes.delete(attributeName.toLowerCase());
} else {
const standardName = getPossibleStandardName(propKey);
if (standardName !== null && standardName !== propKey) {
// If an SVG prop is supplied with bad casing, it will
// be successfully parsed from HTML, but will produce a mismatch
// (and would be incorrectly rendered on the client).
// However, we already warn about bad casing elsewhere.
// So we'll skip the misleading extra mismatch warning in this case.
// 如果一个 SVG 属性使用了错误的大小写,它可以从 HTML 成功解析,但会产生
// 不匹配(并且在客户端会被错误渲染)。然而,我们在其他地方已经对错误的大小
// 写进行了警告。因此,在这种情况下我们将跳过误导性的额外不匹配警告。
isMismatchDueToBadCasing = true;
extraAttributes.delete(standardName);
}
extraAttributes.delete(attributeName);
}
const serverValue = getValueForAttribute(
domElement,
attributeName,
value,
);
if (!isMismatchDueToBadCasing) {
warnForPropDifference(propKey, serverValue, value, serverDifferences);
}
}
}
}
}