本文基于React版本18.0.0源码
简介
一. 事件注册
在ReactJS现阶段事件系统中提供了五个处理合成事件的Plugin,它们的主要职责将原生事件与ReactJS合成事件进行关联,能在事件处理阶段将对应的原生事件转换成最终ReactJS能处理的合成事件。
// 当前React18提供了以下5个合成事件Plugin
SimpleEventPlugin.registerEvents(); // SimpleEventPlugin 处理一些常用的基础事件,详细请参见DOMEventProperties.js 中的registerSimpleEvents注册函数
EnterLeaveEventPlugin.registerEvents(); // 鼠标移动事件
ChangeEventPlugin.registerEvents(); // 修改事件
SelectEventPlugin.registerEvents(); // 处理选择事件
BeforeInputEventPlugin.registerEvents(); // 处理控件输入前事件
EventRegistry.js为关联合成事件提供了两个API函数,registerEvents使用提供的API对数据结构registrationNameDependencies与allNativeEvents进行数据的更新:
registerTwoPhaseEvent: 维护冒泡与捕获事件映射关系registerDirectEvent: 只提供了冒泡事件名称映射维护
同时支持冒泡和捕获的事件,将调用两次registerDirectEvent,绑定两条映射关系,捕获事件将携带Capture字样
1.1 registrationNameDependencies 合成事件与原生事件映射关系
key值为React 组件Props支持的监听函数,value为处理key事件所对应的Native事件名称
1.2 allNativeEvents 所有支持的Native事件
1.3 XXXXEventPlugin 功能
每个
Plugin承担着对合成事件的注册与收集功能,即 registerEvents与extractEvents两个函数 :
| 函数 | 功能 |
|---|---|
registerEvents | 执行ReactDOMRoot.createRoot初始化前(可以理解为事件绑定前),将合成事件名称与原生事件名称通过registrationNameDependencies全局变量来维护映射关系 |
extractEvents | 在事件触发阶段(可阅读dispatchEventsForPlugins函数),会通过domEventName去匹配创建SyntheticEvent(合成事件),并将事件目标节点及父节点的事件回调放入listener数组,最终将listeners与event push到DispatchQueue中,最终由executeDispatch完成apply调用 |
Plugin承担着原生事件匹配合成事件的重要工作,根据事件类型划分到多个Plugin,passive、capture属性影响着最终挂载在DOM节点上的事件监听数量。
二. 事件绑定
从流程图可以看出
React最终注册EventListener也是通过DOM提供的addEventListener来完成事件的绑定,唯一不同的是React的addEventListener全部绑定在rootContainer(createRoot函数传递进来的根DOM节点),从而通过代理的方式完成整个ReactJS生态链中的事件体系。
createRoot阶段调用listenToAllSupportedEvents将所有allNativeEvents原生事件进行绑定nonDelegatedEvents维护了不需要委托代理的事件,isCapturePhaseListener将设置为了false,listenToNativeEvent将不会去注册捕获事件。listenToNativeEvent最终执行的是addTrappedEventListener,isCapturePhaseListener被转化为eventSystemFlags来携带捕获事件、冒泡事件的信息,对应十进制4 表示捕获,0 表示冒泡getEventPriority维护了不同事件对应的执行级别ContinuousEventPriority,DefaultEventPriority,DiscreteEventPrioritygetCurrentUpdatePriority,IdleEventPriority,createEventListenerWrapperWithPriority通过bind偏函数的特性将domEventName,eventSystemFlags,targetContainer携带到DOM原生listener- 针对支持
Passive对touchstart、touchmove、wheel开启了{ passive : true }, 通过isCapturePhaseListener是否支持捕获来选择 调用addEvent____Listener与addEvent____ListenerWithPassiveFlag,Bubble:冒泡、Capture: 捕获
注: 在listenToNativeEvent 执行过程中selectionchange,将事件绑定到 Document 元素
2.1 addTrappedEventListener函数
function addTrappedEventListener(
targetContainer: EventTarget, // 事件挂载节点,为createRoot传入的根DOM节点,一般情况下大家都喜欢使用root来定义根DOM即 div#root
domEventName: DOMEventName, // 原生浏览器事件名称(小写),例如DOMEventProperties.js 中simpleEventPluginEvents 列举了一些常用的
eventSystemFlags: EventSystemFlags, // 4 表示捕获,0 表示冒泡,详细位移计算请到EventSystemFlags.js查阅
isCapturePhaseListener: boolean, // true 表示捕获阶段,false 表示冒泡阶段
isDeferredListenerForLegacyFBSupport?: boolean,
)
当
listenToAllSupportedEvents执行完绑定操作后,通过浏览器的Event Listeners 工具去观察 div#root 事件,allNativeEvents中注册的事件已经全部被绑定。
并且Listener都指向了ReactDOMEventListener.js源文件,唯一不同的是对应着不同的function函数,根据原生是否支持冒泡、捕获、以及passive属性对应的事件会绑定多个listener,
| 事件 | 函数 |
|---|---|
| 离散事件 | dispatchDiscreteEvent |
| 持续触发事件 | dispatchContinuousEvent |
| 默认事件 | dispatchEvent |
2.2 createEventListenerWrapperWithPriority函数
getEventPriority 根据原生事件domEventName去匹配对应的优先级别,该函数优先级主要划分了DiscreteEventPriority、ContinuousEventPriority、DefaultEventPriority三个标准,当然domEventName值为message,返回的优先级别会依赖getCurrentSchedulerPriorityLevel的结果。
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
// 根据domEventName 匹配对应优先级,重点关注:DiscreteEventPriority、ContinuousEventPriority、DefaultEventPriority
const eventPriority = getEventPriority(domEventName);
let listenerWrapper;
// 根据事件名绑定不同的dispatchXXXXEvent函数
switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}
//最终绑定到DOM节点的事件,addEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean): void;
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
createEventListenerWrapperWithPriority的返回值是什么?从上面代码片段得知最终只是返回了一个dispatchEventXXX的bind新函数,从入参可以获取到最终的bind需要的所有参数,
接下来只需要知道
bind干了什么?
function.bind(thisArg[, arg1[, arg2[, ...]]])
// 伪代码,这里涉及到了偏函数、柯里化函数的概念
// thisArg 不需要携带this对象,传null即可,从第二个参数开始为返回值函数的入参
(null, domEventName, 'click', 0) => (nativeEvent) => void
//最终 createEventListenerWrapperWithPriority返回值为如下,这样就返回值就是一个 只需要一个可变入参的函数(这里只需要在传递一个nativeEvent)。
(nativeEvent) => dispatchEvent(null, domEventName, 'click', 0, nativeEvent)
2.3 关于EventListener.js绑定函数addEvent____Listener与addEvent____ListenerWithPassiveFlag
EventListener.js提供的原生DOM绑定事件函数API,只是将listenerWrapper.bind生成的偏函数挂载到对应DOM节点。
注: 无论是哪种事件最终都会通过调用ReactDOMEventListener.js中的dispatchEvent 来完成最终事件处理,区别仅在于不同合成事件的执行优先级别不一样
三. 合成事件响应
无论是哪种事件类型的
dispatchEvent在绑定阶段时就以 EventListener的形式绑定到DOM上(createRoot的入参DOM节点),想了解具体事件的走向,重点关注ReactDOMEventListener.js中个的dispatchEvent默认事件、dispatchContinuousEvent持续事件、dispatchDiscreteEvent离散事件。
findInstanceBlockingEvent寻找触发事件的Fiber与DOM目标节点,这里需要注意的是返回值是阻塞事件的Fiber对象,真正触发事件的Fiber通过return_targetInst全局变量来维护dispatchEventForPluginEventSystem筛选每个Plugin中的extractEvents去匹配并创建合成事件(具体请参见SyntheticEvent.js中的createSyntheticEvent),accumulate____PhaseListeners格式函数负责收集从触发节点开始往父级一层一层寻找目标节点的Listener将其添加到dispatchQueue队列中executeDispatch完成对合成事件的消费
事件示例
构造一个简单的多层级DOM示例结构,每一层级绑定一个onClick事件Listener
观察一下最终
DispatchQueue队列中listener存放情况
3.1 寻找触发事件的Target元素
findInstanceBlockingEvent该函数主要负责从nativeEvent原生事件中提取触发事件的Fiber节点,当 寻找到触发事件的Fiber后通过ReactDOMEventListener.js文件中的return_targetInst变量缓存目标Fiber节点实例
// Returns a SuspenseInstance or Container if it's blocked.
// The return_targetInst field above is conceptually part of the return value.
export function findInstanceBlockingEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
// TODO: Warn if _enabled is false.
return_targetInst = null;
// 从原生事件中提取触发事件的DOM节点
const nativeEventTarget = getEventTarget(nativeEvent);
// 提取触发事件的DOM中所包含的React Fiber对象,当该DOM不存在Fiber对象,则会一级一级往上寻找父节点,
// 并且获取Fiber
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
if (targetInst !== null) {
// 主要为了检查触发事件的节点Fiber所在的父节点有没有被卸载,如未卸载继续返回当前节Fiber,否则返回null
const nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
// 当所属于的父节点被卸载了,则该事件将不会继续处理,return_targetInst 也将返回null
// This tree has been unmounted already. Dispatch without a target.
targetInst = null;
} else {
// tag 如果为HostRoot(根)或SuspenseComponent(懒加载组件) 时,直接返回阻塞节点Fiber,
// dispatchEvent将直接return 一个对象,dispatchEventForPluginEventSystem将判断其返回值存在,放弃继续处理事件
const tag = nearestMounted.tag;
if (tag === SuspenseComponent) {
const instance = getSuspenseInstanceFromFiber(nearestMounted);
if (instance !== null) {
// Queue the event to be replayed later. Abort dispatching since we
// don't want this event dispatched twice through the event system.
// TODO: If this is the first discrete event in the queue. Schedule an increased
// priority for this boundary.
return instance;
}
// This shouldn't happen, something went wrong but to avoid blocking
// the whole system, dispatch the event without a target.
// TODO: Warn.
targetInst = null;
} else if (tag === HostRoot) {
const root: FiberRoot = nearestMounted.stateNode;
if (isRootDehydrated(root)) {
// If this happens during a replay something went wrong and it might block
// the whole system.
return getContainerFromFiber(nearestMounted);
}
targetInst = null;
} else if (nearestMounted !== targetInst) {
// If we get an event (ex: img onload) before committing that
// component's mount, ignore it for now (that is, treat it as if it was an
// event on a non-React tree). We might also consider queueing events and
// dispatching them after the mount.
targetInst = null;
}
}
}
// 最终触发事件DOM节点的Fiber
return_targetInst = targetInst;
// We're not blocked on anything.
return null;
}
getEventTarget: 提取nativeEvent事件中的DOM信息getClosestInstanceFromNode:根据DOM信息提取节点中的Fiber对象,即包含'__reactFiber$'属性的信息getNearestMountedFiber: 检测Fiber 所属 的顶层父节点是否已经被卸载,即顶层父节点是否为node.tag === HostRootgetSuspenseInstanceFromFibergetContainerFromFiber判断传入的Fiber是否为HostRoot,成立则获取stateNode.containerInfo,否则为null
3.2 事件委托分发处理
DOMPluginEventSystem.js中的``dispatchEventsForPlugins`函数为最终触发阶段的核心函数,比较完整的展示了执行流程:
- 事件的收集与匹配
- 事件队列
dispatchQueue事件的消费执行
dispatchEventForPluginEventSystem函数首先验证targetInst 的合法性,并检查targetContainer是否与当前根节点Fiber中的container相等。若条件符合,则通过batchedUpdates执行dispatchEventsForPlugins` 函数extractEvents会调用合成事件系统中注册的Plugins中的extractEvents方法,根据domEventName创建合成事件SyntheticEvent- 合成事件
Plugins.extractEvents会调用DOMPluginEventSystem.js中以accumulate_____PhaseListeners命名的函数,将Fiber节点注册的合成事件监听器转换为DispatchListener,并最终将{ event: SyntheticEvent, listeners: [DispatchListener] }的格式存放到dispatchQueue中 executeDispatch以冒泡的方式执行dispatchQueue中的listeners
3.2.1 事件的匹配与收集
DispatchQueue 队列由 DOMPluginEventSystem.js 中的 extractEvents 函数完成合成事件的匹配与收集工作。extractEvents 是一个同步函数,按照 SimpleEventPlugin -> EnterLeaveEventPlugin -> ChangeEventPlugin -> SelectEventPlugin -> BeforeInputEventPlugin 的顺序,依次筛选需要处理的合成事件及其对应的冒泡事件监听器。
extractEvents主要由两部分组合而成:
- 合成事件生成: 根据
domEventName原生事件名称(全小写英文单词)去匹配合适的EventPlugin插件,匹配生成SyntheticEvent合成事件 - 获取DOM节点上的合成事件
listener函数: 在DOMPluginEventSystem.js提供了accumulate____PhaseListeners格式的函数,该系列函数主要通过reactName去匹配获取合成事件listener,在递归搜索targetFiber以及所有的父节点的EventListener回调(针对持续触发事件存在部分差异,详细请阅读accumulateTwoPhaseListeners、accumulateEnterLeaveTwoPhaseListeners),最终存放至dispatchQueue
如下图所示:
3.2.2 事件的消费执行
// 触发dispatch队列里面的事件
export function processDispatchQueue(
dispatchQueue: DispatchQueue, // extractEvents 匹配与收集的事件缓存
eventSystemFlags: EventSystemFlags, // 主要包含了 冒泡、捕获这些事件设置,由绑定阶段的createEventListenerWrapperWithPriority 通过bind传递而来
): void {
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
// 触发当前节点递归到根节点路径上同名的事件
// 该顺序主要是保持了SimpleEventPlugin -> EnterLeaveEventPlugin -> ChangeEventPlugin -> SelectEventPlugin -> BeforeInputEventPlugin
for (let i = 0; i < dispatchQueue.length; i++) {
const { event, listeners } = dispatchQueue[i];
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
// event system doesn't use pooling.
}
// This would be a good time to rethrow if any of the event handlers threw.
rethrowCaughtError();
}
processDispatchQueueItemsInOrder 函数职责:
stopPropagation()方法防止调用相同事件的传播- 保证了合成事件的执行顺序: 冒泡事件、捕获事件
//
function processDispatchQueueItemsInOrder(
event: ReactSyntheticEvent,
dispatchListeners: Array<DispatchListener>,
inCapturePhase: boolean,
): void {
let previousInstance;
// 在前文已经介绍了listeners的存放顺序
// react事件系统的冒泡、捕获最终通过数组的正反序读取来决定
if (inCapturePhase) {
// 捕获事件
// 触发顺序为: 从子节点到父节点的顺序依次执行
for (let i = dispatchListeners.length - 1; i >= 0; i--) {
const { instance, currentTarget, listener } = dispatchListeners[i];
// 判断执行队列中是否有listener调用了stopPropagation(),检测到后会中断后续的listener执行
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];
// 判断执行队列中是否有listener调用了stopPropagation(),检测到后会中断后续的listener执行
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
// 消费处理事件
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
}
}
function executeDispatch(
event: ReactSyntheticEvent,
listener: Function,
currentTarget: EventTarget,
): void {
const type = event.type || 'unknown-event';
event.currentTarget = currentTarget;
// invokeGuardedCallbackAndCatchFirstError 在 React 中起到了保护作用,通过捕获和处理用户代码中的错误,确保即使在发生错误时,React 应用也能继续运行。这对于提高应用的稳定性和用户体验至关重要。感兴趣的可以阅读一下shared/ReactErrorUtils
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
event.currentTarget = null;
}
本质上其实通过try catch保证页面执行函数异常不中断。
export function invokeGuardedCallbackAndCatchFirstError<
A,
B,
C,
D,
E,
F,
Context,
>(
name: string | null,
func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,
context: Context,
a: A,
b: B,
c: C,
d: D,
e: E,
f: F,
): void {
invokeGuardedCallback.apply(this, arguments);
if (hasError) {
const error = clearCaughtError();
if (!hasRethrowError) {
hasRethrowError = true;
rethrowError = error;
}
}
}
//invokeGuardedCallback 的实现
function invokeGuardedCallbackProd<A, B, C, D, E, F, Context>(
name: string | null,
func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed,
context: Context,
a: A,
b: B,
c: C,
d: D,
e: E,
f: F,
) {
const funcArgs = Array.prototype.slice.call(arguments, 3);
try {
func.apply(context, funcArgs);
} catch (error) {
this.onError(error);
}
}
四. React事件与DOM原生事件的差异
1. 事件处理方式
React 合成事件
- 合成事件:React 使用合成事件(Synthetic Events),这是一个跨浏览器包装器,标准化了事件对象和行为,提供一致的接口。
- 事件委托:React 通过事件委托机制,将所有事件处理程序附加到根元素上,而不是每个子元素。这提高了性能和内存使用效率,特别是在有大量子元素的情况下。
- 统一接口:合成事件为所有事件提供了统一的接口,消除了跨浏览器差异。
原生 DOM 事件
- 原生事件:原生 DOM 事件是由浏览器直接触发的事件,每个事件处理程序直接附加到对应的 DOM 元素上。
- 事件处理:每个元素都可以独立地处理自己的事件,这可能导致在复杂应用中存在大量事件处理程序,从而增加内存和性能开销。
- 跨浏览器差异:原生 DOM 事件在不同浏览器中可能表现不同,开发者需要处理这些差异。
2. 性能优化
React 合成事件
- 标准化:合成事件为所有事件提供一致的接口,消除了浏览器之间的差异,使得开发者不必处理不同浏览器中的细微差别。
- 稳定性:通过合成事件,React 可以在各种浏览器和版本中提供稳定的事件处理行为。
原生 DOM 事件
- 差异化:不同浏览器对事件的实现可能存在差异,开发者需要编写额外的代码来处理这些差异,确保跨浏览器兼容性。
3. 跨浏览器兼容性
React 合成事件
- 标准化:合成事件为所有事件提供一致的接口,消除了浏览器之间的差异,使得开发者不必处理不同浏览器中的细微差别。
- 稳定性:通过合成事件,React 可以在各种浏览器和版本中提供稳定的事件处理行为。
原生 DOM 事件
- 差异化:不同浏览器对事件的实现可能存在差异,开发者需要编写额外的代码来处理这些差异,确保跨浏览器兼容性。
4. 事件命名
React 合成事件
- 驼峰命名法:React 使用驼峰命名法来定义事件处理程序,例如
onClick、onMouseEnter等。 - 一致性:所有事件处理程序都遵循一致的命名规则,便于阅读和维护。
原生 DOM 事件
- 小写命名法:原生 DOM 事件通常使用小写命名法,例如
onclick、onmouseenter等。 - 不一致性:不同事件的命名规则可能不一致,增加了理解和使用的复杂性。