React 独特的事件系统
首先要明确的是,React为了兼容全部的浏览器,模拟实现了一套自己的事件系统,也就是我们现在看到的事件都是被React处理过后的,给元素的事件也并不是真正的事件处理函数,所以原生事件中像 return false 阻止默认行为这种方式在 React 中是不生效的。
在 v17 之前,React事件绑定在 document 上,之后绑定在应用对应容器 container 上,可以理解为事件委托,当然也不是所有事件都会进行事件委托,例如 scroll 等还是直接绑定在DOM上的。并且 React 将事件源 event 进行了重写,所以我们拿到的不是真正的事件源。
事件合成
上面提到我们写的事件都绑定在了 document 或 container 上,并且一个事件可能由多个原生事件组成,
registrationNameDependencies 对象保存了每个事件是由哪写原生事件组成的,例如我们绑定一个 onChange,React 会绑定 blur change click … 方法
| 12
 3
 4
 5
 6
 7
 
 | {onBlur: ['blur'],
 onClick: ['click'],
 onClickCapture: ['click'],
 onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
 
 }
 
 | 
事件插件
registrationNameModules 对象保存了每个事件和处理该事件插件的映射,当触发某个事件就会通过这个对象找到对应的插件
| 12
 3
 4
 5
 6
 
 | const registrationNameModules = {onBlur: SimpleEventPlugin,
 onClick: SimpleEventPlugin,
 onClickCapture: SimpleEventPlugin,
 
 }
 
 | 
事件绑定
老版本事件系统:事件监听(addEventListener) -> 捕获阶段执行 -> 冒泡阶段执行
新版本事件系统:捕获阶段执行 -> 事件监听 -> 冒泡阶段执行
老版本的事件系统模拟的冒泡和捕获阶段,其实都是在浏览器的冒泡阶段执行的,新版本的事件系统在创建 fiberRoot 时通过 listenToAllSupportedEvents 方法将全部事件注册完成事件代理,分别绑定了捕获和冒泡事件。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 
 | 
 export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
 if (enableEagerRootListeners) {
 
 if ((rootContainerElement: any)[listeningMarker]) {
 return;
 }
 (rootContainerElement: any)[listeningMarker] = true;
 
 allNativeEvents.forEach((domEventName) => {
 
 if (!nonDelegatedEvents.has(domEventName)) {
 
 listenToNativeEvent(domEventName, false, ((rootContainerElement: any): Element), null,);
 }
 
 listenToNativeEvent(domEventName, true, ((rootContainerElement: any): Element), null,);
 });
 }
 }
 
 | 
这里的 listenToNativeEvent 方法调用到最后就是通过 addEventCaptureListener 和 addEventBubbleListener 来注册冒泡/捕获的事件,本质都是原生的 addEventListener。
另外我们定义的事件函数最后都会保存在都会保存在 DOM 对应 fiber 的 memoizedProps 上

事件触发
绑定事件后,只要触发就会首先执行 React 的统一的事件处理函数 dispatchEvent,该方法调用会依次执行 dispatchEventForPluginEventSystem -> batchedUpdates -> dispatchEventsForPlugins
dispatchEventsForPlugins 触发
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
 
 var nativeEventTarget = getEventTarget(nativeEvent);
 
 var dispatchQueue = [];
 
 extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
 
 processDispatchQueue(dispatchQueue, eventSystemFlags);
 }
 
 | 
其中 extractEvents 会根据不同的事件使用不同的插件生成对应的事件。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 
 | function extractEvents(dispatchQueue: DispatchQueue,
 domEventName: DOMEventName,
 targetInst: null | Fiber,
 nativeEvent: AnyNativeEvent,
 nativeEventTarget: null | EventTarget,
 eventSystemFlags: EventSystemFlags,
 targetContainer: EventTarget,
 ): void {
 
 const reactName = topLevelEventsToReactNames.get(domEventName);
 if (reactName === undefined) {
 return;
 }
 
 let SyntheticEventCtor = SyntheticEvent;
 let reactEventType: string = domEventName;
 
 const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
 const accumulateTargetOnly = !inCapturePhase && domEventName === 'scroll';
 
 const listeners = accumulateSinglePhaseListeners(
 targetInst,
 reactName,
 nativeEvent.type,
 inCapturePhase,
 accumulateTargetOnly,
 );
 if (listeners.length > 0) {
 
 const event = new SyntheticEventCtor(
 reactName,
 reactEventType,
 null,
 nativeEvent,
 nativeEventTarget,
 );
 dispatchQueue.push({event, listeners});
 }
 }
 
 | 
accumulateSinglePhaseListeners
accumulateSinglePhaseListeners 找到 fiber 的 props 对应的事件加入到监听集合,然后进行递归知直到根节点,如果事件不冒泡就停止。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 
 | export function accumulateSinglePhaseListeners(targetFiber: Fiber | null,
 reactName: string | null,
 nativeEventType: string,
 inCapturePhase: boolean,
 accumulateTargetOnly: boolean,
 ): Array<DispatchListener> {
 const captureName = reactName !== null ? reactName + 'Capture' : null;
 const reactEventName = inCapturePhase ? captureName : reactName;
 const listeners: Array<DispatchListener> = [];
 
 let instance = targetFiber;
 let lastHostComponent = null;
 
 
 while (instance !== null) {
 const { stateNode, tag } = instance;
 
 if (tag === HostComponent && stateNode !== null) {
 lastHostComponent = stateNode;
 if (reactEventName !== null) {
 
 const listener = getListener(instance, reactEventName);
 if (listener != null) {
 listeners.push(
 createDispatchListener(instance, listener, lastHostComponent),
 );
 }
 }
 }
 
 if (accumulateTargetOnly) {
 break;
 }
 instance = instance.return;
 }
 return listeners;
 }
 
 | 
processDispatchQueue 执行
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 
 | export function processDispatchQueue(dispatchQueue: DispatchQueue,
 eventSystemFlags: EventSystemFlags,
 ): void {
 
 const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
 for (let i = 0; i < dispatchQueue.length; i++) {
 const { event, listeners } = dispatchQueue[i];
 processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
 }
 
 }
 
 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;
 }
 }
 }
 
 | 
首先判断是捕获阶段,然后遍历事件列表执行 processDispatchQueueItemsInOrder 如果是捕获事件,倒序执行,冒泡事件正序执行。
执行顺序
v16: document 捕获 => 原生捕获 => 原生冒泡 => 合成事件(react)捕获 => 合成事件(react)冒泡 => document 元素冒泡
v17/v18: document 捕获 => 合成事件(react)捕获 => 原生捕获 => 原生冒泡 => 合成事件(react)冒泡 => document 元素冒泡