React 合成事件
概览
从v17.0.0
开始, React 不会再将事件处理添加到 document
上, 而是将事件处理添加到渲染 React 树的根 DOM 容器中.
引入官方提供的图片:

图中清晰的展示了v17.0.0
的改动, 无论是在document
还是根 DOM 容器
上监听事件, 都可以归为事件委托(代理)
(mdn).
注意: react
的事件体系, 不是全部都通过事件委托
来实现的. 有一些特殊情况, 是直接绑定到对应 DOM 元素上的(如:scroll
, load
), 它们都通过listenToNonDelegatedEvent函数进行绑定.
上述特殊事件最大的不同是监听的 DOM 元素不同, 除此之外, 其他地方的实现与正常事件大体一致.
本节讨论的是可以被根 DOM 容器
代理的正常事件.
事件绑定
在前文 React 应用的启动过程中介绍了React
在启动时会创建全局对象, 其中在创建 fiberRoot 对象时, 调用createRootImpl:
1function createRootImpl(container: Container, tag: RootTag, options: void | RootOptions) {
2 // ... 省略无关代码
3 if (enableEagerRootListeners) {
4 const rootContainerElement = container.nodeType === COMMENT_NODE ? container.parentNode : container;
5 listenToAllSupportedEvents(rootContainerElement);
6 }
7 // ... 省略无关代码
8}
listenToAllSupportedEvents函数, 实际上完成了事件代理:
1// ... 省略无关代码
2export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
3 if (enableEagerRootListeners) {
4 // 1. 节流优化, 保证全局注册只被调用一次
5 if ((rootContainerElement: any)[listeningMarker]) {
6 return;
7 }
8 (rootContainerElement: any)[listeningMarker] = true;
9 // 2. 遍历allNativeEvents 监听冒泡和捕获阶段的事件
10 allNativeEvents.forEach((domEventName) => {
11 if (!nonDelegatedEvents.has(domEventName)) {
12 listenToNativeEvent(
13 domEventName,
14 false, // 冒泡阶段监听
15 ((rootContainerElement: any): Element),
16 null
17 );
18 }
19 listenToNativeEvent(
20 domEventName,
21 true, // 捕获阶段监听
22 ((rootContainerElement: any): Element),
23 null
24 );
25 });
26 }
27}
核心逻辑:
- 节流优化, 保证全局注册只被调用一次.
- 遍历
allNativeEvents
, 调用listenToNativeEvent
监听冒泡和捕获阶段的事件.
allNativeEvents
包括了大量的原生事件名称, 它是在DOMPluginEventSystem.js
中被初始化
listenToNativeEvent:
1// ... 省略无关代码
2export function listenToNativeEvent(
3 domEventName: DOMEventName,
4 isCapturePhaseListener: boolean,
5 rootContainerElement: EventTarget,
6 targetElement: Element | null,
7 eventSystemFlags?: EventSystemFlags = 0
8): void {
9 let target = rootContainerElement;
10
11 const listenerSet = getEventListenerSet(target);
12 const listenerSetKey = getListenerSetKey(domEventName, isCapturePhaseListener);
13 // 利用set数据结构, 保证相同的事件类型只会被注册一次.
14 if (!listenerSet.has(listenerSetKey)) {
15 if (isCapturePhaseListener) {
16 eventSystemFlags |= IS_CAPTURE_PHASE;
17 }
18 // 注册事件监听
19 addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener);
20 listenerSet.add(listenerSetKey);
21 }
22}
addTrappedEventListener:
1// ... 省略无关代码
2function addTrappedEventListener(
3 targetContainer: EventTarget,
4 domEventName: DOMEventName,
5 eventSystemFlags: EventSystemFlags,
6 isCapturePhaseListener: boolean,
7 isDeferredListenerForLegacyFBSupport?: boolean
8) {
9 // 1. 构造listener
10 let listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags);
11 let unsubscribeListener;
12 // 2. 注册事件监听
13 if (isCapturePhaseListener) {
14 unsubscribeListener = addEventCaptureListener(targetContainer, domEventName, listener);
15 } else {
16 unsubscribeListener = addEventBubbleListener(targetContainer, domEventName, listener);
17 }
18}
19
20// 注册原生事件 冒泡
21export function addEventBubbleListener(target: EventTarget, eventType: string, listener: Function): Function {
22 target.addEventListener(eventType, listener, false);
23 return listener;
24}
25
26// 注册原生事件 捕获
27export function addEventCaptureListener(target: EventTarget, eventType: string, listener: Function): Function {
28 target.addEventListener(eventType, listener, true);
29 return listener;
30}
从listenToAllSupportedEvents
开始, 调用链路比较长, 最后调用addEventBubbleListener
和addEventCaptureListener
监听了原生事件.
原生 listener
在注册原生事件的过程中, 需要重点关注一下监听函数, 即listener
函数. 它实现了把原生事件派发到react
体系之内, 非常关键.
比如点击 DOM 触发原生事件, 原生事件最后会被派发到react
内部的onClick
函数. listener
函数就是这个由外至内
的关键环节.
listener
是通过createEventListenerWrapperWithPriority
函数产生:
1export function createEventListenerWrapperWithPriority(
2 targetContainer: EventTarget,
3 domEventName: DOMEventName,
4 eventSystemFlags: EventSystemFlags
5): Function {
6 // 1. 根据优先级设置 listenerWrapper
7 const eventPriority = getEventPriorityForPluginSystem(domEventName);
8 let listenerWrapper;
9 switch (eventPriority) {
10 case DiscreteEvent:
11 listenerWrapper = dispatchDiscreteEvent;
12 break;
13 case UserBlockingEvent:
14 listenerWrapper = dispatchUserBlockingUpdate;
15 break;
16 case ContinuousEvent:
17 default:
18 listenerWrapper = dispatchEvent;
19 break;
20 }
21 // 2. 返回 listenerWrapper
22 return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
23}
可以看到, 不同的domEventName
调用getEventPriorityForPluginSystem
后返回不同的优先级, 最终会有 3 种情况:
DiscreteEvent
: 优先级最高, 包括click, keyDown, input
等事件, 源码
UserBlockingEvent
: 优先级适中, 包括drag, scroll
等事件, 源码
ContinuousEvent
: 优先级最低,包括animation, load
等事件, 源码
这 3 种listener
实际上都是对dispatchEvent的包装:
1// ...省略无关代码
2export function dispatchEvent(
3 domEventName: DOMEventName,
4 eventSystemFlags: EventSystemFlags,
5 targetContainer: EventTarget,
6 nativeEvent: AnyNativeEvent
7): void {
8 if (!_enabled) {
9 return;
10 }
11 const blockedOn = attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent);
12}
事件触发
当原生事件触发之后, 首先会进入到dispatchEvent
这个回调函数. 而dispatchEvent
函数是react
事件体系中最关键的函数, 其调用链路较长, 核心步骤如图所示:
重点关注其中 3 个核心环节:
attemptToDispatchEvent
SimpleEventPlugin.extractEvents
processDispatchQueue
关联 fiber
attemptToDispatchEvent把原生事件和fiber树
关联起来.
1export function attemptToDispatchEvent(
2 domEventName: DOMEventName,
3 eventSystemFlags: EventSystemFlags,
4 targetContainer: EventTarget,
5 nativeEvent: AnyNativeEvent
6): null | Container | SuspenseInstance {
7 // ...省略无关代码
8
9 // 1. 定位原生DOM节点
10 const nativeEventTarget = getEventTarget(nativeEvent);
11 // 2. 获取与DOM节点对应的fiber节点
12 let targetInst = getClosestInstanceFromNode(nativeEventTarget);
13 // 3. 通过插件系统, 派发事件
14 dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer);
15 return null;
16}
核心逻辑:
- 定位原生 DOM 节点: 调用
getEventTarget
- 获取与 DOM 节点对应的 fiber 节点: 调用
getClosestInstanceFromNode
- 通过插件系统, 派发事件: 调用
dispatchEventForPluginEventSystem
收集 fiber 上的 listener
dispatchEvent
函数的调用链路中, 通过不同的插件, 处理不同的事件. 其中最常见的事件都会由SimpleEventPlugin.extractEvents
进行处理:
1function extractEvents(
2 dispatchQueue: DispatchQueue,
3 domEventName: DOMEventName,
4 targetInst: null | Fiber,
5 nativeEvent: AnyNativeEvent,
6 nativeEventTarget: null | EventTarget,
7 eventSystemFlags: EventSystemFlags,
8 targetContainer: EventTarget
9): void {
10 const reactName = topLevelEventsToReactNames.get(domEventName);
11 if (reactName === undefined) {
12 return;
13 }
14 let SyntheticEventCtor = SyntheticEvent;
15 let reactEventType: string = domEventName;
16
17 const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
18 const accumulateTargetOnly = !inCapturePhase && domEventName === 'scroll';
19 // 1. 收集所有监听该事件的函数.
20 const listeners = accumulateSinglePhaseListeners(
21 targetInst,
22 reactName,
23 nativeEvent.type,
24 inCapturePhase,
25 accumulateTargetOnly
26 );
27 if (listeners.length > 0) {
28 // 2. 构造合成事件, 添加到派发队列
29 const event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
30 dispatchQueue.push({ event, listeners });
31 }
32}
核心逻辑:
-
收集所有listener
回调
-
构造合成事件(SyntheticEvent
), 添加到派发队列(dispatchQueue
)
构造合成事件
SyntheticEvent, 是react
内部创建的一个对象, 是原生事件的跨浏览器包装器, 拥有和浏览器原生事件相同的接口(stopPropagation
,preventDefault
), 抹平不同浏览器 api 的差异, 兼容性好.
具体的构造过程并不复杂, 可以直接查看源码.
此处我们需要知道, 在Plugin.extractEvents
过程中, 遍历fiber树
找到listener
之后, 就会创建SyntheticEvent
, 加入到dispatchQueue
中, 等待派发.
执行派发
extractEvents
完成之后, 逻辑来到processDispatchQueue, 终于要真正执行派发了.
1export function processDispatchQueue(dispatchQueue: DispatchQueue, eventSystemFlags: EventSystemFlags): void {
2 const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
3 for (let i = 0; i < dispatchQueue.length; i++) {
4 const { event, listeners } = dispatchQueue[i];
5 processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
6 }
7 // ...省略无关代码
8}
9
10function processDispatchQueueItemsInOrder(
11 event: ReactSyntheticEvent,
12 dispatchListeners: Array<DispatchListener>,
13 inCapturePhase: boolean
14): void {
15 let previousInstance;
16 if (inCapturePhase) {
17 // 1. capture事件: 倒序遍历listeners
18 for (let i = dispatchListeners.length - 1; i >= 0; i--) {
19 const { instance, currentTarget, listener } = dispatchListeners[i];
20 if (instance !== previousInstance && event.isPropagationStopped()) {
21 return;
22 }
23 executeDispatch(event, listener, currentTarget);
24 previousInstance = instance;
25 }
26 } else {
27 // 2. bubble事件: 顺序遍历listeners
28 for (let i = 0; i < dispatchListeners.length; i++) {
29 const { instance, currentTarget, listener } = dispatchListeners[i];
30 if (instance !== previousInstance && event.isPropagationStopped()) {
31 return;
32 }
33 executeDispatch(event, listener, currentTarget);
34 previousInstance = instance;
35 }
36 }
37}
在processDispatchQueueItemsInOrder遍历dispatchListeners
数组, 执行executeDispatch派发事件, 在fiber
节点上绑定的listener
函数被执行.
在processDispatchQueueItemsInOrder
函数中, 根据捕获(capture)
或冒泡(bubble)
的不同, 采取了不同的遍历方式:
capture
事件: 从上至下
调用fiber树
中绑定的回调函数, 所以倒序
遍历dispatchListeners
.
bubble
事件: 从下至上
调用fiber树
中绑定的回调函数, 所以顺序
遍历dispatchListeners
.
总结
从架构上来讲, SyntheticEvent打通了从外部原生事件
到内部fiber树
的交互渠道, 使得react
能够感知到浏览器提供的原生事件
, 进而做出不同的响应, 修改fiber树
, 变更视图等.
从实现上讲, 主要分为 3 步:
- 监听原生事件: 对齐
DOM元素
和fiber元素
- 收集
listeners
: 遍历fiber树
, 收集所有监听本事件的listener
函数.
- 派发合成事件: 构造合成事件, 遍历
listeners
进行派发.