通俗易懂的React原理(十二):React的合成事件系统

代码以React v19.1.0为例,https://github.com/facebook/react/tree/v19.1.0 ,并且经过简化,仅保留了关键的逻辑,以便于理解流程

React自己实现的事件系统

如果我们用原生的js,去给dom添加事件,应该用addEventListener,或者添加onclick之类的属性。但是这两种方法,都和我们React的写法不同。

React里,我们想给一个节点添加点击的响应事件,应该用onClick属性。这不是DOM API的内容,而是借助React自己实现的事件系统完成的。并且,React自己实现的事件系统,还会模拟原生事件的捕获和冒泡流程。

React自己实现一套事件系统的原因很多。

首先,这套事件系统是基于事件委托的,它的好处在于不需要真的为每个dom注册和删除事件监听器。

其次,React可以更自由的控制事件的逻辑,比如可以给不同的事件设置不同的优先级(lane),这样事件触发的更新就可以根据优先级不同而区别处理。

在React17的时候,控制事件批量更新也是依赖这个自己实现的事件系统。只不过目前已经不需要在事件系统里实现批量更新了,React的调度机制会在一次渲染中自动合并所有同一优先级的更新,一起完成。

事件委托

React自己实现的事件系统,是基于事件委托实现的。事件委托的思想在于,将事件的监听器加在高层级的dom上面,然后监听到事件后,通过event.target确定事件具体发生在哪个子元素上。

React在18版本开始,将事件委托到了createRoot方法传入的dom节点上,也就是上一篇中我们提到的container。为了方便,我们后面有时候会称它为根dom。

我们还记得上一篇的createRoot方法中,调用了listenToAllSupportedEvents方法。这个方法,就是负责添加事件监听器的。

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
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (!(rootContainerElement: any)[listeningMarker]) {
(rootContainerElement: any)[listeningMarker] = true;
allNativeEvents.forEach(domEventName => {
// We handle selectionchange separately because it
// doesn't bubble and needs to be on the document.
if (domEventName !== 'selectionchange') {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
const ownerDocument =
(rootContainerElement: any).nodeType === DOCUMENT_NODE
? rootContainerElement
: (rootContainerElement: any).ownerDocument;
if (ownerDocument !== null) {
// The selectionchange event also needs deduplication
// but it is attached to the document.
if (!(ownerDocument: any)[listeningMarker]) {
(ownerDocument: any)[listeningMarker] = true;
listenToNativeEvent('selectionchange', false, ownerDocument);
}
}
}
}

这里的rootContainerElement入参,就是createRoot的入参。

我们可以看到,这段代码遍历全部的event,逐个为他们添加监听器,但也对部分事件做了特殊处理。

首先,selectionchange事件被单独添加到了document上。

其次,有一部分事件是nonDelegatedEvents,它们包含了scroll等等事件。它们只会执行一遍listenToNativeEvent,且第二个参数是,表示这些事件只添加了捕获阶段的监听器。而其他大部分事件,则会调用两遍listenToNativeEvent,表示在冒泡和捕获阶段,都添加了事件监听器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
target: EventTarget,
): void {
let eventSystemFlags = 0;
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
}

根据是否为冒泡阶段的监听器,决定是否给eventSystemFlags的IS_CAPTURE_PHASE位置为1,然后传入addTrappedEventListener

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean,
) {
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
// If passive option is not supported, then the event will be
// active and not passive.
let isPassiveListener: void | boolean = undefined;
if (passiveBrowserEventsSupported) {
// Browsers introduced an intervention, making these events
// passive by default on document. React doesn't bind them
// to document anymore, but changing this now would undo
// the performance wins from the change. So we emulate
// the existing behavior manually on the roots now.
// https://github.com/facebook/react/issues/19651
if (
domEventName === 'touchstart' ||
domEventName === 'touchmove' ||
domEventName === 'wheel'
) {
isPassiveListener = true;
}
}

let unsubscribeListener;
// TODO: There are too many combinations here. Consolidate them.
if (isCapturePhaseListener) {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener,
);
} else {
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
}
} else {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener,
);
} else {
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
}
}

首先,通过createEventListenerWrapperWithPriority方法,得到一个事件的回调方法。

然后,有个passive事件的问题,React这里简单的处理了一下。addEventListener的第三个入参支持两种类型,通常我们会传一个布尔值表示是否为捕获阶段监听,其实也可以传入一个options对象。对象可接受的字段中,有个passive字段,当为true的时候表示回调方法里永远不会preventDefault。这个优化会大幅改善部分事件的性能。根据规范,这个值默认是false,可大部分浏览器将文档级节点Window节点、Document节点、Document.body节点上面的部分事件设为了true。

而React由于做了事件委托,不会将这些事件绑定到上述节点上。为了也能改善这些事件的性能,React在浏览器支持的情况下,给这些事件设置监听器的时候,也会传入{passive: true}

addTrappedEventListener的最后,根据是否为捕获阶段,是否需要设置passive,React分了四种情况去执行addEventListener

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
42
export function addEventBubbleListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, false);
return listener;
}

export function addEventCaptureListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, true);
return listener;
}

export function addEventCaptureListenerWithPassiveFlag(
target: EventTarget,
eventType: string,
listener: Function,
passive: boolean,
): Function {
target.addEventListener(eventType, listener, {
capture: true,
passive,
});
return listener;
}

export function addEventBubbleListenerWithPassiveFlag(
target: EventTarget,
eventType: string,
listener: Function,
passive: boolean,
): Function {
target.addEventListener(eventType, listener, {
passive,
});
return listener;
}

这样,React就在createRoot传入的dom节点上,注册了监听事件。而具体的事件回调方法,我们在下面详细展开。

事件回调方法

我们接下来要看具体的回调方法的逻辑了,看看前面被添加上去的listener是什么。

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
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
const eventPriority = getEventPriority(domEventName);
let listenerWrapper;
switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}

首先,根据具体是什么事件,得到事件的优先级。getEventPriority方法可以认为是穷举了dom上的事件,每个事件都被分配了一个的优先级。例如是离散事件优先级,还是持续性事件优先级,或者是默认优先级等等。

这里的优先级会设置在ReactDOMSharedInternals.p上面,后面React要更新状态的时候,会读取这个优先级,决定要产生一次什么lane的更新。其实上一篇,我们将React要开始首次mount,执行updateContainer的时候,就是从``ReactDOMSharedInternals.p`读取的优先级。

根据不同的优先级,listenerWrapper会被赋值为不同的方法,他们之间的区别就是要设置的优先级不同,本质上都是dispatchEvent方法。

最后,return一个方法,会给dispatchEvent方法绑定上事件名称,事件的flag,和container(依旧是createRoot的入参dom)这些参数。

也就是说,无论React里面触发了什么事件(除了少部分没有委托的),发生在哪个React Fiber对应的dom上,都会执行绑定在根dom上面的dispatchEvent方法。

执行回调

假设我们有一个div,上面绑定了一个onClick事件。当我们点击这个div的时候,我们相当于是执行到了绑定在根dom上,onClick冒泡阶段的那个dispatchEvent。那么接下来,这个回调是怎么对应我们写的onClick方法,以及怎么处理模拟原生事件的捕获和冒泡问题呢?

一切问题都在dispatchEvent里了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): void {
let blockedOn = findInstanceBlockingEvent(nativeEvent);
if (blockedOn === null) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
return_targetInst,
targetContainer,
);
clearIfContinuousEvent(domEventName, nativeEvent);
return;
}
}

首先,findInstanceBlockingEvent返回了一个blockedOn,它表示是否有被阻塞的事件。阻塞的原因可能是,页面由服务端渲染,当前正处在客户端水合的过程中,无法响应点击事件。

如果被阻塞了,那么React会尝试将事件入队,等阻塞解除后重新处理。不过我们这里不考虑那么复杂的情况,只看没有被阻塞的情况就好了。

只不过,即便没有阻塞,blockedOn是null,我们也需要看看findInstanceBlockingEvent里面的代码。因为下面用到的模块变量return_targetInst,就是在findInstanceBlockingEvent方法里面被更新的。

寻找事件发生的Fiber

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
42
43
44
45
46
47
48
49
50
51
52
export function findInstanceBlockingEvent(
nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
const nativeEventTarget = getEventTarget(nativeEvent);
return findInstanceBlockingTarget(nativeEventTarget);
}


export function findInstanceBlockingTarget(
targetNode: Node,
): null | Container | SuspenseInstance {
// TODO: Warn if _enabled is false.

return_targetInst = null;

let targetInst = getClosestInstanceFromNode(targetNode);

if (targetInst !== null) {
const nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
// This tree has been unmounted already. Dispatch without a target.
targetInst = null;
} else {
const tag = nearestMounted.tag;
if (tag === SuspenseComponent) {
const instance = getSuspenseInstanceFromFiber(nearestMounted);
if (instance !== null) {
return instance;
}
// This shouldn't happen
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;
}
}
}
return_targetInst = targetInst;
// We're not blocked on anything.
return null;
}

可以看到,这里面还是嵌套调用了很多方法的。

findInstanceBlockingEvent方法中,首先是通过getEventTarget,获取这个原生event所发生的dom,其实就是event.target啦,只不过考虑到兼容性和边界场景,封装成了一个函数。

接下来,找到了事件发生的dom,会调用findInstanceBlockingTarget方法,将dom传进去,去判断是否有发生阻塞,不能响应事件的Fiber。

findInstanceBlockingTarget方法中,会调用getClosestInstanceFromNode方法,根据事件发生的dom节点,找到离这个dom最接近的Fiber节点。通常就是这个dom本身对应的Fiber。别忘了,dom节点最初也是从Fiber节点生成的,在那个时候为了方便反查,就已经在dom节点的internalInstanceKey字段上(实际internalInstanceKey是一个变量名称),保存了它的Fiber对象。

所以,targetNode[internalInstanceKey]就是事件所发生在的Fiber节点。

不考虑后面跟水合、suspense相关的代码的话,return_targetInst也就是事件发生的Fiber了。

此时我们再回到上面dispatchEvent的代码里,调用dispatchEventForPluginEventSystem这段,看调用时候的传参就非常清楚了,分别是事件名称、事件的flag(比如是否为冒泡)、事件对象本身、事件发生的Fiber,以及根dom。

插件系统

接下来,看看dispatchEventForPluginEventSystem里面的逻辑

dispatchEventForPluginEventSystem里面,有一段处理的逻辑,我没太看懂为什么。是判断事件发生的Fiber,是不是根dom对应的树上的Fiber。我还不太理解什么场景下会不在同一棵树上,所以不清楚这段代码的必要性,下面的代码也就略去这一段吧。不过,不耽误我们继续看绝大多数场景下的流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export function dispatchEventForPluginEventSystem(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
let ancestorInst = targetInst;

batchedUpdates(() =>
dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
ancestorInst,
targetContainer,
),
);
}

batchedUpdates方法,在如今的React版本里,已经只是个空壳了,只是会执行传入的方法而已,不会包含批量更新的逻辑。

所以,接下来就是会执行dispatchEventsForPlugins方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function dispatchEventsForPlugins(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue: DispatchQueue = [];
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}

dispatchEventsForPlugins方法的逻辑可以氛围两部分,先是调用extractEvents,收集全部需要执行的回调。然后,调用processDispatchQueue方法,把所有回调都执行一遍。

其中,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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
) {
// TODO: we should remove the concept of a "SimpleEventPlugin".
// This is the basic functionality of the event system. All
// the other plugins are essentially polyfills. So the plugin
// should probably be inlined somewhere and have its logic
// be core the to event system. This would potentially allow
// us to ship builds of React without the polyfilled plugins below.
SimpleEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
const shouldProcessPolyfillPlugins =
(eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
// We don't process these events unless we are in the
// event's native "bubble" phase, which means that we're
// not in the capture phase. That's because we emulate
// the capture phase here still. This is a trade-off,
// because in an ideal world we would not emulate and use
// the phases properly, like we do with the SimpleEvent
// plugin. However, the plugins below either expect
// emulation (EnterLeave) or use state localized to that
// plugin (BeforeInput, Change, Select). The state in
// these modules complicates things, as you'll essentially
// get the case where the capture phase event might change
// state, only for the following bubble event to come in
// later and not trigger anything as the state now
// invalidates the heuristics of the event plugin. We
// could alter all these plugins to work in such ways, but
// that might cause other unknown side-effects that we
// can't foresee right now.
if (shouldProcessPolyfillPlugins) {
EnterLeaveEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
ChangeEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
SelectEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
BeforeInputEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
FormActionEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
}
if (enableScrollEndPolyfill) {
ScrollEndEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
}
}

首先,对于所有事件,都用SimpleEventPlugin处理一下。

SimpleEventPlugin.extractEvents的中,针对各种事件会赋予不同的合成事件。由于switch case枚举的事件太多,所以我们这里只列出click事件

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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;
switch (domEventName) {
case 'click':
// Firefox creates a click event on right mouse clicks. This removes the
// unwanted click events.
// TODO: Fixed in https://phabricator.services.mozilla.com/D26793. Can
// probably remove.
if (nativeEvent.button === 2) {
return;
}
SyntheticEventCtor = SyntheticMouseEvent;
break;
}

const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;

// Some events don't bubble in the browser.
// In the past, React has always bubbled them, but this can be surprising.
// We're going to try aligning closer to the browser behavior by not bubbling
// them in React either. We'll start by not bubbling onScroll, and then expand.
const accumulateTargetOnly =
!inCapturePhase &&
// TODO: ideally, we'd eventually add all events from
// nonDelegatedEvents list in DOMPluginEventSystem.
// Then we can remove this special list.
// This is a breaking change that can wait until React 18.
(domEventName === 'scroll' || domEventName === 'scrollend');

const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
nativeEvent,
);
if (listeners.length > 0) {
// Intentionally create event lazily.
const event: ReactSyntheticEvent = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
dispatchQueue.push({event, listeners});
}
}

首先,对于事件名称做个转换,domEventName是原生的事件名称,reactName是对应的属于React的事件名称。对于大部分事件,React都是采用on前缀+首字母大写的命名法,来将原本的事件名和React定义的事件名对应起来。

对于点击事件,我们先把事件对象设为鼠标合成事件,SyntheticMouseEvent

然后,模拟浏览器的捕获和冒泡流程,收集目标Fiber到根元素的所有事件监听器。

其中,SyntheticMouseEvent是经由这段代码得到的

1
export const SyntheticMouseEvent: $FlowFixMe = createSyntheticEvent(MouseEventInterface);

MouseEventInterface包含了一些事件的属性,比如screenXmovementX等等。而实际上,其他的合成事件类型,也只是将不同的对象传给createSyntheticEvent方法,所以createSyntheticEvent是一个通用的,可以根据入参来生成不同合成事件对象的方法。

createSyntheticEvent的逻辑如下

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// This is intentionally a factory so that we have different returned constructors.
// If we had a single constructor, it would be megamorphic and engines would deopt.
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;
}
});
return SyntheticBaseEvent;
}

乍一看很长,其实核心就是定义了一个SyntheticBaseEvent的构造函数,然后往这个函数的prototype上面定义了preventDefaultstopPropagation等方法,最后将SyntheticBaseEvent构造函数return回去。构造函数里面记录了一些事件的信息,比如事件名称,事件发生的Fiber,原生事件对象等等。

接下来一段代码比较取巧,是给合成事件对象添加特定属性和方法的步骤。前面我们提到了,为了得到鼠标合成事件,我们会传入MouseEventInterface对象。它上面有很多属性,它们的初始值是0。也有一些属性是需要从原生事件得到,经过处理的(比如需要针对不同浏览器做polyfill),这些属性在MouseEventInterface上会被定义成一个函数。而属性的初始值都被设成了0,所以Interface[propName]如果有值,就一定是一个函数,传入原生的事件对象,React就能计算出经过处理后自己需要的值。

随后defaultPrevented属性表示当前原生事件是否会阻止默认行为,而returnValue是一种过时的等效属性。根据原生事件是否阻止默认行为,而给合成事件的isDefaultPrevented赋值成true或false。

然后,默认是不阻止冒泡的。

以上就是构造函数内部的逻辑。

接下来的原型方法也得比较简单了,preventDefaultstopPropagation分别就是设置对象里面对应的标志位,然后执行原生事件上面的同名方法就可以了。

之所以同时设置自己的标志位,又要执行原生事件的对应方法,是因为React的合成事件只能管好自己,但是并不能阻止原生事件的行为,比如它该冒泡还是冒泡,就有可能被React管控外的事件监听器所监听到。当你在div的onClick方法里写下e.stopPropagation的时候,你显然是希望所有的上层的监听器都不会接收到事件,不管是React加的监听器,还是其他方式添加的监听器。不过isDefaultPrevented的设置就比较微妙了,好像没看到其他地方读这个值,哈哈。

收集监听器

接下来,要调用accumulateSinglePhaseListeners方法,收集所有的监听器了。

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
42
43
44
45
46
47
48
export function accumulateSinglePhaseListeners(
targetFiber: Fiber | null,
reactName: string | null,
nativeEventType: string,
inCapturePhase: boolean,
accumulateTargetOnly: boolean,
nativeEvent: AnyNativeEvent,
): Array<DispatchListener> {
const captureName = reactName !== null ? reactName + 'Capture' : null;
const reactEventName = inCapturePhase ? captureName : reactName;
let listeners: Array<DispatchListener> = [];

let instance = targetFiber;
let lastHostComponent = null;

// Accumulate all instances and listeners via the target -> root path.
while (instance !== null) {
const {stateNode, tag} = instance;
// Handle listeners that are on HostComponents (i.e. <div>)
if (
(tag === HostComponent ||
tag === HostHoistable ||
tag === HostSingleton) &&
stateNode !== null
) {
lastHostComponent = stateNode;

// Standard React on* listeners, i.e. onClick or onClickCapture
if (reactEventName !== null) {
const listener = getListener(instance, reactEventName);
if (listener != null) {
listeners.push(
createDispatchListener(instance, listener, lastHostComponent),
);
}
}
}
// If we are only accumulating events for the target, then we don't
// continue to propagate through the React fiber tree to find other
// listeners.
if (accumulateTargetOnly) {
break;
}

instance = instance.return;
}
return listeners;
}

首先,根据当前事件触发的是否为捕获阶段的监听器,来确定最终的事件名到底是onClick,还是onClickCapture

然后,从事件发生的Fiber,沿着Fiber树,不断向上,收集经过的每一个dom类型的Fiber节点的对应名字的监听器。由于监听器是在Fiber的props里面,而每个Fiber都把props记在了Fiber对象的internalPropsKey字段上,所以这自然很容易做到。

特别的,如果被遍历到的节点是可交互的dom元素(如button, input),且disabled属性为true,那么它们的点击事件就不会被收集进去。

收集到一个listener后,会把{instance, listener, currentTarget: lastHostComponent}这样的对象放进listeners数组里,最后,会把这个listeners返回回去。

然后就剩下这段逻辑了

1
2
3
4
5
6
7
8
9
10
11
if (listeners.length > 0) {
// Intentionally create event lazily.
const event: ReactSyntheticEvent = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
dispatchQueue.push({event, listeners});
}

如果这个事件是有监听器在关注着他的,那么就用到前面的合成事件构造函数,创建合成事件。把合成事件,和它所有的监听器数组,打包放入dispatchQueue中。

那么接下来我们又要回到前面了,前面除了SimpleEventPlugin.extractEvents,还有很多XXXEventPlugin.extractEvents。这里的意思就是,SimpleEventPlugin把通用的主流程搭好,只不过并没有枚举出全部的事件。没被枚举到的各种事件,需要各种各样的特殊处理,就自己用专门的插件去处理。例如中文输入法对应的onCompositionStartonCompositionEnd事件,就是在BeforeInputEventPlugin里面专门去特殊处理的。

待全部的插件都走完后,那么这次触发的原生事件一定都有了自己对应的合成事件对象,并且还收集了合成事件对象的全部监听器。

接下来,回看dispatchEventsForPlugins方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function dispatchEventsForPlugins(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue: DispatchQueue = [];
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}

dispatchQueue已经收集完毕,该执行下面的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
42
43
44
45
46
47
48
49
50
51
52
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);
// event system doesn't use pooling.
}
}

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;
}
}
}

function executeDispatch(
event: ReactSyntheticEvent,
listener: Function,
currentTarget: EventTarget,
): void {
event.currentTarget = currentTarget;
try {
listener(event);
} catch (error) {
reportGlobalError(error);
}
event.currentTarget = null;
}

首先,根据事件的监听器的eventSystemFlags,来确认是冒泡阶段的监听器,还是捕获阶段的监听器。这影响着最后遍历监听器的顺序。

而我们首先要遍历dispatchQueue。别忘了,我们是在插件处理完这个事件的时候,会把event和监听器数组推到dispatchQueue里面。而一个原生事件可能因为被多个插件处理等原因,对应多个合成事件。所以需要遍历dispatchQueue,把一个原生事件产生的所有合成事件都处理一遍。

处理每个合成事件的方法就是processDispatchQueueItemsInOrder。他根据是捕获还是冒泡,按照不同的顺序,遍历一个合成事件所有的监听器,逐个取出执行。但是当遍历到某个监听器,发现isPropagationStopped返回了true,那么就说明上一个listener里,执行了e.stopPropagation,所以当前这个listener就不会被执行了,遍历终止。

总结

自定义事件系统的原因

React中的事件不是原生的dom事件,而是基于事件委托自己实现的一套事件系统,同样氛围捕获和冒泡两阶段。

自己实现的原因:事件委托比给每个dom添加事件更方便、自己实现更可控,可以自由设置不同优先级

简要原理:

所有事件都委托到根dom上监听,通过event.target找到事件真实发生的dom。由于dom有个字段用于保存对应的Fiber,所以很容易找到事件发生在哪个Fiber上。

每个原生事件,都可能被若干种事件插件处理。每次被处理的时候,它都会对应一种React事件名,生成一个合成事件。并且从事件发生的Fiber往上遍历,收集沿途所有Fiber中的props中这个合成事件的回调。

最后,把这个原生事件对应的所有React事件的回调,根据捕获还是冒泡,按不同顺序执行一遍


通俗易懂的React原理(十二):React的合成事件系统
https://miku03090831.github.io/2025/11/30/通俗易懂的React原理(十二):React的合成事件系统/
作者
qh_meng
发布于
2025年11月30日
许可协议