跳到主要内容

React DOM input

一、作用

二、验证输入属性

备注
/**
* Implements an <input> host component that allows setting these optional
* props: `checked`, `value`, `defaultChecked`, and `defaultValue`.
*
* If `checked` or `value` are not supplied (or null/undefined), user actions
* that affect the checked state or value will trigger updates to the element.
*
* If they are supplied (and not null/undefined), the rendered element will not
* trigger updates to the element. Instead, the props must change in order for
* the rendered element to be updated.
*
* The rendered element will be initialized as unchecked (or `defaultChecked`)
* with an empty value (or `defaultValue`).
*
* 实现了一个 <input> 宿主组件,允许设置以下可选属性:`checked`、`value`、
* `defaultChecked` 和 `defaultValue`。
*
* 如果未提供 `checked` 或 `value`(或为 null/undefined),用户操作影响选中状态或值
* 时,将触发元素的更新。
*
* 如果提供了它们(且不为 null/undefined),渲染出的元素将不会触发自身更新。相反,需要
* 通过更改属性来更新渲染的元素。
*
* 渲染的元素将初始化为未选中状态(或 `defaultChecked`)且值为空(或
* `defaultValue`)。
*
* See http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html
*/

export function validateInputProps(element: Element, props: Object) {
if (__DEV__) {
// Normally we check for undefined and null the same, but explicitly specifying both
// properties, at all is probably worth warning for. We could move this either direction
// and just make it ok to pass null or just check hasOwnProperty.
// 通常我们会将 undefined 和 null 视为相同来检查,但明确指定这两个属性,可能值得
// 发出警告。我们可以向任意方向调整,或者让传入 null 是可以的,或者仅检查
// hasOwnProperty。
if (
props.checked !== undefined &&
props.defaultChecked !== undefined &&
!didWarnCheckedDefaultChecked
) {
console.error(
'%s contains an input of type %s with both checked and defaultChecked props. ' +
'Input elements must be either controlled or uncontrolled ' +
'(specify either the checked prop, or the defaultChecked prop, but not ' +
'both). Decide between using a controlled or uncontrolled input ' +
'element and remove one of these props. More info: ' +
'https://react.dev/link/controlled-components',
getCurrentFiberOwnerNameInDevOrNull() || 'A component',
props.type,
);
didWarnCheckedDefaultChecked = true;
}
if (
props.value !== undefined &&
props.defaultValue !== undefined &&
!didWarnValueDefaultValue
) {
console.error(
'%s contains an input of type %s with both value and defaultValue props. ' +
'Input elements must be either controlled or uncontrolled ' +
'(specify either the value prop, or the defaultValue prop, but not ' +
'both). Decide between using a controlled or uncontrolled input ' +
'element and remove one of these props. More info: ' +
'https://react.dev/link/controlled-components',
getCurrentFiberOwnerNameInDevOrNull() || 'A component',
props.type,
);
didWarnValueDefaultValue = true;
}
}
}

三、更新输入

备注
export function updateInput(
element: Element,
value: ?string,
defaultValue: ?string,
lastDefaultValue: ?string,
checked: ?boolean,
defaultChecked: ?boolean,
type: ?string,
name: ?string,
) {
const node: HTMLInputElement = element as any;

// Temporarily disconnect the input from any radio buttons.
// Changing the type or name as the same time as changing the checked value
// needs to be atomically applied. We can only ensure that by disconnecting
// the name while do the mutations and then reapply the name after that's done.
// 暂时断开输入与任何单选按钮的连接。 在更改选中值的同时更改类型或名称,需要原子性应用。
// 我们只能通过在进行变更时断开名称,然后在完成后重新应用名称来确保这一点。
node.name = '';

if (
type != null &&
typeof type !== 'function' &&
typeof type !== 'symbol' &&
typeof type !== 'boolean'
) {
if (__DEV__) {
checkAttributeStringCoercion(type, 'type');
}
node.type = type;
} else {
node.removeAttribute('type');
}

if (value != null) {
if (type === 'number') {
if (
(value === 0 && node.value === '') ||
// We explicitly want to coerce to number here if possible.
// 如果可能,我们明确希望在这里强制转换为数字。
node.value != (value as any)
) {
node.value = toString(getToStringValue(value));
}
} else if (node.value !== toString(getToStringValue(value))) {
node.value = toString(getToStringValue(value));
}
} else if (type === 'submit' || type === 'reset') {
// Submit/reset inputs need the attribute removed completely to avoid
// blank-text buttons.
// 提交/重置输入需要完全移除该属性,以避免出现空文本按钮。
node.removeAttribute('value');
}

if (disableInputAttributeSyncing) {
// When not syncing the value attribute, React only assigns a new value
// whenever the defaultValue React prop has changed. When not present,
// React does nothing
// 当不同步 value 属性时,React 只有在 defaultValue React 属性发生变化时才会
// 分配一个新值。如果未设置,React 将不执行任何操作
if (defaultValue != null) {
setDefaultValue(node, type, getToStringValue(defaultValue));
} else if (lastDefaultValue != null) {
node.removeAttribute('value');
}
} else {
// When syncing the value attribute, the value comes from a cascade of
// properties:
// 1. The value React property
// 2. The defaultValue React property
// 3. Otherwise there should be no change
// 在同步 value 属性时,值来源于一系列属性:
// 1. React 的 value 属性
// 2. React 的 defaultValue 属性
// 3. 否则不应有任何更改
if (value != null) {
setDefaultValue(node, type, getToStringValue(value));
} else if (defaultValue != null) {
setDefaultValue(node, type, getToStringValue(defaultValue));
} else if (lastDefaultValue != null) {
node.removeAttribute('value');
}
}

if (disableInputAttributeSyncing) {
// When not syncing the checked attribute, the attribute is directly
// controllable from the defaultValue React property. It needs to be
// updated as new props come in.
// 当不同步 checked 属性时,该属性可以直接
// 从 defaultValue React 属性控制。当有新的 props 传入时,需要更新它。
if (defaultChecked == null) {
node.removeAttribute('checked');
} else {
node.defaultChecked = !!defaultChecked;
}
} else {
// When syncing the checked attribute, it only changes when it needs
// to be removed, such as transitioning from a checkbox into a text input
// 在同步 checked 属性时,只有在需要移除时才会更改,例如从复选框切换到文本输入框时
if (checked == null && defaultChecked != null) {
node.defaultChecked = !!defaultChecked;
}
}

if (checked != null) {
// Important to set this even if it's not a change in order to update input
// value tracking with radio buttons
// TODO: Should really update input value tracking for the whole radio
// button group in an effect or something (similar to #27024)
// 即使没有变化,也重要设置此项以更新单选按钮的输入值跟踪
// TODO: 实际上应该在某种 effect 中更新整个单选按钮组的输入值跟踪(类似于 #27024)
node.checked =
checked && typeof checked !== 'function' && typeof checked !== 'symbol';
}

if (
name != null &&
typeof name !== 'function' &&
typeof name !== 'symbol' &&
typeof name !== 'boolean'
) {
if (__DEV__) {
checkAttributeStringCoercion(name, 'name');
}
node.name = toString(getToStringValue(name));
} else {
node.removeAttribute('name');
}
}

四、初始化输入

备注
export function initInput(
element: Element,
value: ?string,
defaultValue: ?string,
checked: ?boolean,
defaultChecked: ?boolean,
type: ?string,
name: ?string,
isHydrating: boolean,
) {
const node: HTMLInputElement = element as any;

if (
type != null &&
typeof type !== 'function' &&
typeof type !== 'symbol' &&
typeof type !== 'boolean'
) {
if (__DEV__) {
checkAttributeStringCoercion(type, 'type');
}
node.type = type;
}

if (value != null || defaultValue != null) {
const isButton = type === 'submit' || type === 'reset';

// Avoid setting value attribute on submit/reset inputs as it overrides the
// default value provided by the browser. See: #12872
// 避免在提交/重置输入框上设置 value 属性,因为它会覆盖浏览器提供的默认值。
// 参见:#12872
if (isButton && (value === undefined || value === null)) {
// We track the value just in case it changes type later on.
// 我们跟踪该值,以防它以后类型发生变化。
track(element as any);
return;
}

const defaultValueStr =
defaultValue != null ? toString(getToStringValue(defaultValue)) : '';
const initialValue =
value != null ? toString(getToStringValue(value)) : defaultValueStr;

// Do not assign value if it is already set. This prevents user text input
// from being lost during SSR hydration.
// 如果值已设置,则不要赋值。这可以防止在服务器端渲染时用户输入的文本丢失。
if (!isHydrating || enableHydrationChangeEvent) {
if (disableInputAttributeSyncing) {
// When not syncing the value attribute, the value property points
// directly to the React prop. Only assign it if it exists.
// 当不同步 value 属性时,value 属性会直接指向 React 的 prop。只有在它存在
// 时才赋值。
if (value != null) {
// Always assign on buttons so that it is possible to assign an
// empty string to clear button text.
//
// Otherwise, do not re-assign the value property if is empty. This
// potentially avoids a DOM write and prevents Firefox (~60.0.1) from
// prematurely marking required inputs as invalid. Equality is compared
// to the current value in case the browser provided value is not an
// empty string.
// 始终对按钮进行赋值,以便可以将空字符串分配给清除按钮文本。
//
// 否则,如果值为空,请不要重新分配 value 属性。这可能会避免 DOM 写入
// 并防止 Firefox (~60.0.1) 过早地将必填输入标记为无效。比较时与当前
// 值进行比较,以防浏览器提供的值不是空字符串。
if (isButton || toString(getToStringValue(value)) !== node.value) {
node.value = toString(getToStringValue(value));
}
}
} else {
// When syncing the value attribute, the value property should use
// the wrapperState._initialValue property. This uses:
//
// 1. The value React property when present
// 2. The defaultValue React property when present
// 3. An empty string

// 在同步 value 属性时,value 属性应使用 wrapperState._initialValue
// 属性。具体使用顺序为:

// 1. 当存在时使用 React 的 value 属性
// 2. 当存在时使用 React 的 defaultValue 属性
// 3. 使用空字符串
if (initialValue !== node.value) {
node.value = initialValue;
}
}
}

if (disableInputAttributeSyncing) {
// When not syncing the value attribute, assign the value attribute
// directly from the defaultValue React property (when present)
// 当不同步 value 属性时,直接从默认的 React 属性 defaultValue 赋值给
// value 属性(如果存在)
if (defaultValue != null) {
node.defaultValue = defaultValueStr;
}
} else {
// Otherwise, the value attribute is synchronized to the property,
// so we assign defaultValue to the same thing as the value property
// assignment step above.
// 否则,value 属性会与对应的属性同步,
// 因此我们将 defaultValue 分配为与上面 value 属性分配相同的值。
node.defaultValue = initialValue;
}
}

// Normally, we'd just do `node.checked = node.checked` upon initial mount, less this bug
// this is needed to work around a chrome bug where setting defaultChecked
// will sometimes influence the value of checked (even after detachment).
// Reference: https://bugs.chromium.org/p/chromium/issues/detail?id=608416
// We need to temporarily unset name to avoid disrupting radio button groups.
// 通常,我们只需在组件初次挂载时使用 `node.checked = node.checked`,以避免这个
// 错误。这是为了规避 Chrome 的一个 bug,该 bug 会在设置 defaultChecked 时。有
// 时会影响 checked 的值(即使在分离后仍然如此)。
// 参考:https://bugs.chromium.org/p/chromium/issues/detail?id=608416
// 我们需要暂时取消 name 属性,以避免干扰单选按钮组。

const checkedOrDefault = checked != null ? checked : defaultChecked;
// TODO: This 'function' or 'symbol' check isn't replicated in other places
// so this semantic is inconsistent.
// TODO: 这个“函数”或“符号”的检查在其他地方没有重复实现。所以这种语义不一致。
const initialChecked =
typeof checkedOrDefault !== 'function' &&
typeof checkedOrDefault !== 'symbol' &&
!!checkedOrDefault;

if (isHydrating && !enableHydrationChangeEvent) {
// Detach .checked from .defaultChecked but leave user input alone
// 将 .checked 从 .defaultChecked 中分离,但保留用户输入不变
node.checked = node.checked;
} else {
node.checked = !!initialChecked;
}

if (disableInputAttributeSyncing) {
// Only assign the checked attribute if it is defined. This saves
// a DOM write when controlling the checked attribute isn't needed
// (text inputs, submit/reset)
// 只有在定义了 checked 属性时才赋值。当不需要控制 checked 属性时,这样可以节省
// 一次 DOM 写入(文本输入框、提交/重置按钮)
if (defaultChecked != null) {
node.defaultChecked = !node.defaultChecked;
node.defaultChecked = !!defaultChecked;
}
} else {
// When syncing the checked attribute, both the checked property and
// attribute are assigned at the same time using defaultChecked. This uses:
//
// 1. The checked React property when present
// 2. The defaultChecked React property when present
// 3. Otherwise, false
// 在同步 checked 属性时,checked 属性和属性会同时使用 defaultChecked 来赋值。
// 其使用顺序为:

// 1. 当存在时使用 checked React 属性
// 2. 当存在时使用 defaultChecked React 属性
// 3. 否则,使用 false
node.defaultChecked = !node.defaultChecked;
node.defaultChecked = !!initialChecked;
}

// Name needs to be set at the end so that it applies atomically to connected radio buttons.
// 名称需要在最后设置,以便原子地应用于连接的单选按钮。
if (
name != null &&
typeof name !== 'function' &&
typeof name !== 'symbol' &&
typeof name !== 'boolean'
) {
if (__DEV__) {
checkAttributeStringCoercion(name, 'name');
}
node.name = name;
}
track(element as any);
}

五、水合输入

备注
export function hydrateInput(
element: Element,
value: ?string,
defaultValue: ?string,
checked: ?boolean,
defaultChecked: ?boolean,
): void {
const node: HTMLInputElement = element as any;

const defaultValueStr =
defaultValue != null ? toString(getToStringValue(defaultValue)) : '';
const initialValue =
value != null ? toString(getToStringValue(value)) : defaultValueStr;

const checkedOrDefault = checked != null ? checked : defaultChecked;
// TODO: This 'function' or 'symbol' check isn't replicated in other places
// so this semantic is inconsistent.
// TODO: 这个“函数”或“符号”的检查在其他地方没有重复实现
// 所以这种语义不一致。
const initialChecked =
typeof checkedOrDefault !== 'function' &&
typeof checkedOrDefault !== 'symbol' &&
!!checkedOrDefault;

// Detach .checked from .defaultChecked but leave user input alone
// 将 .checked 从 .defaultChecked 中分离,但保留用户输入不变
node.checked = node.checked;

const changed = trackHydrated(node as any, initialValue, initialChecked);
if (changed) {
// If the current value is different, that suggests that the user
// changed it before hydration. Queue a replay of the change event.
// For radio buttons the change event only fires on the selected one.
// 如果当前值不同,这表明用户在水化前更改了它。将更改事件的重放加入队列。
// 对于单选按钮,change 事件仅在选中的按钮上触发。
if (node.type !== 'radio' || node.checked) {
queueChangeEvent(node);
}
}
}

六、恢复受控输入状态

备注
export function restoreControlledInputState(element: Element, props: Object) {
const rootNode: HTMLInputElement = element as any;
updateInput(
rootNode,
props.value,
props.defaultValue,
props.defaultValue,
props.checked,
props.defaultChecked,
props.type,
props.name,
);
const name = props.name;
if (props.type === 'radio' && name != null) {
let queryRoot: Element = rootNode;

while (queryRoot.parentNode) {
queryRoot = queryRoot.parentNode as any as Element;
}

// If `rootNode.form` was non-null, then we could try `form.elements`,
// but that sometimes behaves strangely in IE8. We could also try using
// `form.getElementsByName`, but that will only return direct children
// and won't include inputs that use the HTML5 `form=` attribute. Since
// the input might not even be in a form. It might not even be in the
// document. Let's just use the local `querySelectorAll` to ensure we don't
// miss anything.
// 如果 `rootNode.form` 非空,那么我们可以尝试 `form.elements`,但在 IE8 中
// 有时会表现得很奇怪。我们也可以尝试使用 `form.getElementsByName`,但它只会返
// 回直接子元素,并且不会包含使用 HTML5 `form=` 属性的输入元素。因为该输入可能根
// 本不在表单中,甚至可能不在文档中。我们还是使用本地的 `querySelectorAll` 来确
// 保不遗漏任何内容。
if (__DEV__) {
checkAttributeStringCoercion(name, 'name');
}
const group = queryRoot.querySelectorAll(
'input[name="' +
escapeSelectorAttributeValueInsideDoubleQuotes('' + name) +
'"][type="radio"]',
);

for (let i = 0; i < group.length; i++) {
const otherNode = group[i] as any as HTMLInputElement;
if (otherNode === rootNode || otherNode.form !== rootNode.form) {
continue;
}
// This will throw if radio buttons rendered by different copies of React
// and the same name are rendered into the same form (same as #1939).
// That's probably okay; we don't support it just as we don't support
// mixing React radio buttons with non-React ones.
// 如果由不同的 React 实例渲染的单选按钮使用相同的 name 并被渲染到同一个表单中
// ,这将会抛出错误(与 #1939 相同)。
// 这可能没问题;我们不支持这种情况,就像我们不支持将 React 单选按钮与非 React
// 单选按钮混合使用一样。
const otherProps: any = getFiberCurrentPropsFromNode(otherNode);

if (!otherProps) {
throw new Error(
'ReactDOMInput: Mixing React and non-React radio inputs with the ' +
'same `name` is not supported.',
);
}

// If this is a controlled radio button group, forcing the input that
// was previously checked to update will cause it to be come re-checked
// as appropriate.
// 如果这是一个受控的单选按钮组,强制更新之前选中的输入会导致它重新被选中,这样
// 是正常的。
updateInput(
otherNode,
otherProps.value,
otherProps.defaultValue,
otherProps.defaultValue,
otherProps.checked,
otherProps.defaultChecked,
otherProps.type,
otherProps.name,
);
}

// If any updateInput() call set .checked to true, an input in this group
// (often, `rootNode` itself) may have become unchecked
// 如果任何 updateInput() 调用将 .checked 设置为 true,这个组中的某个输入项
// (通常是 `rootNode` 本身)可能已经变为未选中
for (let i = 0; i < group.length; i++) {
const otherNode = group[i] as any as HTMLInputElement;
if (otherNode.form !== rootNode.form) {
continue;
}
updateValueIfChanged(otherNode);
}
}
}

七、设置默认值

备注
// In Chrome, assigning defaultValue to certain input types triggers input validation.
// For number inputs, the display value loses trailing decimal points. For email inputs,
// Chrome raises "The specified value <x> is not a valid email address".
//
// Here we check to see if the defaultValue has actually changed, avoiding these problems
// when the user is inputting text
// 在 Chrome 中,将 defaultValue 分配给某些输入类型会触发输入验证。对于数字输入,显
// 示值会丢失尾随小数点。对于电子邮件输入,Chrome 会抛出 “指定的值 <x> 不是有效的电子
// 邮件地址”。
//
// 这里我们检查 defaultValue 是否真的发生了变化,从而在用户输入文本时避免这些问题
//
// https://github.com/facebook/react/issues/7253
export function setDefaultValue(
node: HTMLInputElement,
type: ?string,
value: ToStringValue,
) {
if (
// Focused number inputs synchronize on blur. See ChangeEventPlugin.js
// 聚焦的数字输入在失去焦点时同步。详见 ChangeEventPlugin.js
type !== 'number' ||
getActiveElement(node.ownerDocument) !== node
) {
if (node.defaultValue !== toString(value)) {
node.defaultValue = toString(value);
}
}
}

八、变量

1. 已警告默认值

// 已警告默认值
let didWarnValueDefaultValue = false;
// 已警告已检查默认已选中
let didWarnCheckedDefaultChecked = false;