React事件系统

React 独特的事件系统

首先要明确的是,React为了兼容全部的浏览器,模拟实现了一套自己的事件系统,也就是我们现在看到的事件都是被React处理过后的,给元素的事件也并不是真正的事件处理函数,所以原生事件中像 return false 阻止默认行为这种方式在 React 中是不生效的。

在 v17 之前,React事件绑定在 document 上,之后绑定在应用对应容器 container 上,可以理解为事件委托,当然也不是所有事件都会进行事件委托,例如 scroll 等还是直接绑定在DOM上的。并且 React 将事件源 event 进行了重写,所以我们拿到的不是真正的事件源。

事件合成

上面提到我们写的事件都绑定在了 document 或 container 上,并且一个事件可能由多个原生事件组成,
registrationNameDependencies 对象保存了每个事件是由哪写原生事件组成的,例如我们绑定一个 onChange,React 会绑定 blur change click … 方法

1
2
3
4
5
6
7
{
onBlur: ['blur'],
onClick: ['click'],
onClickCapture: ['click'],
onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
// ...
}

事件插件

registrationNameModules 对象保存了每个事件和处理该事件插件的映射,当触发某个事件就会通过这个对象找到对应的插件

1
2
3
4
5
6
const registrationNameModules = {
onBlur: SimpleEventPlugin,
onClick: SimpleEventPlugin,
onClickCapture: SimpleEventPlugin,
// ...
}

事件绑定

老版本事件系统:事件监听(addEventListener) -> 捕获阶段执行 -> 冒泡阶段执行
新版本事件系统:捕获阶段执行 -> 事件监听 -> 冒泡阶段执行

老版本的事件系统模拟的冒泡和捕获阶段,其实都是在浏览器的冒泡阶段执行的,新版本的事件系统在创建 fiberRoot 时通过 listenToAllSupportedEvents 方法将全部事件注册完成事件代理,分别绑定了捕获和冒泡事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// listenToAllSupportedEvents 是事件绑定的开始
// rootContainerElement 为根节点
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (enableEagerRootListeners) {
// 优化处理,防止多次触发
if ((rootContainerElement: any)[listeningMarker]) {
return;
}
(rootContainerElement: any)[listeningMarker] = true;
// allNativeEvents 保存了大多数的浏览器事件(set集合,v18 保存了81个事件)
allNativeEvents.forEach((domEventName) => {
// nonDelegatedEvents 保存了不冒泡的事件
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 上

memoizedProps.png

事件触发

绑定事件后,只要触发就会首先执行 React 的统一的事件处理函数 dispatchEvent,该方法调用会依次执行 dispatchEventForPluginEventSystem -> batchedUpdates -> dispatchEventsForPlugins

dispatchEventsForPlugins 触发

1
2
3
4
5
6
7
8
9
10
11
// 参数(事件名,冒泡阶段/捕获阶段,事件源,fiber,根节点)
function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
// 找到发生事件的元素的事件源
var nativeEventTarget = getEventTarget(nativeEvent);
// 事件队列
var dispatchQueue = [];
// 收集事件
extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
// 执行事件
processDispatchQueue(dispatchQueue, eventSystemFlags);
}

extractEvents 收集

其中 extractEvents 会根据不同的事件使用不同的插件生成对应的事件。

1
2
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 对应的事件加入到监听集合,然后进行递归知直到根节点,如果事件不冒泡就停止。

1
2
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;

// 从targetFiber开始, 向上遍历, 直到 root 为止·
while (instance !== null) {
const { stateNode, tag } = instance;
// 当节点类型是HostComponent时(如: div, span, button等类型)
if (tag === HostComponent && stateNode !== null) {
lastHostComponent = stateNode;
if (reactEventName !== null) {
// 获取标准的监听函数 (如onClick , onClickCapture等)
const listener = getListener(instance, reactEventName);
if (listener != null) {
listeners.push(
createDispatchListener(instance, listener, lastHostComponent),
);
}
}
}
// 如果只收集目标节点, 则不用向上遍历, 直接退出
if (accumulateTargetOnly) {
break;
}
instance = instance.return;
}
return listeners;
}

processDispatchQueue 执行

1
2
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) {
// 捕获事件倒序遍历listeners
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 {
// 冒泡事件顺序遍历listeners
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 元素冒泡


React事件系统
https://l1ushun.github.io/2023/08/19/react-event/
作者
liu shun
发布于
2023年8月19日