0%

react18.2合成事件原理

事件注册

注册事件名

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
103
104
105
106
107
108
109
110
111
112
113
// \react-dom-bindings\src\events\DOMEventNames.js

import getVendorPrefixedEventName from './getVendorPrefixedEventName';

export type DOMEventName =
| 'abort'
| 'afterblur' // Not a real event. This is used by event experiments.
// These are vendor-prefixed so you should use the exported constants instead:
// 'animationiteration' |
// 'animationend |
// 'animationstart' |
| 'beforeblur' // Not a real event. This is used by event experiments.
| 'beforeinput'
| 'blur'
| 'canplay'
| 'canplaythrough'
| 'cancel'
| 'change'
| 'click'
| 'close'
| 'compositionend'
| 'compositionstart'
| 'compositionupdate'
| 'contextmenu'
| 'copy'
| 'cut'
| 'dblclick'
| 'auxclick'
| 'drag'
| 'dragend'
| 'dragenter'
| 'dragexit'
| 'dragleave'
| 'dragover'
| 'dragstart'
| 'drop'
| 'durationchange'
| 'emptied'
| 'encrypted'
| 'ended'
| 'error'
| 'focus'
| 'focusin'
| 'focusout'
| 'fullscreenchange'
| 'gotpointercapture'
| 'hashchange'
| 'input'
| 'invalid'
| 'keydown'
| 'keypress'
| 'keyup'
| 'load'
| 'loadstart'
| 'loadeddata'
| 'loadedmetadata'
| 'lostpointercapture'
| 'message'
| 'mousedown'
| 'mouseenter'
| 'mouseleave'
| 'mousemove'
| 'mouseout'
| 'mouseover'
| 'mouseup'
| 'paste'
| 'pause'
| 'play'
| 'playing'
| 'pointercancel'
| 'pointerdown'
| 'pointerenter'
| 'pointerleave'
| 'pointermove'
| 'pointerout'
| 'pointerover'
| 'pointerup'
| 'popstate'
| 'progress'
| 'ratechange'
| 'reset'
| 'resize'
| 'scroll'
| 'scrollend'
| 'seeked'
| 'seeking'
| 'select'
| 'selectstart'
| 'selectionchange'
| 'stalled'
| 'submit'
| 'suspend'
| 'textInput' // Intentionally camelCase. Non-standard.
| 'timeupdate'
| 'toggle'
| 'touchcancel'
| 'touchend'
| 'touchmove'
| 'touchstart'
// These are vendor-prefixed so you should use the exported constants instead:
// 'transitionend' |
| 'volumechange'
| 'waiting'
| 'wheel';

export const ANIMATION_END: DOMEventName =
getVendorPrefixedEventName('animationend');
export const ANIMATION_ITERATION: DOMEventName =
getVendorPrefixedEventName('animationiteration');
export const ANIMATION_START: DOMEventName =
getVendorPrefixedEventName('animationstart');
export const TRANSITION_END: DOMEventName =
getVendorPrefixedEventName('transitionend');
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
// \react-dom-bindings\src\events\EventRegistry.js

// React中的事件
export const allNativeEvents: Set<DOMEventName> = new Set();
export const registrationNameDependencies: {
[registrationName: string]: Array<DOMEventName>,
} = {};

// 事件注册
export function registerTwoPhaseEvent(
registrationName: string,
dependencies: Array<DOMEventName>,
): void {
registerDirectEvent(registrationName, dependencies);
registerDirectEvent(registrationName + 'Capture', dependencies);
}

export function registerDirectEvent(
registrationName: string,
dependencies: Array<DOMEventName>,
) {
registrationNameDependencies[registrationName] = dependencies;
for (let i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);
}
}

不同类型的事件注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// \react-dom-bindings\src\events\DOMPluginEventSystem.js

import * as BeforeInputEventPlugin from './plugins/BeforeInputEventPlugin';
import * as ChangeEventPlugin from './plugins/ChangeEventPlugin';
import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin';
import * as SelectEventPlugin from './plugins/SelectEventPlugin';
import * as SimpleEventPlugin from './plugins/SimpleEventPlugin';

// ! 注册事件
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();

SimpleEventPlugin

普通事件,如 click、drag、drop 等。

1
2
3
4
// \react-dom-bindings\src\events\plugins\SimpleEventPlugin.js
import {
registerSimpleEvents,
} from '../DOMEventProperties';
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
// \react-dom-bindings\src\events\DOMEventProperties.js
const simpleEventPluginEvents = [
'abort',
'auxClick',
'cancel',
'canPlay',
'canPlayThrough',
'click',
'close',
'contextMenu',
'copy',
'cut',
'drag',
'dragEnd',
'dragEnter',
'dragExit',
'dragLeave',
'dragOver',
'dragStart',
'drop',
'durationChange',
'emptied',
'encrypted',
'ended',
'error',
'gotPointerCapture',
'input',
'invalid',
'keyDown',
'keyPress',
'keyUp',
'load',
'loadedData',
'loadedMetadata',
'loadStart',
'lostPointerCapture',
'mouseDown',
'mouseMove',
'mouseOut',
'mouseOver',
'mouseUp',
'paste',
'pause',
'play',
'playing',
'pointerCancel',
'pointerDown',
'pointerMove',
'pointerOut',
'pointerOver',
'pointerUp',
'progress',
'rateChange',
'reset',
'resize',
'seeked',
'seeking',
'stalled',
'submit',
'suspend',
'timeUpdate',
'touchCancel',
'touchEnd',
'touchStart',
'volumeChange',
'scroll',
'scrollEnd',
'toggle',
'touchMove',
'waiting',
'wheel',
];

function registerSimpleEvent(domEventName: DOMEventName, reactName: string) {
topLevelEventsToReactNames.set(domEventName, reactName);
registerTwoPhaseEvent(reactName, [domEventName]);
}

export function registerSimpleEvents() {
for (let i = 0; i < simpleEventPluginEvents.length; i++) {
const eventName = ((simpleEventPluginEvents[i]: any): string);
const domEventName = ((eventName.toLowerCase(): any): DOMEventName);
const capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);
registerSimpleEvent(domEventName, 'on' + capitalizedEvent);
}
// Special cases where event names don't match.
registerSimpleEvent(ANIMATION_END, 'onAnimationEnd');
registerSimpleEvent(ANIMATION_ITERATION, 'onAnimationIteration');
registerSimpleEvent(ANIMATION_START, 'onAnimationStart');
registerSimpleEvent('dblclick', 'onDoubleClick');
registerSimpleEvent('focusin', 'onFocus');
registerSimpleEvent('focusout', 'onBlur');
registerSimpleEvent(TRANSITION_END, 'onTransitionEnd');
}

EnterLeaveEventPlugin

1
2
3
4
5
6
7
8
// \react-dom-bindings\src\events\plugins\EnterLeaveEventPlugin.js

function registerEvents() {
registerDirectEvent('onMouseEnter', ['mouseout', 'mouseover']);
registerDirectEvent('onMouseLeave', ['mouseout', 'mouseover']);
registerDirectEvent('onPointerEnter', ['pointerout', 'pointerover']);
registerDirectEvent('onPointerLeave', ['pointerout', 'pointerover']);
}

ChangeEventPlugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// \react-dom-bindings\src\events\plugins\ChangeEventPlugin.js

function registerEvents() {
registerTwoPhaseEvent('onChange', [
'change',
'click',
'focusin',
'focusout',
'input',
'keydown',
'keyup',
'selectionchange',
]);
}

SelectEventPlugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// \react-dom-bindings\src\events\plugins\SelectEventPlugin.js
function registerEvents() {
registerTwoPhaseEvent('onSelect', [
'focusout',
'contextmenu',
'dragend',
'focusin',
'keydown',
'keyup',
'mousedown',
'mouseup',
'selectionchange',
]);
}

BeforeInputEventPlugin

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
// \react-dom-bindings\src\events\plugins\BeforeInputEventPlugin.js
function registerEvents() {
registerTwoPhaseEvent('onBeforeInput', [
'compositionend',
'keypress',
'textInput',
'paste',
]);
registerTwoPhaseEvent('onCompositionEnd', [
'compositionend',
'focusout',
'keydown',
'keypress',
'keyup',
'mousedown',
]);
registerTwoPhaseEvent('onCompositionStart', [
'compositionstart',
'focusout',
'keydown',
'keypress',
'keyup',
'mousedown',
]);
registerTwoPhaseEvent('onCompositionUpdate', [
'compositionupdate',
'focusout',
'keydown',
'keypress',
'keyup',
'mousedown',
]);
}

事件绑定 - listenToAllSupportedEvents

React 初始化渲染的时候,会调⽤函数 listenToAllSupportedEvents 来绑定事件。listenAllSupportedEvents 将事件注册在页面的根节点,也就是 div#root

主要做了两件事情:

  • 为避免重复注册事件,在 DOM 上设置一个属性,如果有这个属性说明事件已经被注册了
  • 遍历 allNativeEvents,调用 listenToNativeEvent 进行事件注册
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
): RootType {
// ...

// comment nodes 已弃用,这里是为了兼容FB老代码 https://github.com/facebook/react/pull/24110
const rootContainerElement: Document | Element | DocumentFragment =
container.nodeType === COMMENT_NODE
? (container.parentNode: any)
: container;

// 事件绑定
listenToAllSupportedEvents(rootContainerElement);

return new ReactDOMRoot(root);
}
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
// \react-dom-bindings\src\events\DOMPluginEventSystem.js
// 事件绑定
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (!(rootContainerElement: any)[listeningMarker]) {
// sy 防止重复绑定
(rootContainerElement: any)[listeningMarker] = true;
allNativeEvents.forEach(domEventName => {
// We handle selectionchange separately because it
// doesn't bubble and needs to be on the document.
// 单独处理selectionchange事件,因为它不会冒泡,需要在文档上处理。
if (domEventName !== 'selectionchange') {
if (!nonDelegatedEvents.has(domEventName)) {
// ! 这些事件都是委托在rootContainerElement上的
// nonDelegatedEvents中都是不需要委托的事件,也就是不需要冒泡的,如cancel、close、invalid、load、scroll、scrollend、toggle等
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});

// 单独处理selectionchange事件
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.
// selectionchange事件也需要去重,但它附加在document上。
if (!(ownerDocument: any)[listeningMarker]) {
(ownerDocument: any)[listeningMarker] = true;
listenToNativeEvent('selectionchange', false, ownerDocument);
}
}
}
}

// \react-dom-bindings\src\events\DOMPluginEventSystem.js
// We should not delegate these events to the container, but rather
// set them on the actual target element itself. This is primarily
// because these events do not consistently bubble in the DOM.
// 我们不应该将这些事件委托给容器,而是应该直接在实际的目标元素上设置它们。这主要是因为这些事件在DOM中的冒泡行为并不一致。
export const nonDelegatedEvents: Set<DOMEventName> = new Set([
'cancel',
'close',
'invalid',
'load',
'scroll',
'scrollend',
'toggle',
// In order to reduce bytes, we insert the above array of media events
// into this Set. Note: the "error" event isn't an exclusive media event,
// and can occur on other elements too. Rather than duplicate that event,
// we just take it from the media events array.
// 为了减少字节数,我们将上述媒体事件数组插入到这个 Set 中。
// 注意:"error" 事件并不是一个独占的媒体事件,也可能发生在其他元素上。我们不会重复这个事件,而是直接从媒体事件数组中取出。
...mediaEventTypes,
]);
// List of events that need to be individually attached to media elements.
// 需要分别附加到媒体元素的事件列表。
export const mediaEventTypes: Array<DOMEventName> = [
'abort',
'canplay',
'canplaythrough',
'durationchange',
'emptied',
'encrypted',
'ended',
'error',
'loadeddata',
'loadedmetadata',
'loadstart',
'pause',
'play',
'playing',
'progress',
'ratechange',
'resize',
'seeked',
'seeking',
'stalled',
'suspend',
'timeupdate',
'volumechange',
'waiting',
];

listenToNativeEvent

这个函数只做一件事情:定义一个 flags 变量表示当前是冒泡还是捕获,然后调用 addTrappedEventListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// \packages\react-dom-bindings\src\events\DOMPluginEventSystem.js
export function listenToNativeEvent(
domEventName: DOMEventName, // 原生事件名
isCapturePhaseListener: boolean, // 是否是捕获阶段
target: EventTarget, // 事件绑定的目标节点,也就是 div#root
): void {

// 冒泡用 0 表示,捕获用 4 表示
let eventSystemFlags = 0;
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
}

addTrappedEventListener

这个函数主要做了两件事:

  1. 创建一个监听器,用于事件挂载,具体查看 createEventListenerWrapperWithPriority
  2. 事件挂载,将事件挂载到目标节点上,也就是将事件绑定到 div#root
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// \packages\react-dom-bindings\src\events\DOMPluginEventSystem.js
function addTrappedEventListener(
targetContainer: EventTarget, // 事件挂载节点,也就是 div#root
domEventName: DOMEventName, // 原生事件名,比如 click
eventSystemFlags: EventSystemFlags, // 4 表示捕获,0 表示冒泡
isCapturePhaseListener: boolean, // true 表示捕获阶段,false 表示冒泡阶段
isDeferredListenerForLegacyFBSupport?: boolean,
) {
// 获取对应事件,事件定义在ReactDOMEventListener.js中
// 如DiscreteEventPriority对应dispatchDiscreteEvent,ContinuousEventPriority对应dispatchContinuousEvent
// 创建一个事件监听器
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) {
// sy
// 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.
// 浏览器引入了一种干预措施,使这些事件在document上默认为passive状态。
// React不再将它们绑定到document上,但是现在改变这一点将会撤销之前的性能优势。
// 因此,我们现在在根节点上手动模拟现有的行为。
// https://github.com/facebook/react/issues/19651
if (
domEventName === 'touchstart' ||
domEventName === 'touchmove' ||
domEventName === 'wheel'
) {
isPassiveListener = true;
}
}

// React17之后,事件委托在targetContainer,但是兼容之前的版本委托在document
targetContainer =
enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport
? (targetContainer: any).ownerDocument
: targetContainer;

let unsubscribeListener;
// When legacyFBSupport is enabled, it's for when we
// want to add a one time event listener to a container.
// This should only be used with enableLegacyFBSupport
// due to requirement to provide compatibility with
// internal FB www event tooling. This works by removing
// the event listener as soon as it is invoked. We could
// also attempt to use the {once: true} param on
// addEventListener, but that requires support and some
// browsers do not support this today, and given this is
// to support legacy code patterns, it's likely they'll
// need support for such browsers.
// 当启用legacyFBSupport时,是为了当我们想要向container添加一次性事件监听器时使用。
// 这应该只与enableLegacyFBSupport一起使用,因为需要与内部FB www事件工具提供的兼容性。
// 这通过在调用后立即移除事件监听器来实现。我们也可以尝试在addEventListener上使用{once: true}参数,但这需要支持,
// 一些浏览器今天不支持这一点,考虑到这是为了支持传统代码模式,它们可能需要支持这些浏览器。
if (enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport) {
const originalListener = listener;
listener = function (...p) {
removeEventListener(
targetContainer,
domEventName,
unsubscribeListener,
isCapturePhaseListener,
);
return originalListener.apply(this, p);
};
}
// TODO: There are too many combinations here. Consolidate them.

if (isCapturePhaseListener) {
// ! 捕获阶段
// sy
if (isPassiveListener !== undefined) {
// touchstart、touchmove、wheel
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener,
);
} else {
// sy
// click、contextmenu、drag、drop、input、mousedown、change等事件
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
}
} else {
// ! 冒泡阶段
if (isPassiveListener !== undefined) {
// touchstart、touchmove、wheel
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener,
);
} else {
// click、contextmenu、drag、drop、input、mousedown、change等事件
// sy
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
}
}

createEventListenerWrapperWithPriority

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
// \packages\react-dom-bindings\src\events\ReactDOMEventListener.js
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
// 根据事件名称,获取优先级。比如click、input、drop等对应DiscreteEventPriority,drag、scroll等对应ContinuousEventPriority,
// message也许处于Scheduler中,根据getCurrentSchedulerPriorityLevel()获取优先级。其它是DefaultEventPriority。
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

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
103
export function getEventPriority(domEventName: DOMEventName): EventPriority {
switch (domEventName) {
// Used by SimpleEventPlugin:
case 'cancel':
case 'click':
case 'close':
case 'contextmenu':
case 'copy':
case 'cut':
case 'auxclick':
case 'dblclick':
case 'dragend':
case 'dragstart':
case 'drop':
case 'focusin':
case 'focusout':
case 'input':
case 'invalid':
case 'keydown':
case 'keypress':
case 'keyup':
case 'mousedown':
case 'mouseup':
case 'paste':
case 'pause':
case 'play':
case 'pointercancel':
case 'pointerdown':
case 'pointerup':
case 'ratechange':
case 'reset':
case 'resize':
case 'seeked':
case 'submit':
case 'touchcancel':
case 'touchend':
case 'touchstart':
case 'volumechange':
// Used by polyfills: (fall through)
case 'change':
case 'selectionchange':
case 'textInput':
case 'compositionstart':
case 'compositionend':
case 'compositionupdate':
// Only enableCreateEventHandleAPI: (fall through)
case 'beforeblur':
case 'afterblur':
// Not used by React but could be by user code: (fall through)
case 'beforeinput':
case 'blur':
case 'fullscreenchange':
case 'focus':
case 'hashchange':
case 'popstate':
case 'select':
case 'selectstart':
return DiscreteEventPriority;
case 'drag':
case 'dragenter':
case 'dragexit':
case 'dragleave':
case 'dragover':
case 'mousemove':
case 'mouseout':
case 'mouseover':
case 'pointermove':
case 'pointerout':
case 'pointerover':
case 'scroll':
case 'toggle':
case 'touchmove':
case 'wheel':
// Not used by React but could be by user code: (fall through)
case 'mouseenter':
case 'mouseleave':
case 'pointerenter':
case 'pointerleave':
return ContinuousEventPriority;
case 'message': {
// We might be in the Scheduler callback.
// Eventually this mechanism will be replaced by a check
// of the current priority on the native scheduler.
const schedulerPriority = getCurrentSchedulerPriorityLevel();
switch (schedulerPriority) {
case ImmediateSchedulerPriority:
return DiscreteEventPriority;
case UserBlockingSchedulerPriority:
return ContinuousEventPriority;
case NormalSchedulerPriority:
case LowSchedulerPriority:
// TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
return DefaultEventPriority;
case IdleSchedulerPriority:
return IdleEventPriority;
default:
return DefaultEventPriority;
}
}
default:
return DefaultEventPriority;
}
}

事件挂载 - 捕获阶段

支持passive - addEventCaptureListenerWithPassiveFlag
1
2
3
4
5
6
7
8
9
10
11
12
13
// \packages\react-dom-bindings\src\events\EventListener.js
export function addEventCaptureListenerWithPassiveFlag(
target: EventTarget,
eventType: string,
listener: Function,
passive: boolean,
): Function {
target.addEventListener(eventType, listener, {
capture: true,
passive,
});
return listener;
}
addEventCaptureListener
1
2
3
4
5
6
7
8
9
// \packages\react-dom-bindings\src\events\EventListener.js
export function addEventCaptureListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, true);
return listener;
}

事件挂载 - 冒泡阶段

支持passive - addEventBubbleListenerWithPassiveFlag
1
2
3
4
5
6
7
8
9
10
11
12
13
// \packages\react-dom-bindings\src\events\EventListener.js
export function addEventCaptureListenerWithPassiveFlag(
target: EventTarget,
eventType: string,
listener: Function,
passive: boolean,
): Function {
target.addEventListener(eventType, listener, {
capture: true,
passive,
});
return listener;
}
addEventBubbleListener
1
2
3
4
5
6
7
8
9
// \packages\react-dom-bindings\src\events\EventListener.js
export function addEventBubbleListener(
target: EventTarget,
eventType: string,
listener: Function,
): Function {
target.addEventListener(eventType, listener, false);
return listener;
}

事件派发

dispatchDiscreteEvent

适用事件:click、drop、input、drop 等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function dispatchDiscreteEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
container: EventTarget,
nativeEvent: AnyNativeEvent,
) {
// ! 1. 记录上一次的事件优先级
const previousPriority = getCurrentUpdatePriority();
// ! 2. 记录上一次的transition
const prevTransition = ReactCurrentBatchConfig.transition;
// !3. 清空transition,transition为非紧急更新,这里不处理
ReactCurrentBatchConfig.transition = null;
try {
// !4. 设置当前事件优先级为DiscreteEventPriority
setCurrentUpdatePriority(DiscreteEventPriority);
// !5. 调用dispatchEvent,执行事件
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
} finally {
// !6. 恢复
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
}
}

事件优先级记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// \react-reconciler\src\ReactEventPriorities.js

export opaque type EventPriority = Lane;

// 优先级从上往下,越来越小
export const DiscreteEventPriority: EventPriority = SyncLane; // 2
export const ContinuousEventPriority: EventPriority = InputContinuousLane; // 8
export const DefaultEventPriority: EventPriority = DefaultLane; // 页面初次渲染的lane 32, transition
export const IdleEventPriority: EventPriority = IdleLane;

let currentUpdatePriority: EventPriority = NoLane;

export function getCurrentUpdatePriority(): EventPriority {
return currentUpdatePriority;
}

export function setCurrentUpdatePriority(newPriority: EventPriority) {
currentUpdatePriority = newPriority;
}

dispatchContinuousEvent

适用事件:drag、mouse的各种事件等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function dispatchContinuousEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
container: EventTarget,
nativeEvent: AnyNativeEvent,
) {
const previousPriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = null;
try {
setCurrentUpdatePriority(ContinuousEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
}
}

dispatchEvent

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
// \react-dom-bindings\src\events\ReactDOMEventListener.js

// 派发事件
export function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): void {
// 有些场景下是禁止事件的,比如在commit阶段
if (!_enabled) {
return;
}

let blockedOn = findInstanceBlockingEvent(nativeEvent);

if (blockedOn === null) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
return_targetInst,
targetContainer,
);

clearIfContinuousEvent(domEventName, nativeEvent);
return;
}

if (
queueIfContinuousEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
)
) {
nativeEvent.stopPropagation();
return;
}
// We need to clear only if we didn't queue because
// queueing is accumulative.
clearIfContinuousEvent(domEventName, nativeEvent);

if (
eventSystemFlags & IS_CAPTURE_PHASE &&
isDiscreteEventThatRequiresHydration(domEventName)
) {
while (blockedOn !== null) {
const fiber = getInstanceFromNode(blockedOn);
if (fiber !== null) {
attemptSynchronousHydration(fiber);
}
const nextBlockedOn = findInstanceBlockingEvent(nativeEvent);
if (nextBlockedOn === null) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
return_targetInst,
targetContainer,
);
}
if (nextBlockedOn === blockedOn) {
break;
}
blockedOn = nextBlockedOn;
}
if (blockedOn !== null) {
nativeEvent.stopPropagation();
}
return;
}

// This is not replayable so we'll invoke it but without a target,
// in case the event system needs to trace it.
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
}

findInstanceBlockingEvent

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
// \react-dom-bindings\src\events\ReactDOMEventListener.js

export function findInstanceBlockingEvent(
nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
const nativeEventTarget = getEventTarget(nativeEvent);
return findInstanceBlockingTarget(nativeEventTarget);
}

export let return_targetInst: null | Fiber = null;

// Returns a SuspenseInstance or Container if it's blocked.
// The return_targetInst field above is conceptually part of the return value.
// 如果被阻塞,返回一个 SuspenseInstance 或 Container。
// 上面的 return_targetInst 字段在概念上是返回值的一部分。
export function findInstanceBlockingTarget(
targetNode: Node,
): null | Container | SuspenseInstance {
// TODO: Warn if _enabled is false.

return_targetInst = null;

// 通过 targetNode 获取最近的 Fiber 实例
let targetInst = getClosestInstanceFromNode(targetNode);

if (targetInst !== null) {
// 寻找最近的已挂载的 Fiber 实例
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) {
// 寻找最近的已挂载的 Suspense 实例
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.
// 如果在提交该组件的挂载之前收到事件(例如:图片加载完成),暂时忽略它(也就是,将其视为在非React树上的事件)。
// 我们也可以考虑将事件排队,并在挂载后分发它们。
targetInst = null;
}
}
}
return_targetInst = targetInst;
// We're not blocked on anything.
// 没有阻塞
return null;
}

dispatchEventForPluginEventSystem

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// \react-dom-bindings\src\events\DOMPluginEventSystem.js

export function dispatchEventForPluginEventSystem(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
let ancestorInst = targetInst;
if (
(eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
(eventSystemFlags & IS_NON_DELEGATED) === 0
) {
const targetContainerNode = ((targetContainer: any): Node);

// If we are using the legacy FB support flag, we
// defer the event to the null with a one
// time event listener so we can defer the event.
if (
enableLegacyFBSupport &&
// If our event flags match the required flags for entering
// FB legacy mode and we are processing the "click" event,
// then we can defer the event to the "document", to allow
// for legacy FB support, where the expected behavior was to
// match React < 16 behavior of delegated clicks to the doc.
domEventName === 'click' &&
(eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0 &&
!isReplayingEvent(nativeEvent)
) {
deferClickToDocumentForLegacyFBSupport(domEventName, targetContainer);
return;
}
if (targetInst !== null) {
// The below logic attempts to work out if we need to change
// the target fiber to a different ancestor. We had similar logic
// in the legacy event system, except the big difference between
// systems is that the modern event system now has an event listener
// attached to each React Root and React Portal Root. Together,
// the DOM nodes representing these roots are the "rootContainer".
// To figure out which ancestor instance we should use, we traverse
// up the fiber tree from the target instance and attempt to find
// root boundaries that match that of our current "rootContainer".
// If we find that "rootContainer", we find the parent fiber
// sub-tree for that root and make that our ancestor instance.
let node: null | Fiber = targetInst;

mainLoop: while (true) {
if (node === null) {
// 事件没有对应的fiber,没法执行事件,退出
return;
}
const nodeTag = node.tag;
if (nodeTag === HostRoot || nodeTag === HostPortal) {
let container = node.stateNode.containerInfo;
if (isMatchingRootContainer(container, targetContainerNode)) {
// container和targetContainerNode相等,说明找到了对应的rootContainer
break;
}
if (nodeTag === HostPortal) {
// The target is a portal, but it's not the rootContainer we're looking for.
// Normally portals handle their own events all the way down to the root.
// So we should be able to stop now. However, we don't know if this portal
// was part of *our* root.
let grandNode = node.return;
while (grandNode !== null) {
const grandTag = grandNode.tag;
if (grandTag === HostRoot || grandTag === HostPortal) {
const grandContainer = grandNode.stateNode.containerInfo;
if (
isMatchingRootContainer(grandContainer, targetContainerNode)
) {
// This is the rootContainer we're looking for and we found it as
// a parent of the Portal. That means we can ignore it because the
// Portal will bubble through to us.
return;
}
}
grandNode = grandNode.return;
}
}
// Now we need to find it's corresponding host fiber in the other
// tree. To do this we can use getClosestInstanceFromNode, but we
// need to validate that the fiber is a host instance, otherwise
// we need to traverse up through the DOM till we find the correct
// node that is from the other tree.
while (container !== null) {
const parentNode = getClosestInstanceFromNode(container);
if (parentNode === null) {
return;
}
const parentTag = parentNode.tag;
if (
parentTag === HostComponent ||
parentTag === HostText ||
(enableFloat ? parentTag === HostHoistable : false) ||
parentTag === HostSingleton
) {
node = ancestorInst = parentNode;
continue mainLoop;
}
container = container.parentNode;
}
}
node = node.return;
}
}
}

// 批量更新
batchedUpdates(() =>
dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
ancestorInst,
targetContainer,
)
);
}

batchedUpdates

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// \react-dom-bindings\src\events\ReactDOMUpdateBatching.js
export function batchedUpdates(fn, a, b) {
if (isInsideEventHandler) {
// If we are currently inside another batch, we need to wait until it
// fully completes before restoring state.
// 如果我们当前正在另一个批处理中,需要等待其完全完成后再恢复状态。
return fn(a, b);
}
isInsideEventHandler = true;
try {
return batchedUpdatesImpl(fn, a, b);
} finally {
isInsideEventHandler = false;
finishEventHandler();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// \packages\react-reconciler\src\ReactFiberWorkLoop.js
export function batchedUpdates<A, R>(fn: A => R, a: A): R {
const prevExecutionContext = executionContext;
executionContext |= BatchedContext;
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
// If there were legacy sync updates, flush them at the end of the outer
// most batchedUpdates-like method.
if (
executionContext === NoContext &&
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
) {
resetRenderTimer();
flushSyncWorkOnLegacyRootsOnly();
}
}
}

dispatchEventsForPlugins

这个函数主要做了两件事:

  1. extractEvents - 提取事件,将事件函数放到 dispatchQueue 队列中
  2. processDispatchQueue - 执行事件,将 dispatchQueue 队列中的事件函数依次执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function dispatchEventsForPlugins(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
targetContainer: EventTarget,
): void {
// nativeEvent.target
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue: DispatchQueue = [];
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}

extractEvents

在一开始事件注册时,不同类型的事件由不同的事件插件进行注册,它们的特征可能不一样,但是不同类型的事件底层处理的逻辑是相同的,所以 extractEvents 函数调用了 SimpleEventPlugin.extractEvents 函数,用来提取事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
) {
// 调用插件的 extractEvents 函数,用来提取事件函数
SimpleEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
);
// ...
}
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// \react-dom-bindings\src\events\plugins\SimpleEventPlugin.js
function extractEvents(
dispatchQueue: DispatchQueue,
domEventName: DOMEventName,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
): void {
// 1.根据原生事件名拿到 react 事件名
const reactName = topLevelEventsToReactNames.get(domEventName);
if (reactName === undefined) {
return;
}
// 2.定义一个合成事件
let SyntheticEventCtor = SyntheticEvent;
let reactEventType: string = domEventName;
// 3.根据原生事件名调用不同的合成事件
switch (domEventName) {
case 'keypress':
// Firefox creates a keypress event for function keys too. This removes
// the unwanted keypress events. Enter is however both printable and
// non-printable. One would expect Tab to be as well (but it isn't).
// TODO: Fixed in https://bugzilla.mozilla.org/show_bug.cgi?id=968056. Can
// probably remove.
if (getEventCharCode(((nativeEvent: any): KeyboardEvent)) === 0) {
return;
}
/* falls through */
case 'keydown':
case 'keyup':
SyntheticEventCtor = SyntheticKeyboardEvent;
break;
case 'focusin':
reactEventType = 'focus';
SyntheticEventCtor = SyntheticFocusEvent;
break;
case 'focusout':
reactEventType = 'blur';
SyntheticEventCtor = SyntheticFocusEvent;
break;
case 'beforeblur':
case 'afterblur':
SyntheticEventCtor = SyntheticFocusEvent;
break;
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;
}
/* falls through */
case 'auxclick':
case 'dblclick':
case 'mousedown':
case 'mousemove':
case 'mouseup':
// TODO: Disabled elements should not respond to mouse events
/* falls through */
case 'mouseout':
case 'mouseover':
case 'contextmenu':
SyntheticEventCtor = SyntheticMouseEvent;
break;
case 'drag':
case 'dragend':
case 'dragenter':
case 'dragexit':
case 'dragleave':
case 'dragover':
case 'dragstart':
case 'drop':
SyntheticEventCtor = SyntheticDragEvent;
break;
case 'touchcancel':
case 'touchend':
case 'touchmove':
case 'touchstart':
SyntheticEventCtor = SyntheticTouchEvent;
break;
case ANIMATION_END:
case ANIMATION_ITERATION:
case ANIMATION_START:
SyntheticEventCtor = SyntheticAnimationEvent;
break;
case TRANSITION_END:
SyntheticEventCtor = SyntheticTransitionEvent;
break;
case 'scroll':
case 'scrollend':
SyntheticEventCtor = SyntheticUIEvent;
break;
case 'wheel':
SyntheticEventCtor = SyntheticWheelEvent;
break;
case 'copy':
case 'cut':
case 'paste':
SyntheticEventCtor = SyntheticClipboardEvent;
break;
case 'gotpointercapture':
case 'lostpointercapture':
case 'pointercancel':
case 'pointerdown':
case 'pointermove':
case 'pointerout':
case 'pointerover':
case 'pointerup':
SyntheticEventCtor = SyntheticPointerEvent;
break;
default:
// Unknown event. This is used by createEventHandle.
break;
}

// 是否是捕获阶段
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;

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.
// TODO:理想情况下,我们最终会将nonDelegatedEvents列表中的所有事件添加到DOMPluginEventSystem中。
// 然后我们可以移除这个特殊列表。这是一个破坏性的变更,可以等到React 18再处理。
(domEventName === 'scroll' || domEventName === 'scrollend');

// 4.收集从事件源到事件绑定的地方所有的事件函数
const listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly,
nativeEvent,
);
// 5.将事件函数保存到事件函数队列中
if (listeners.length > 0) {
// Intentionally create event lazily.
const event: ReactSyntheticEvent = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget,
);
dispatchQueue.push({event, listeners});
}
}

processDispatchQueue

processDispatchQueue 函数主要功能是执行事件函数队列中的事件函数

dispatchQueue 队列的数据结构如下:

1
2
3
4
5
6
[
{
event, // 合成事件对象
listeners: [事件源对应的事件函数, 事件源父节点对应的事件函数, ..., div#root 对应的事件函数], // 事件函数队列
},
];
1
2
3
4
5
6
7
8
9
10
11
12
13
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.
}
// This would be a good time to rethrow if any of the event handlers threw.
rethrowCaughtError();
}

具体执行是由 processDispatchQueueItemsInOrder 执行

  1. 判断是不是捕获阶段
    1. 如果是捕获阶段,就从后往前执行
    2. 如果是冒泡阶段,就从前往后执行
    3. 在执行事件函数之前,如果事件被阻止了,则不在继续执行事件函数
  2. 事件函数执行由 executeDispatch 函数完成
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
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;
}
}
}

执行事件 - executeDispatch

1
2
3
4
5
6
7
8
9
10
function executeDispatch(
event: ReactSyntheticEvent,
listener: Function,
currentTarget: EventTarget,
): void {
const type = event.type || 'unknown-event';
event.currentTarget = currentTarget;
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
event.currentTarget = null;
}

invokeGuardedCallbackAndCatchFirstError

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
export function invokeGuardedCallbackAndCatchFirstError<
A,
B,
C,
D,
E,
F,
Context,
>(
this: mixed,
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;
}
}
}

export function invokeGuardedCallback<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,
): void {
hasError = false;
caughtError = null;
invokeGuardedCallbackImpl.apply(reporter, arguments);
}

export default function invokeGuardedCallbackImpl<Args: Array<mixed>, Context>(
this: {onError: (error: mixed) => void},
name: string | null,
func: (...Args) => mixed,
context: Context,
): void {
const funcArgs = Array.prototype.slice.call(arguments, 3);
try {
func.apply(context, funcArgs);
} catch (error) {
this.onError(error);
}
}

总结

事件系统核心原理比较简单,主要的作用是抹平各浏览器之间的差异。

通过 react 合成事件的学习,学习 react 对于事件系统的分层设计,它将不同类型的事件做成了插件,每个插件提供注册事件和提取事件两个接口,插件自身去实现事件的注册和事件提取。

这样做的好处是,当我们需要扩展新的事件时,只需要实现这两个接口就可以了,不需要修改原有的代码。