跳到主要内容

change event plugin

一、作用

二、注册事件

备注
function registerEvents() {
registerTwoPhaseEvent('onChange', [
'change',
'click',
'focusin',
'focusout',
'input',
'keydown',
'keyup',
'selectionchange',
]);
}

三、提取事件

备注
/**
* This plugin creates an `onChange` event that normalizes change events
* across form elements. This event fires at a time when it's possible to
* change the element's value without seeing a flicker.
*
* 这个插件创建了一个 `onChange` 事件,用于标准化表单元素的变更事件。当可以在不看到闪烁的情况下
* 更改元素的值时,该事件会触发。
*
* Supported elements are:
* 支持的元素有:
* - input (see `isTextInputElement`)
* - 输入框(参见 `isTextInputElement`)
* - textarea
* - 多行文本框
* - select
* - 选择框
*/
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: null | EventTarget,
) {
const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;

let getTargetInstFunc, handleEventFunc;
if (shouldUseChangeEvent(targetNode)) {
getTargetInstFunc = getTargetInstForChangeEvent;
} else if (isTextInputElement(targetNode as any as HTMLElement)) {
if (isInputEventSupported) {
getTargetInstFunc = getTargetInstForInputOrChangeEvent;
} else {
getTargetInstFunc = getTargetInstForInputEventPolyfill;
handleEventFunc = handleEventsForInputEventPolyfill;
}
} else if (shouldUseClickEvent(targetNode)) {
getTargetInstFunc = getTargetInstForClickEvent;
} else if (
targetInst &&
isCustomElement(targetInst.elementType, targetInst.memoizedProps)
) {
getTargetInstFunc = getTargetInstForChangeEvent;
}

if (getTargetInstFunc) {
const inst = getTargetInstFunc(domEventName, targetInst);
if (inst) {
createAndAccumulateChangeEvent(
dispatchQueue,
inst,
nativeEvent,
nativeEventTarget,
);
return;
}
}

if (handleEventFunc) {
handleEventFunc(domEventName, targetNode, targetInst);
}

// When blurring, set the value attribute for number inputs
// 在失焦时,为数字输入框设置 value 属性
if (domEventName === 'focusout' && targetInst) {
// These props aren't necessarily the most current but we warn for changing
// between controlled and uncontrolled, so it doesn't matter and the previous
// code was also broken for changes.
// 这些属性不一定是最新的,但我们会警告在受控和非受控之间切换,因此无所谓,而且之前的代码在更
// 改时也会出错。
const props = targetInst.memoizedProps;
handleControlledInputBlur(targetNode as any as HTMLInputElement, props);
}
}

四、变量

1. 活动元素

备注

源码中 70 - 74 行

/**
* For IE shims
* 针对 IE 的修补程序
*/

// 活动元素
let activeElement = null;
// 活动元素实例
let activeElementInst = null;

2. 是否支持输入事件

备注
/**
* SECTION: handle `input` event
* 部分:处理 `input` 事件
*/
let isInputEventSupported = false;
if (canUseDOM) {
// IE9 claims to support the input event but fails to trigger it when
// deleting text, so we ignore its input events.
// IE9声称支持input事件,但在删除文本时无法触发它,因此我们忽略它的input事件。
isInputEventSupported =
isEventSupported('input') &&
(!document.documentMode || document.documentMode > 9);
}

五、工具

1. 创建并累积变更事件

备注
function createAndAccumulateChangeEvent(
dispatchQueue: DispatchQueue,
inst: null | Fiber,
nativeEvent: AnyNativeEvent,
target: null | EventTarget,
) {
// Flag this event loop as needing state restore.
// 将此事件循环标记为需要恢复状态。
enqueueStateRestore(target as any as Node);
const listeners = accumulateTwoPhaseListeners(inst, 'onChange');
if (listeners.length > 0) {
const event: ReactSyntheticEvent = new SyntheticEvent(
'onChange',
'change',
null,
nativeEvent,
target,
);
dispatchQueue.push({ event, listeners });
}
}

2. 应该使用更改事件

/**
* SECTION: handle `change` event
* 部分:处理 `change` 事件
*/
function shouldUseChangeEvent(elem: Instance | TextInstance) {
const nodeName = elem.nodeName && elem.nodeName.toLowerCase();
return (
nodeName === 'select' ||
(nodeName === 'input' && (elem as any).type === 'file')
);
}

3. 手动调度变更事件

备注
function manualDispatchChangeEvent(nativeEvent: AnyNativeEvent) {
const dispatchQueue: DispatchQueue = [];
createAndAccumulateChangeEvent(
dispatchQueue,
activeElementInst,
nativeEvent,
getEventTarget(nativeEvent),
);

// If change and propertychange bubbled, we'd just bind to it like all the
// other events and have it go through ReactBrowserEventEmitter. Since it
// doesn't, we manually listen for the events and so we have to enqueue and
// process the abstract event manually
//
// 如果 change 和 propertychange 事件会冒泡,我们就像处理其他所有事件一样绑定它,并通过
// ReactBrowserEventEmitter 处理。由于它们不会冒泡,我们必须手动监听这些事件,因此我们需要
// 手动将抽象事件入队并处理。
//
// Batching is necessary here in order to ensure that all event handlers run
// before the next rerender (including event handlers attached to ancestor
// elements instead of directly on the input). Without this, controlled
// components don't work properly in conjunction with event bubbling because
// the component is rerendered and the value reverted before all the event
// handlers can run.
//
// 在这里需要进行批处理,以确保在下一次重新渲染之前,所有事件处理程序都能运行(包括附加到祖先元
// 素而非直接附加到输入框的事件处理程序)。否则,受控组件在与事件冒泡结合使用时无法正常工作,因
// 为组件会在所有事件处理程序运行之前重新渲染并恢复值。
//
// See https://github.com/facebook/react/issues/708.
batchedUpdates(runEventInBatch, dispatchQueue);
}

4. 批量运行事件

备注
function runEventInBatch(dispatchQueue: DispatchQueue) {
processDispatchQueue(dispatchQueue, 0);
}

5. 如果值变化则获取实例

备注
function getInstIfValueChanged(targetInst: Object) {
const targetNode = getNodeFromInstance(targetInst);
if (updateValueIfChanged(((targetNode: any): HTMLInputElement))) {
return targetInst;
}
}

6. 获取变化事件的目标实例

function getTargetInstForChangeEvent(
domEventName: DOMEventName,
targetInst: null | Fiber,
) {
if (domEventName === 'change') {
return targetInst;
}
}

7. 开始监听值变化

/**
* (For IE <=9) Starts tracking propertychange events on the passed-in element
* and override the value property so that we can distinguish user events from
* value changes in JS.
*
* (用于 IE <=9)开始在传入的元素上跟踪 propertychange 事件并重写 value 属性,以便我们能够
* 区分用户事件和 JS 中的值更改。
*/
function startWatchingForValueChange(
target: Instance | TextInstance,
targetInst: null | Fiber,
) {
activeElement = target;
activeElementInst = targetInst;
(activeElement as any).attachEvent('onpropertychange', handlePropertyChange);
}

8. 停止监听值变化

/**
* (For IE <=9) Removes the event listeners from the currently-tracked element,
* if any exists.
*(适用于 IE <=9)从当前跟踪的元素中移除事件监听器(如果存在)
*/
function stopWatchingForValueChange() {
if (!activeElement) {
return;
}
(activeElement as any).detachEvent('onpropertychange', handlePropertyChange);
activeElement = null;
activeElementInst = null;
}

9. 处理属性变化

/**
* (For IE <=9) Handles a propertychange event, sending a `change` event if
* the value of the active element has changed.
*
*(针对 IE <=9)处理 propertychange 事件,如果活动元素的值已更改,则发送 `change` 事件
*/
function handlePropertyChange(nativeEvent) {
if (nativeEvent.propertyName !== 'value') {
return;
}
if (getInstIfValueChanged(activeElementInst)) {
manualDispatchChangeEvent(nativeEvent);
}
}

10. 处理输入事件补丁的事件

function handleEventsForInputEventPolyfill(
domEventName: DOMEventName,
target: Instance | TextInstance,
targetInst: null | Fiber,
) {
if (domEventName === 'focusin') {
// In IE9, propertychange fires for most input events but is buggy and
// doesn't fire when text is deleted, but conveniently, selectionchange
// appears to fire in all of the remaining cases so we catch those and
// forward the event if the value has changed
// 在 IE9 中,propertychange 会在大多数输入事件中触发,但存在漏洞,删除文本时不会触发,
// 不过,方便的是,selectionchange 似乎在所有其他情况下都会触发,所以我们捕捉这些情况并
// 如果值已更改,则转发事件
// In either case, we don't want to call the event handler if the value
// is changed from JS so we redefine a setter for `.value` that updates
// our activeElementValue variable, allowing us to ignore those changes
//
// 在任何情况下,如果值是由 JS 修改的,我们都不想调用事件处理程序,所以我们重新定义了
// `.value` 的 setter,以更新我们的 activeElementValue 变量,从而允许我们忽略这些更改
//
// stopWatching() should be a noop here but we call it just in case we
// missed a blur event somehow.
// stopWatching() 在这里应该是无操作的,但我们还是调用它,以防我们以某种方式错过了 blur 事件。
stopWatchingForValueChange();
startWatchingForValueChange(target, targetInst);
} else if (domEventName === 'focusout') {
stopWatchingForValueChange();
}
}

11. 为输入事件获取目标实例的填充函数

// For IE8 and IE9.
// 适用于 IE8 和 IE9
function getTargetInstForInputEventPolyfill(
domEventName: DOMEventName,
targetInst: null | Fiber,
) {
if (
domEventName === 'selectionchange' ||
domEventName === 'keyup' ||
domEventName === 'keydown'
) {
// On the selectionchange event, the target is just document which isn't
// helpful for us so just check activeElement instead.
//
// 在 selectionchange 事件中,目标只是 document,这对我们没有帮助,所以只需检查 activeElement。
//
// 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire
// propertychange on the first input event after setting `value` from a
// script and fires only keydown, keypress, keyup. Catching keyup usually
// gets it and catching keydown lets us fire an event for the first
// keystroke if user does a key repeat (it'll be a little delayed: right
// before the second keystroke). Other input methods (e.g., paste) seem to
// fire selectionchange normally.
//
// 99%的情况下,keydown 和 keyup 并不是必需的。IE8 在通过脚本设置 `value` 后的第一次输
// 入事件不会触发 propertychange,只会触发 keydown、keypress 和 keyup。通常捕获
// keyup 就能处理,而捕获 keydown 可以在用户按键重复时为第一次按键触发一个事件(会有一点延
// 迟:就在第二次按键之前)。其他输入方式(例如粘贴)似乎会正常触发 selectionchange。
return getInstIfValueChanged(activeElementInst);
}
}

12. 应该使用点击事件

/**
* SECTION: handle `click` event
*/
function shouldUseClickEvent(elem: any) {
// Use the `click` event to detect changes to checkbox and radio inputs.
// This approach works across all browsers, whereas `change` does not fire
// until `blur` in IE8.
//
// 使用 `click` 事件来检测复选框和单选框的变化。这种方法在所有浏览器中都有效,而 `change`
// 事件在 IE8 中要等到 `blur` 后才触发。
const nodeName = elem.nodeName;
return (
nodeName &&
nodeName.toLowerCase() === 'input' &&
(elem.type === 'checkbox' || elem.type === 'radio')
);
}

13. 获取点击事件的目标实例

function getTargetInstForClickEvent(
domEventName: DOMEventName,
targetInst: null | Fiber,
) {
if (domEventName === 'click') {
return getInstIfValueChanged(targetInst);
}
}

14. 获取输入或更改事件的目标实例

function getTargetInstForInputOrChangeEvent(
domEventName: DOMEventName,
targetInst: null | Fiber,
) {
if (domEventName === 'input' || domEventName === 'change') {
return getInstIfValueChanged(targetInst);
}
}

15. 处理受控输入失焦

备注
function handleControlledInputBlur(node: HTMLInputElement, props: any) {
if (node.type !== 'number') {
return;
}

if (!disableInputAttributeSyncing) {
const isControlled = props.value != null;
if (isControlled) {
// If controlled, assign the value attribute to the current value on blur
// 如果是受控组件,在失去焦点时将 value 属性赋值为当前值
setDefaultValue(node as any, 'number', (node as any).value);
}
}
}