Skip to content

Commit 8658611

Browse files
authored
Event API: ensure event keys are unique + add validation (#15501)
1 parent d983974 commit 8658611

File tree

5 files changed

+287
-70
lines changed

5 files changed

+287
-70
lines changed

packages/events/EventSystemFlags.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ export const PLUGIN_EVENT_SYSTEM = 1;
1313
export const RESPONDER_EVENT_SYSTEM = 1 << 1;
1414
export const IS_PASSIVE = 1 << 2;
1515
export const IS_ACTIVE = 1 << 3;
16-
export const PASSIVE_NOT_SUPPORTED = 1 << 4;
16+
export const IS_CAPTURE = 1 << 4;
17+
export const PASSIVE_NOT_SUPPORTED = 1 << 5;

packages/react-dom/src/client/ReactDOMComponent.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import {canUseDOM} from 'shared/ExecutionEnvironment';
1515
import warningWithoutStack from 'shared/warningWithoutStack';
1616
import type {ReactEventResponderEventType} from 'shared/ReactTypes';
1717
import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
18-
import {setListenToResponderEventTypes} from '../events/DOMEventResponderSystem';
18+
import {
19+
setListenToResponderEventTypes,
20+
generateListeningKey,
21+
} from '../events/DOMEventResponderSystem';
1922

2023
import {
2124
getValueForAttribute,
@@ -1320,12 +1323,11 @@ export function listenToEventResponderEventTypes(
13201323
capture = targetEventConfigObject.capture;
13211324
}
13221325
}
1323-
// Create a unique name for this event, plus its properties. We'll
1324-
// use this to ensure we don't listen to the same event with the same
1325-
// properties again.
1326-
const passiveKey = passive ? '_passive' : '_active';
1327-
const captureKey = capture ? '_capture' : '';
1328-
const listeningName = `${topLevelType}${passiveKey}${captureKey}`;
1326+
const listeningName = generateListeningKey(
1327+
topLevelType,
1328+
passive,
1329+
capture,
1330+
);
13291331
if (!listeningSet.has(listeningName)) {
13301332
trapEventForResponderEventSystem(
13311333
element,

packages/react-dom/src/events/DOMEventResponderSystem.js

Lines changed: 142 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {
1010
type EventSystemFlags,
1111
IS_PASSIVE,
12+
IS_CAPTURE,
1213
PASSIVE_NOT_SUPPORTED,
1314
} from 'events/EventSystemFlags';
1415
import type {AnyNativeEvent} from 'events/PluginModuleType';
@@ -73,7 +74,7 @@ const rootEventTypesToEventComponentInstances: Map<
7374
> = new Map();
7475
const targetEventTypeCached: Map<
7576
Array<ReactEventResponderEventType>,
76-
Set<DOMTopLevelEventType>,
77+
Set<string>,
7778
> = new Map();
7879
const ownershipChangeListeners: Set<ReactEventComponentInstance> = new Set();
7980
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
@@ -235,32 +236,8 @@ const eventResponderContext: ReactResponderContext = {
235236
listenToResponderEventTypesImpl(rootEventTypes, activeDocument);
236237
for (let i = 0; i < rootEventTypes.length; i++) {
237238
const rootEventType = rootEventTypes[i];
238-
const topLevelEventType =
239-
typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
240-
let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get(
241-
topLevelEventType,
242-
);
243-
if (rootEventComponentInstances === undefined) {
244-
rootEventComponentInstances = new Set();
245-
rootEventTypesToEventComponentInstances.set(
246-
topLevelEventType,
247-
rootEventComponentInstances,
248-
);
249-
}
250-
const componentInstance = ((currentInstance: any): ReactEventComponentInstance);
251-
let rootEventTypesSet = componentInstance.rootEventTypes;
252-
if (rootEventTypesSet === null) {
253-
rootEventTypesSet = componentInstance.rootEventTypes = new Set();
254-
}
255-
invariant(
256-
!rootEventTypesSet.has(topLevelEventType),
257-
'addRootEventTypes() found a duplicate root event ' +
258-
'type of "%s". This might be because the event type exists in the event responder "rootEventTypes" ' +
259-
'array or because of a previous addRootEventTypes() using this root event type.',
260-
rootEventType,
261-
);
262-
rootEventTypesSet.add(topLevelEventType);
263-
rootEventComponentInstances.add(componentInstance);
239+
const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance);
240+
registerRootEventType(rootEventType, eventComponentInstance);
264241
}
265242
},
266243
removeRootEventTypes(
@@ -269,15 +246,37 @@ const eventResponderContext: ReactResponderContext = {
269246
validateResponderContext();
270247
for (let i = 0; i < rootEventTypes.length; i++) {
271248
const rootEventType = rootEventTypes[i];
272-
const topLevelEventType =
273-
typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
249+
let name = rootEventType;
250+
let capture = false;
251+
let passive = true;
252+
253+
if (typeof rootEventType !== 'string') {
254+
const targetEventConfigObject = ((rootEventType: any): {
255+
name: string,
256+
passive?: boolean,
257+
capture?: boolean,
258+
});
259+
name = targetEventConfigObject.name;
260+
if (targetEventConfigObject.passive !== undefined) {
261+
passive = targetEventConfigObject.passive;
262+
}
263+
if (targetEventConfigObject.capture !== undefined) {
264+
capture = targetEventConfigObject.capture;
265+
}
266+
}
267+
268+
const listeningName = generateListeningKey(
269+
((name: any): string),
270+
passive,
271+
capture,
272+
);
274273
let rootEventComponents = rootEventTypesToEventComponentInstances.get(
275-
topLevelEventType,
274+
listeningName,
276275
);
277276
let rootEventTypesSet = ((currentInstance: any): ReactEventComponentInstance)
278277
.rootEventTypes;
279278
if (rootEventTypesSet !== null) {
280-
rootEventTypesSet.delete(topLevelEventType);
279+
rootEventTypesSet.delete(listeningName);
281280
}
282281
if (rootEventComponents !== undefined) {
283282
rootEventComponents.delete(
@@ -476,14 +475,15 @@ function createResponderEvent(
476475
topLevelType: string,
477476
nativeEvent: AnyNativeEvent,
478477
nativeEventTarget: Element | Document,
479-
eventSystemFlags: EventSystemFlags,
478+
passive: boolean,
479+
passiveSupported: boolean,
480480
): ReactResponderEvent {
481481
const responderEvent = {
482482
nativeEvent: nativeEvent,
483483
target: nativeEventTarget,
484484
type: topLevelType,
485-
passive: (eventSystemFlags & IS_PASSIVE) !== 0,
486-
passiveSupported: (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0,
485+
passive,
486+
passiveSupported,
487487
};
488488
if (__DEV__) {
489489
Object.freeze(responderEvent);
@@ -529,24 +529,45 @@ export function processEventQueue(): void {
529529

530530
function getTargetEventTypesSet(
531531
eventTypes: Array<ReactEventResponderEventType>,
532-
): Set<DOMTopLevelEventType> {
532+
): Set<string> {
533533
let cachedSet = targetEventTypeCached.get(eventTypes);
534534

535535
if (cachedSet === undefined) {
536536
cachedSet = new Set();
537537
for (let i = 0; i < eventTypes.length; i++) {
538538
const eventType = eventTypes[i];
539-
const topLevelEventType =
540-
typeof eventType === 'string' ? eventType : eventType.name;
541-
cachedSet.add(((topLevelEventType: any): DOMTopLevelEventType));
539+
let name = eventType;
540+
let capture = false;
541+
let passive = true;
542+
543+
if (typeof eventType !== 'string') {
544+
const targetEventConfigObject = ((eventType: any): {
545+
name: string,
546+
passive?: boolean,
547+
capture?: boolean,
548+
});
549+
name = targetEventConfigObject.name;
550+
if (targetEventConfigObject.passive !== undefined) {
551+
passive = targetEventConfigObject.passive;
552+
}
553+
if (targetEventConfigObject.capture !== undefined) {
554+
capture = targetEventConfigObject.capture;
555+
}
556+
}
557+
const listeningName = generateListeningKey(
558+
((name: any): string),
559+
passive,
560+
capture,
561+
);
562+
cachedSet.add(listeningName);
542563
}
543564
targetEventTypeCached.set(eventTypes, cachedSet);
544565
}
545566
return cachedSet;
546567
}
547568

548569
function getTargetEventResponderInstances(
549-
topLevelType: DOMTopLevelEventType,
570+
listeningName: string,
550571
targetFiber: null | Fiber,
551572
): Array<ReactEventComponentInstance> {
552573
const eventResponderInstances = [];
@@ -560,7 +581,7 @@ function getTargetEventResponderInstances(
560581
// Validate the target event type exists on the responder
561582
if (targetEventTypes !== undefined) {
562583
const targetEventTypesSet = getTargetEventTypesSet(targetEventTypes);
563-
if (targetEventTypesSet.has(topLevelType)) {
584+
if (targetEventTypesSet.has(listeningName)) {
564585
eventResponderInstances.push(eventComponentInstance);
565586
}
566587
}
@@ -571,11 +592,11 @@ function getTargetEventResponderInstances(
571592
}
572593

573594
function getRootEventResponderInstances(
574-
topLevelType: DOMTopLevelEventType,
595+
listeningName: string,
575596
): Array<ReactEventComponentInstance> {
576597
const eventResponderInstances = [];
577598
const rootEventInstances = rootEventTypesToEventComponentInstances.get(
578-
topLevelType,
599+
listeningName,
579600
);
580601
if (rootEventInstances !== undefined) {
581602
const rootEventComponentInstances = Array.from(rootEventInstances);
@@ -618,20 +639,30 @@ function traverseAndHandleEventResponderInstances(
618639
nativeEventTarget: EventTarget,
619640
eventSystemFlags: EventSystemFlags,
620641
): void {
642+
const isPassiveEvent = (eventSystemFlags & IS_PASSIVE) !== 0;
643+
const isCaptureEvent = (eventSystemFlags & IS_CAPTURE) !== 0;
644+
const isPassiveSupported = (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0;
645+
const listeningName = generateListeningKey(
646+
((topLevelType: any): string),
647+
isPassiveEvent || !isPassiveSupported,
648+
isCaptureEvent,
649+
);
650+
621651
// Trigger event responders in this order:
622652
// - Capture target phase
623653
// - Bubble target phase
624654
// - Root phase
625655

626656
const targetEventResponderInstances = getTargetEventResponderInstances(
627-
topLevelType,
657+
listeningName,
628658
targetFiber,
629659
);
630660
const responderEvent = createResponderEvent(
631661
((topLevelType: any): string),
632662
nativeEvent,
633663
((nativeEventTarget: any): Element | Document),
634-
eventSystemFlags,
664+
isPassiveEvent,
665+
isPassiveSupported,
635666
);
636667
const propagatedEventResponders: Set<ReactEventResponder> = new Set();
637668
let length = targetEventResponderInstances.length;
@@ -684,7 +715,7 @@ function traverseAndHandleEventResponderInstances(
684715
}
685716
// Root phase
686717
const rootEventResponderInstances = getRootEventResponderInstances(
687-
topLevelType,
718+
listeningName,
688719
);
689720
length = rootEventResponderInstances.length;
690721
if (length > 0) {
@@ -835,25 +866,74 @@ export function addRootEventTypesForComponentInstance(
835866
): void {
836867
for (let i = 0; i < rootEventTypes.length; i++) {
837868
const rootEventType = rootEventTypes[i];
838-
const topLevelEventType =
839-
typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
840-
let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get(
841-
topLevelEventType,
842-
);
843-
if (rootEventComponentInstances === undefined) {
844-
rootEventComponentInstances = new Set();
845-
rootEventTypesToEventComponentInstances.set(
846-
topLevelEventType,
847-
rootEventComponentInstances,
848-
);
869+
registerRootEventType(rootEventType, eventComponentInstance);
870+
}
871+
}
872+
873+
function registerRootEventType(
874+
rootEventType: ReactEventResponderEventType,
875+
eventComponentInstance: ReactEventComponentInstance,
876+
): void {
877+
let name = rootEventType;
878+
let capture = false;
879+
let passive = true;
880+
881+
if (typeof rootEventType !== 'string') {
882+
const targetEventConfigObject = ((rootEventType: any): {
883+
name: string,
884+
passive?: boolean,
885+
capture?: boolean,
886+
});
887+
name = targetEventConfigObject.name;
888+
if (targetEventConfigObject.passive !== undefined) {
889+
passive = targetEventConfigObject.passive;
849890
}
850-
let rootEventTypesSet = eventComponentInstance.rootEventTypes;
851-
if (rootEventTypesSet === null) {
852-
rootEventTypesSet = eventComponentInstance.rootEventTypes = new Set();
891+
if (targetEventConfigObject.capture !== undefined) {
892+
capture = targetEventConfigObject.capture;
853893
}
854-
rootEventTypesSet.add(topLevelEventType);
855-
rootEventComponentInstances.add(
856-
((eventComponentInstance: any): ReactEventComponentInstance),
894+
}
895+
896+
const listeningName = generateListeningKey(
897+
((name: any): string),
898+
passive,
899+
capture,
900+
);
901+
let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get(
902+
listeningName,
903+
);
904+
if (rootEventComponentInstances === undefined) {
905+
rootEventComponentInstances = new Set();
906+
rootEventTypesToEventComponentInstances.set(
907+
listeningName,
908+
rootEventComponentInstances,
857909
);
858910
}
911+
let rootEventTypesSet = eventComponentInstance.rootEventTypes;
912+
if (rootEventTypesSet === null) {
913+
rootEventTypesSet = eventComponentInstance.rootEventTypes = new Set();
914+
}
915+
invariant(
916+
!rootEventTypesSet.has(listeningName),
917+
'addRootEventTypes() found a duplicate root event ' +
918+
'type of "%s". This might be because the event type exists in the event responder "rootEventTypes" ' +
919+
'array or because of a previous addRootEventTypes() using this root event type.',
920+
name,
921+
);
922+
rootEventTypesSet.add(listeningName);
923+
rootEventComponentInstances.add(
924+
((eventComponentInstance: any): ReactEventComponentInstance),
925+
);
926+
}
927+
928+
export function generateListeningKey(
929+
topLevelType: string,
930+
passive: boolean,
931+
capture: boolean,
932+
): string {
933+
// Create a unique name for this event, plus its properties. We'll
934+
// use this to ensure we don't listen to the same event with the same
935+
// properties again.
936+
const passiveKey = passive ? '_passive' : '_active';
937+
const captureKey = capture ? '_capture' : '';
938+
return `${topLevelType}${passiveKey}${captureKey}`;
859939
}

packages/react-dom/src/events/ReactDOMEventListener.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
RESPONDER_EVENT_SYSTEM,
2323
IS_PASSIVE,
2424
IS_ACTIVE,
25+
IS_CAPTURE,
2526
PASSIVE_NOT_SUPPORTED,
2627
} from 'events/EventSystemFlags';
2728

@@ -189,6 +190,9 @@ export function trapEventForResponderEventSystem(
189190
} else {
190191
eventFlags |= IS_ACTIVE;
191192
}
193+
if (capture) {
194+
eventFlags |= IS_CAPTURE;
195+
}
192196
// Check if interactive and wrap in interactiveUpdates
193197
const listener = dispatchEvent.bind(null, topLevelType, eventFlags);
194198
addEventListener(element, rawEventName, listener, {

0 commit comments

Comments
 (0)