深入理解React事件机制
React 的事件机制是 React 应用中的一个关键部分,它负责处理用户交互和事件响应。本文将深入讨论 React 事件机制的工作原理,包括事件注册、事件合成和事件分发。
# 事件注册
# Root 节点的统一监听
React 在 createRoot 阶段的时候,就会对 root 节点绑定浏览器所有的事件如 onclick, onchange,message 等,并根据事件冒泡和捕获的机制来区分绑定的方法,因为对于后续的事件触发,React 大部分的事件是通过目标 DOM 的事件冒泡或捕获的方式来集中到 root 节点去处理,而不是单独对于目标 DOM 去绑定(当然有一些事件会做特殊处理,例如 input 的 invalid 就直接绑定给 DOM 对象上,因为没有冒泡或者捕获)。
ps: 如果绑定 input onchange 事件,React 也会帮助你绑定 onInput 事件来更好完成这个事件。
ps2: 由于事件绑定在 IE 和其他浏览器的不同,React 也会帮助兼容各个浏览器。
React 会根据事件类型去区分不同的事件优先级用户主动操作的事件都是 ContinuousEventPriority,而通过 Scheduler 去调度的 render 则根据更新优先级去判断事件优先级;
优先级分为:
- DiscreteEventPriority
- ContinuousEventPriority
- IdleEventPriority
- DefaultEventPriority
根据优先级的不同,设置给 currentUpdatePriority(此处用于后续的调度优先级),但最后还是交给 dispatchEvent 方法去分发事件。
# 虚拟 DOM 的单独注册
以前的 React 版本中,在渲染阶段,会通过对比 props 中的属性,把事件回调储存在 WeakMap 里面,当在事件分发阶段时就会取出来执行。
而在 React 18 版本中,最后在 commit 阶段,仅对一些特殊的事件,例如 invalid 做处理,而其他常见的事件不处理(onclick 会添加一个 noop 函数,这是处理 safari 的 BUG),把整个 props 存储到 fiber 中。等事件分发阶段的时,再从整个 props 中挑取对应的回调来执行。
export function commitUpdate(
domElement: Instance,
updatePayload: any,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object
): void {
// Diff and update the properties.
updateProperties(domElement, type, oldProps, newProps);
// Update the props handle so that we know which props are the ones with
// with current event handlers.
updateFiberProps(domElement, newProps);
}
# 事件合成
React 在触发事件执行 dispatchEvent 事件的时候,根据事件的类型来合成事件,不同的事件类型对应不同的事件插件,每个插件都会添加自己特殊的属性和逻辑。
- SimpleEventPlugin (默认事件插件,所有事件都会使用)
- EnterLeaveEventPlugin (enter/leave 相关的事件,例如 pointerEnter, pointerLeave)
- FormActionEventPlugin (表单相关事件)
- ChangeEventPlugin (change 相关的事件,例如 input onChange 会使用原生各种事件合成实现)
- SelectEventPlugin (select 事件)
- BeforeInputEventPlugin(Input 相关的事件,也是通过原生各种事件来帮忙实现)
在以前的 React 版本中,React 还有事件池的概念,就是所有的 React 事件会从一个池子里获取对象,使用完后归还来减少内存。
如果为了继续使用,不让回收,可以使用下面的方法:
event.persist()
, 告诉 React 不要回收- 缓存并使用
event.nativeEvent
, 这个是原生的事件对象,不会回收
但在 React 18 中,取消了这个概念,直接 new SyntheticBaseEvent 这个事件基类去合成。
function createSyntheticEvent(Interface: EventInterfaceType) {
/**
* Synthetic events are dispatched by event plugins, typically in response to a
* top-level event delegation handler.
*
* These systems should generally use pooling to reduce the frequency of garbage
* collection. The system should check `isPersistent` to determine whether the
* event should be released into the pool after being dispatched. Users that
* need a persisted event should invoke `persist`.
*
* Synthetic events (and subclasses) implement the DOM Level 3 Events API by
* normalizing browser quirks. Subclasses do not necessarily have to implement a
* DOM interface; custom application-specific events can also subclass this.
*/
// $FlowFixMe[missing-this-annot]
function SyntheticBaseEvent(
reactName: string | null,
reactEventType: string,
targetInst: Fiber | null,
nativeEvent: { [propName: string]: mixed, ... },
nativeEventTarget: null | EventTarget
) {
this._reactName = reactName;
this._targetInst = targetInst;
this.type = reactEventType;
this.nativeEvent = nativeEvent;
this.target = nativeEventTarget;
this.currentTarget = null;
for (const propName in Interface) {
if (!Interface.hasOwnProperty(propName)) {
continue;
}
const normalize = Interface[propName];
if (normalize) {
this[propName] = normalize(nativeEvent);
} else {
this[propName] = nativeEvent[propName];
}
}
const defaultPrevented =
nativeEvent.defaultPrevented != null
? nativeEvent.defaultPrevented
: nativeEvent.returnValue === false;
if (defaultPrevented) {
this.isDefaultPrevented = functionThatReturnsTrue;
} else {
this.isDefaultPrevented = functionThatReturnsFalse;
}
this.isPropagationStopped = functionThatReturnsFalse;
return this;
}
// $FlowFixMe[prop-missing] found when upgrading Flow
assign(SyntheticBaseEvent.prototype, {
// $FlowFixMe[missing-this-annot]
preventDefault: function () {
this.defaultPrevented = true;
const event = this.nativeEvent;
if (!event) {
return;
}
if (event.preventDefault) {
event.preventDefault();
// $FlowFixMe[illegal-typeof] - flow is not aware of `unknown` in IE
} else if (typeof event.returnValue !== 'unknown') {
event.returnValue = false;
}
this.isDefaultPrevented = functionThatReturnsTrue;
},
// $FlowFixMe[missing-this-annot]
stopPropagation: function () {
const event = this.nativeEvent;
if (!event) {
return;
}
if (event.stopPropagation) {
event.stopPropagation();
// $FlowFixMe[illegal-typeof] - flow is not aware of `unknown` in IE
} else if (typeof event.cancelBubble !== 'unknown') {
// The ChangeEventPlugin registers a "propertychange" event for
// IE. This event does not support bubbling or cancelling, and
// any references to cancelBubble throw "Member not found". A
// typeof check of "unknown" circumvents this issue (and is also
// IE specific).
event.cancelBubble = true;
}
this.isPropagationStopped = functionThatReturnsTrue;
},
/**
* We release all dispatched `SyntheticEvent`s after each event loop, adding
* them back into the pool. This allows a way to hold onto a reference that
* won't be added back into the pool.
*/
persist: function () {
// Modern event system doesn't use pooling.
},
/**
* Checks if this event should be released back into the pool.
*
* @return {boolean} True if this should not be released, false otherwise.
*/
isPersistent: functionThatReturnsTrue,
});
return SyntheticBaseEvent;
}
# 事件分发
在执行 dispatchEvent 的时候,会根据 DOM 来获取对应 Fiber 对象,然后根据 Fiber 获取 props,并在 props 中获取对应事件的回调。
在新版本中,React 会执行两次事件,包括事件的捕获和冒泡阶段。更接近原生事件执行顺序,提高了事件处理的准确性。
function processDispatchQueueItemsInOrder(
event: ReactSyntheticEvent,
dispatchListeners: Array<DispatchListener>,
inCapturePhase: boolean
): void {
let previousInstance;
if (inCapturePhase) {
for (let i = dispatchListeners.length - 1; i >= 0; i--) {
const { instance, currentTarget, listener } = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
} else {
for (let i = 0; i < dispatchListeners.length; i++) {
const { instance, currentTarget, listener } = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
}
}
在老版本中,React 会在事件底层用一个数组队列来收集 fiber 树上一条分支上的所有的 onClick 和 onClickCapture 事件,遇到捕获阶段执行的事件,比如 onClickCapture,就会通过 unshift 放在数组的前面,如果遇到冒泡阶段执行的事件,比如 onClick,就会通过 push 放在数组的后面,最后依次执行队列中的事件处理函数,模拟事件流。