Refine event registration + event signatures (#19244)
* Refine event registration + event signatures * Address feedback
Dominic Gannaway committed
Jul 6, 2020 at 17:42 UTC
26071abbe1eda6fb88dfaef9e7bce43f3a774818
10 files changed
+69
-69
packages/react-dom/src/client/ReactDOMComponent.js
+11
-4
@@ -7,6 +7,8 @@
7
* @flow
8
*/
9
10
+import type {ElementListenerMapEntry} from '../client/ReactDOMComponentTree';
11
+
12
import {
13
registrationNameDependencies,
14
possibleRegistrationNames,
@@ -82,7 +84,7 @@ import {
84
enableDeprecatedFlareAPI,
85
enableTrustedTypesIntegration,
86
} from 'shared/ReactFeatureFlags';
85
-import {listenToEvent} from '../events/DOMModernPluginEventSystem';
87
+import {listenToReactPropEvent} from '../events/DOMModernPluginEventSystem';
88
import {getEventListenerMap} from './ReactDOMComponentTree';
89
90
let didWarnInvalidHydration = false;
@@ -262,7 +264,7 @@ if (__DEV__) {
264
265
export function ensureListeningTo(
266
rootContainerInstance: Element | Node,
265
- registrationName: string,
267
+ reactPropEvent: string,
268
): void {
269
// If we have a comment node, then use the parent node,
270
// which should be an element.
@@ -279,7 +281,10 @@ export function ensureListeningTo(
281
'ensureListeningTo(): received a container that was not an element node. ' +
282
'This is likely a bug in React.',
283
);
282
- listenToEvent(registrationName, ((rootContainerElement: any): Element));
284
+ listenToReactPropEvent(
285
+ reactPropEvent,
286
+ ((rootContainerElement: any): Element),
287
+ );
288
}
289
290
function getOwnerDocumentFromRootContainer(
@@ -1267,7 +1272,9 @@ export function listenToEventResponderEventTypes(
1272
// existing passive event listener before we add the
1273
// active event listener.
1274
const passiveKey = targetEventType + '_passive';
1270
- const passiveItem = listenerMap.get(passiveKey);
1275
+ const passiveItem = ((listenerMap.get(
1276
+ passiveKey,
1277
+ ): any): ElementListenerMapEntry | void);
1278
if (passiveItem !== undefined) {
1279
removeTrappedEventListener(
1280
document,
packages/react-dom/src/client/ReactDOMComponentTree.js
+1
-1
@@ -42,7 +42,7 @@ const internalEventHandlerListenersKey = '__reactListeners$' + randomKey;
42
43
export type ElementListenerMap = Map<
44
DOMTopLevelEventType | string,
45
- ElementListenerMapEntry,
45
+ ElementListenerMapEntry | null,
46
>;
47
48
export type ElementListenerMapEntry = {
packages/react-dom/src/client/ReactDOMEventHandle.js
+4
-1
@@ -26,6 +26,7 @@ import {ELEMENT_NODE} from '../shared/HTMLNodeType';
26
import {
27
listenToTopLevelEvent,
28
addEventTypeToDispatchConfig,
29
+ capturePhaseEvents,
30
} from '../events/DOMModernPluginEventSystem';
31
32
import {HostRoot, HostPortal} from 'react-reconciler/src/ReactWorkTags';
@@ -98,11 +99,13 @@ function registerEventOnNearestTargetContainer(
99
);
100
}
101
const listenerMap = getEventListenerMap(targetContainer);
102
+ const capture = capturePhaseEvents.has(topLevelType);
103
listenToTopLevelEvent(
104
topLevelType,
105
targetContainer,
106
listenerMap,
107
PLUGIN_EVENT_SYSTEM,
108
+ capture,
109
passive,
110
priority,
111
);
@@ -201,9 +204,9 @@ export function createEventHandle(
204
eventTarget,
205
listenerMap,
206
PLUGIN_EVENT_SYSTEM | IS_TARGET_PHASE_ONLY,
207
+ capture,
208
passive,
209
priority,
206
- capture,
210
);
211
} else {
212
invariant(
packages/react-dom/src/client/ReactDOMHostConfig.js
+2
-2
@@ -81,7 +81,7 @@ import {
81
import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
82
import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';
83
import {
84
- listenToEvent,
84
+ listenToReactPropEvent,
85
clearEventHandleListenersForTarget,
86
} from '../events/DOMModernPluginEventSystem';
87
@@ -1122,7 +1122,7 @@ export function makeOpaqueHydratingObject(
1122
}
1123
1124
export function preparePortalMount(portalInstance: Instance): void {
1125
- listenToEvent('onMouseEnter', portalInstance);
1125
+ listenToReactPropEvent('onMouseEnter', portalInstance);
1126
}
1127
1128
export function prepareScopeUpdate(
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+25
-12
@@ -30,6 +30,7 @@ import {
30
LEGACY_FB_SUPPORT,
31
IS_REPLAYED,
32
IS_TARGET_PHASE_ONLY,
33
+ IS_CAPTURE_PHASE,
34
} from './EventSystemFlags';
35
36
import {
@@ -301,9 +302,9 @@ export function listenToTopLevelEvent(
302
target: EventTarget,
303
listenerMap: ElementListenerMap,
304
eventSystemFlags: EventSystemFlags,
305
+ capture: boolean,
306
passive?: boolean,
307
priority?: EventPriority,
306
- capture?: boolean,
308
): void {
309
// TOP_SELECTION_CHANGE needs to be attached to the document
310
// otherwise it won't capture incoming events that are only
@@ -312,12 +313,10 @@ export function listenToTopLevelEvent(
313
target = (target: any).ownerDocument || target;
314
listenerMap = getEventListenerMap(target);
315
}
315
- capture =
316
- capture === undefined ? capturePhaseEvents.has(topLevelType) : capture;
316
const listenerMapKey = getListenerMapKey(topLevelType, capture);
318
- const listenerEntry: ElementListenerMapEntry | void = listenerMap.get(
317
+ const listenerEntry = ((listenerMap.get(
318
listenerMapKey,
320
- );
319
+ ): any): ElementListenerMapEntry | void);
320
const shouldUpgrade = shouldUpgradeListener(listenerEntry, passive);
321
322
// If the listener entry is empty or we should upgrade, then
@@ -333,6 +332,9 @@ export function listenToTopLevelEvent(
332
((listenerEntry: any): ElementListenerMapEntry).listener,
333
);
334
}
335
+ if (capture) {
336
+ eventSystemFlags |= IS_CAPTURE_PHASE;
337
+ }
338
const listener = addTrappedEventListener(
339
target,
340
topLevelType,
@@ -346,20 +348,31 @@ export function listenToTopLevelEvent(
348
}
349
}
350
349
-export function listenToEvent(
350
- registrationName: string,
351
+export function listenToReactPropEvent(
352
+ reactPropEvent: string,
353
rootContainerElement: Element,
354
): void {
355
const listenerMap = getEventListenerMap(rootContainerElement);
354
- const dependencies = registrationNameDependencies[registrationName];
356
+ // For optimization, let's check if we have the registration name
357
+ // on the rootContainerElement.
358
+ if (listenerMap.has(reactPropEvent)) {
359
+ return;
360
+ }
361
+ // Add the registration name to the map, so we can avoid processing
362
+ // this React prop event again.
363
+ listenerMap.set(reactPropEvent, null);
364
+ const dependencies = registrationNameDependencies[reactPropEvent];
365
366
for (let i = 0; i < dependencies.length; i++) {
367
const dependency = dependencies[i];
368
+ const capture = capturePhaseEvents.has(dependency);
369
+
370
listenToTopLevelEvent(
371
dependency,
372
rootContainerElement,
373
listenerMap,
374
PLUGIN_EVENT_SYSTEM,
375
+ capture,
376
);
377
}
378
}
@@ -892,10 +905,11 @@ export function accumulateEnterLeaveListeners(
905
}
906
}
907
895
-export function accumulateEventTargetListeners(
908
+export function accumulateEventHandleTargetListeners(
909
dispatchQueue: DispatchQueue,
910
event: ReactSyntheticEvent,
911
currentTarget: EventTarget,
912
+ inCapturePhase: boolean,
913
): void {
914
const capturePhase: DispatchQueueItemPhase = [];
915
const bubblePhase: DispatchQueueItemPhase = [];
@@ -904,17 +918,16 @@ export function accumulateEventTargetListeners(
918
if (eventListeners !== null) {
919
const listenersArr = Array.from(eventListeners);
920
const targetType = ((event.type: any): DOMTopLevelEventType);
907
- const isCapturePhase = (event: any).eventPhase === 1;
921
922
for (let i = 0; i < listenersArr.length; i++) {
923
const listener = listenersArr[i];
924
const {callback, capture, type} = listener;
925
if (type === targetType) {
913
- if (isCapturePhase && capture) {
926
+ if (inCapturePhase && capture) {
927
capturePhase.push(
928
createDispatchQueueItemPhaseEntry(null, callback, currentTarget),
929
);
917
- } else if (!isCapturePhase && !capture) {
930
+ } else if (!inCapturePhase && !capture) {
931
bubblePhase.push(
932
createDispatchQueueItemPhaseEntry(null, callback, currentTarget),
933
);
packages/react-dom/src/events/EventSystemFlags.js
+6
-5
@@ -12,8 +12,9 @@ export type EventSystemFlags = number;
12
export const PLUGIN_EVENT_SYSTEM = 1;
13
export const RESPONDER_EVENT_SYSTEM = 1 << 1;
14
export const IS_TARGET_PHASE_ONLY = 1 << 2;
15
-export const IS_PASSIVE = 1 << 3;
16
-export const PASSIVE_NOT_SUPPORTED = 1 << 4;
17
-export const IS_REPLAYED = 1 << 5;
18
-export const IS_FIRST_ANCESTOR = 1 << 6;
19
-export const LEGACY_FB_SUPPORT = 1 << 7;
15
+export const IS_CAPTURE_PHASE = 1 << 3;
16
+export const IS_PASSIVE = 1 << 4;
17
+export const PASSIVE_NOT_SUPPORTED = 1 << 5;
18
+export const IS_REPLAYED = 1 << 6;
19
+export const IS_FIRST_ANCESTOR = 1 << 7;
20
+export const LEGACY_FB_SUPPORT = 1 << 8;
packages/react-dom/src/events/ReactDOMEventReplaying.js
+1
@@ -220,6 +220,7 @@ function trapReplayableEventForContainer(
220
((container: any): Element),
221
listenerMap,
222
PLUGIN_EVENT_SYSTEM,
223
+ false,
224
);
225
}
226
packages/react-dom/src/events/plugins/ModernSelectEventPlugin.js
+5
-37
@@ -29,11 +29,7 @@ import {
29
} from '../../client/ReactDOMComponentTree';
30
import {hasSelectionCapabilities} from '../../client/ReactInputSelection';
31
import {DOCUMENT_NODE} from '../../shared/HTMLNodeType';
32
-import {
33
- accumulateTwoPhaseListeners,
34
- getListenerMapKey,
35
- capturePhaseEvents,
36
-} from '../DOMModernPluginEventSystem';
32
+import {accumulateTwoPhaseListeners} from '../DOMModernPluginEventSystem';
33
34
const skipSelectionChangeEvent =
35
canUseDOM && 'documentMode' in document && document.documentMode <= 11;
@@ -148,32 +144,6 @@ function constructSelectEvent(dispatchQueue, nativeEvent, nativeEventTarget) {
144
}
145
}
146
151
-function isListeningToEvents(
152
- events: Array<string>,
153
- mountAt: Document | Element,
154
-): boolean {
155
- const listenerMap = getEventListenerMap(mountAt);
156
- for (let i = 0; i < events.length; i++) {
157
- const event = events[i];
158
- const capture = capturePhaseEvents.has(event);
159
- const listenerMapKey = getListenerMapKey(event, capture);
160
- if (!listenerMap.has(listenerMapKey)) {
161
- return false;
162
- }
163
- }
164
- return true;
165
-}
166
-
167
-function isListeningToEvent(
168
- registrationName: string,
169
- mountAt: Document | Element,
170
-): boolean {
171
- const listenerMap = getEventListenerMap(mountAt);
172
- const capture = capturePhaseEvents.has(registrationName);
173
- const listenerMapKey = getListenerMapKey(registrationName, capture);
174
- return listenerMap.has(listenerMapKey);
175
-}
176
-
147
/**
148
* This plugin creates an `onSelect` event that normalizes select events
149
* across form elements.
@@ -197,19 +167,17 @@ function extractEvents(
167
eventSystemFlags,
168
targetContainer,
169
) {
200
- const doc = getEventTargetDocument(nativeEventTarget);
170
+ const eventListenerMap = getEventListenerMap(targetContainer);
171
// Track whether all listeners exists for this plugin. If none exist, we do
172
// not extract events. See #3639.
173
if (
204
- // We only listen to TOP_SELECTION_CHANGE on the document, never the
205
- // root.
206
- !isListeningToEvent(TOP_SELECTION_CHANGE, doc) ||
174
// If we are handling TOP_SELECTION_CHANGE, then we don't need to
175
// check for the other dependencies, as TOP_SELECTION_CHANGE is only
176
// event attached from the onChange plugin and we don't expose an
177
// onSelectionChange event from React.
211
- (topLevelType !== TOP_SELECTION_CHANGE &&
212
- !isListeningToEvents(rootTargetDependencies, targetContainer))
178
+ topLevelType !== TOP_SELECTION_CHANGE &&
179
+ !eventListenerMap.has('onSelect') &&
180
+ !eventListenerMap.has('onSelectCapture')
181
) {
182
return;
183
}
packages/react-dom/src/events/plugins/ModernSimpleEventPlugin.js
+9
-3
@@ -24,10 +24,9 @@ import {
24
} from '../DOMEventProperties';
25
import {
26
accumulateTwoPhaseListeners,
27
- accumulateEventTargetListeners,
27
+ accumulateEventHandleTargetListeners,
28
} from '../DOMModernPluginEventSystem';
29
import {IS_TARGET_PHASE_ONLY} from '../EventSystemFlags';
30
-
30
import SyntheticAnimationEvent from '../SyntheticAnimationEvent';
31
import SyntheticClipboardEvent from '../SyntheticClipboardEvent';
32
import SyntheticFocusEvent from '../SyntheticFocusEvent';
@@ -40,6 +39,7 @@ import SyntheticTransitionEvent from '../SyntheticTransitionEvent';
39
import SyntheticUIEvent from '../SyntheticUIEvent';
40
import SyntheticWheelEvent from '../SyntheticWheelEvent';
41
import getEventCharCode from '../getEventCharCode';
42
+import {IS_CAPTURE_PHASE} from '../EventSystemFlags';
43
44
import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
45
@@ -158,7 +158,13 @@ function extractEvents(
158
eventSystemFlags & IS_TARGET_PHASE_ONLY &&
159
targetContainer != null
160
) {
161
- accumulateEventTargetListeners(dispatchQueue, event, targetContainer);
161
+ const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
162
+ accumulateEventHandleTargetListeners(
163
+ dispatchQueue,
164
+ event,
165
+ targetContainer,
166
+ inCapturePhase,
167
+ );
168
} else {
169
accumulateTwoPhaseListeners(targetInst, dispatchQueue, event, true);
170
}
packages/react-interactions/events/src/dom/create-event-handle/Focus.js
+5
-4
@@ -126,6 +126,7 @@ function handleGlobalFocusVisibleEvent(
126
}
127
128
const passiveObject = {passive: true};
129
+const passiveCaptureObject = {capture: true, passive: false};
130
131
function handleFocusVisibleTargetEvent(
132
type: string,
@@ -242,8 +243,8 @@ export function useFocus(
243
): void {
244
// Setup controlled state for this useFocus hook
245
const stateRef = useRef({isFocused: false, isFocusVisible: false});
245
- const focusHandle = useEvent('focus', passiveObject);
246
- const blurHandle = useEvent('blur', passiveObject);
246
+ const focusHandle = useEvent('focus', passiveCaptureObject);
247
+ const blurHandle = useEvent('blur', passiveCaptureObject);
248
const focusVisibleHandles = useFocusVisibleInputHandles();
249
250
useEffect(() => {
@@ -329,8 +330,8 @@ export function useFocusWithin(
330
) {
331
// Setup controlled state for this useFocus hook
332
const stateRef = useRef({isFocused: false, isFocusVisible: false});
332
- const focusHandle = useEvent('focus', passiveObject);
333
- const blurHandle = useEvent('blur', passiveObject);
333
+ const focusHandle = useEvent('focus', passiveCaptureObject);
334
+ const blurHandle = useEvent('blur', passiveCaptureObject);
335
const afterBlurHandle = useEvent('afterblur', passiveObject);
336
const beforeBlurHandle = useEvent('beforeblur', passiveObject);
337
const focusVisibleHandles = useFocusVisibleInputHandles();