Skip to content

Commit

Permalink
Event API: ensure event keys are unique + add validation (#15501)
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm authored Apr 26, 2019
1 parent d983974 commit 8658611
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 70 deletions.
3 changes: 2 additions & 1 deletion packages/events/EventSystemFlags.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export const PLUGIN_EVENT_SYSTEM = 1;
export const RESPONDER_EVENT_SYSTEM = 1 << 1;
export const IS_PASSIVE = 1 << 2;
export const IS_ACTIVE = 1 << 3;
export const PASSIVE_NOT_SUPPORTED = 1 << 4;
export const IS_CAPTURE = 1 << 4;
export const PASSIVE_NOT_SUPPORTED = 1 << 5;
16 changes: 9 additions & 7 deletions packages/react-dom/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {canUseDOM} from 'shared/ExecutionEnvironment';
import warningWithoutStack from 'shared/warningWithoutStack';
import type {ReactEventResponderEventType} from 'shared/ReactTypes';
import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
import {setListenToResponderEventTypes} from '../events/DOMEventResponderSystem';
import {
setListenToResponderEventTypes,
generateListeningKey,
} from '../events/DOMEventResponderSystem';

import {
getValueForAttribute,
Expand Down Expand Up @@ -1320,12 +1323,11 @@ export function listenToEventResponderEventTypes(
capture = targetEventConfigObject.capture;
}
}
// Create a unique name for this event, plus its properties. We'll
// use this to ensure we don't listen to the same event with the same
// properties again.
const passiveKey = passive ? '_passive' : '_active';
const captureKey = capture ? '_capture' : '';
const listeningName = `${topLevelType}${passiveKey}${captureKey}`;
const listeningName = generateListeningKey(
topLevelType,
passive,
capture,
);
if (!listeningSet.has(listeningName)) {
trapEventForResponderEventSystem(
element,
Expand Down
204 changes: 142 additions & 62 deletions packages/react-dom/src/events/DOMEventResponderSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {
type EventSystemFlags,
IS_PASSIVE,
IS_CAPTURE,
PASSIVE_NOT_SUPPORTED,
} from 'events/EventSystemFlags';
import type {AnyNativeEvent} from 'events/PluginModuleType';
Expand Down Expand Up @@ -73,7 +74,7 @@ const rootEventTypesToEventComponentInstances: Map<
> = new Map();
const targetEventTypeCached: Map<
Array<ReactEventResponderEventType>,
Set<DOMTopLevelEventType>,
Set<string>,
> = new Map();
const ownershipChangeListeners: Set<ReactEventComponentInstance> = new Set();
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
Expand Down Expand Up @@ -235,32 +236,8 @@ const eventResponderContext: ReactResponderContext = {
listenToResponderEventTypesImpl(rootEventTypes, activeDocument);
for (let i = 0; i < rootEventTypes.length; i++) {
const rootEventType = rootEventTypes[i];
const topLevelEventType =
typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get(
topLevelEventType,
);
if (rootEventComponentInstances === undefined) {
rootEventComponentInstances = new Set();
rootEventTypesToEventComponentInstances.set(
topLevelEventType,
rootEventComponentInstances,
);
}
const componentInstance = ((currentInstance: any): ReactEventComponentInstance);
let rootEventTypesSet = componentInstance.rootEventTypes;
if (rootEventTypesSet === null) {
rootEventTypesSet = componentInstance.rootEventTypes = new Set();
}
invariant(
!rootEventTypesSet.has(topLevelEventType),
'addRootEventTypes() found a duplicate root event ' +
'type of "%s". This might be because the event type exists in the event responder "rootEventTypes" ' +
'array or because of a previous addRootEventTypes() using this root event type.',
rootEventType,
);
rootEventTypesSet.add(topLevelEventType);
rootEventComponentInstances.add(componentInstance);
const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance);
registerRootEventType(rootEventType, eventComponentInstance);
}
},
removeRootEventTypes(
Expand All @@ -269,15 +246,37 @@ const eventResponderContext: ReactResponderContext = {
validateResponderContext();
for (let i = 0; i < rootEventTypes.length; i++) {
const rootEventType = rootEventTypes[i];
const topLevelEventType =
typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
let name = rootEventType;
let capture = false;
let passive = true;

if (typeof rootEventType !== 'string') {
const targetEventConfigObject = ((rootEventType: any): {
name: string,
passive?: boolean,
capture?: boolean,
});
name = targetEventConfigObject.name;
if (targetEventConfigObject.passive !== undefined) {
passive = targetEventConfigObject.passive;
}
if (targetEventConfigObject.capture !== undefined) {
capture = targetEventConfigObject.capture;
}
}

const listeningName = generateListeningKey(
((name: any): string),
passive,
capture,
);
let rootEventComponents = rootEventTypesToEventComponentInstances.get(
topLevelEventType,
listeningName,
);
let rootEventTypesSet = ((currentInstance: any): ReactEventComponentInstance)
.rootEventTypes;
if (rootEventTypesSet !== null) {
rootEventTypesSet.delete(topLevelEventType);
rootEventTypesSet.delete(listeningName);
}
if (rootEventComponents !== undefined) {
rootEventComponents.delete(
Expand Down Expand Up @@ -476,14 +475,15 @@ function createResponderEvent(
topLevelType: string,
nativeEvent: AnyNativeEvent,
nativeEventTarget: Element | Document,
eventSystemFlags: EventSystemFlags,
passive: boolean,
passiveSupported: boolean,
): ReactResponderEvent {
const responderEvent = {
nativeEvent: nativeEvent,
target: nativeEventTarget,
type: topLevelType,
passive: (eventSystemFlags & IS_PASSIVE) !== 0,
passiveSupported: (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0,
passive,
passiveSupported,
};
if (__DEV__) {
Object.freeze(responderEvent);
Expand Down Expand Up @@ -529,24 +529,45 @@ export function processEventQueue(): void {

function getTargetEventTypesSet(
eventTypes: Array<ReactEventResponderEventType>,
): Set<DOMTopLevelEventType> {
): Set<string> {
let cachedSet = targetEventTypeCached.get(eventTypes);

if (cachedSet === undefined) {
cachedSet = new Set();
for (let i = 0; i < eventTypes.length; i++) {
const eventType = eventTypes[i];
const topLevelEventType =
typeof eventType === 'string' ? eventType : eventType.name;
cachedSet.add(((topLevelEventType: any): DOMTopLevelEventType));
let name = eventType;
let capture = false;
let passive = true;

if (typeof eventType !== 'string') {
const targetEventConfigObject = ((eventType: any): {
name: string,
passive?: boolean,
capture?: boolean,
});
name = targetEventConfigObject.name;
if (targetEventConfigObject.passive !== undefined) {
passive = targetEventConfigObject.passive;
}
if (targetEventConfigObject.capture !== undefined) {
capture = targetEventConfigObject.capture;
}
}
const listeningName = generateListeningKey(
((name: any): string),
passive,
capture,
);
cachedSet.add(listeningName);
}
targetEventTypeCached.set(eventTypes, cachedSet);
}
return cachedSet;
}

function getTargetEventResponderInstances(
topLevelType: DOMTopLevelEventType,
listeningName: string,
targetFiber: null | Fiber,
): Array<ReactEventComponentInstance> {
const eventResponderInstances = [];
Expand All @@ -560,7 +581,7 @@ function getTargetEventResponderInstances(
// Validate the target event type exists on the responder
if (targetEventTypes !== undefined) {
const targetEventTypesSet = getTargetEventTypesSet(targetEventTypes);
if (targetEventTypesSet.has(topLevelType)) {
if (targetEventTypesSet.has(listeningName)) {
eventResponderInstances.push(eventComponentInstance);
}
}
Expand All @@ -571,11 +592,11 @@ function getTargetEventResponderInstances(
}

function getRootEventResponderInstances(
topLevelType: DOMTopLevelEventType,
listeningName: string,
): Array<ReactEventComponentInstance> {
const eventResponderInstances = [];
const rootEventInstances = rootEventTypesToEventComponentInstances.get(
topLevelType,
listeningName,
);
if (rootEventInstances !== undefined) {
const rootEventComponentInstances = Array.from(rootEventInstances);
Expand Down Expand Up @@ -618,20 +639,30 @@ function traverseAndHandleEventResponderInstances(
nativeEventTarget: EventTarget,
eventSystemFlags: EventSystemFlags,
): void {
const isPassiveEvent = (eventSystemFlags & IS_PASSIVE) !== 0;
const isCaptureEvent = (eventSystemFlags & IS_CAPTURE) !== 0;
const isPassiveSupported = (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0;
const listeningName = generateListeningKey(
((topLevelType: any): string),
isPassiveEvent || !isPassiveSupported,
isCaptureEvent,
);

// Trigger event responders in this order:
// - Capture target phase
// - Bubble target phase
// - Root phase

const targetEventResponderInstances = getTargetEventResponderInstances(
topLevelType,
listeningName,
targetFiber,
);
const responderEvent = createResponderEvent(
((topLevelType: any): string),
nativeEvent,
((nativeEventTarget: any): Element | Document),
eventSystemFlags,
isPassiveEvent,
isPassiveSupported,
);
const propagatedEventResponders: Set<ReactEventResponder> = new Set();
let length = targetEventResponderInstances.length;
Expand Down Expand Up @@ -684,7 +715,7 @@ function traverseAndHandleEventResponderInstances(
}
// Root phase
const rootEventResponderInstances = getRootEventResponderInstances(
topLevelType,
listeningName,
);
length = rootEventResponderInstances.length;
if (length > 0) {
Expand Down Expand Up @@ -835,25 +866,74 @@ export function addRootEventTypesForComponentInstance(
): void {
for (let i = 0; i < rootEventTypes.length; i++) {
const rootEventType = rootEventTypes[i];
const topLevelEventType =
typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get(
topLevelEventType,
);
if (rootEventComponentInstances === undefined) {
rootEventComponentInstances = new Set();
rootEventTypesToEventComponentInstances.set(
topLevelEventType,
rootEventComponentInstances,
);
registerRootEventType(rootEventType, eventComponentInstance);
}
}

function registerRootEventType(
rootEventType: ReactEventResponderEventType,
eventComponentInstance: ReactEventComponentInstance,
): void {
let name = rootEventType;
let capture = false;
let passive = true;

if (typeof rootEventType !== 'string') {
const targetEventConfigObject = ((rootEventType: any): {
name: string,
passive?: boolean,
capture?: boolean,
});
name = targetEventConfigObject.name;
if (targetEventConfigObject.passive !== undefined) {
passive = targetEventConfigObject.passive;
}
let rootEventTypesSet = eventComponentInstance.rootEventTypes;
if (rootEventTypesSet === null) {
rootEventTypesSet = eventComponentInstance.rootEventTypes = new Set();
if (targetEventConfigObject.capture !== undefined) {
capture = targetEventConfigObject.capture;
}
rootEventTypesSet.add(topLevelEventType);
rootEventComponentInstances.add(
((eventComponentInstance: any): ReactEventComponentInstance),
}

const listeningName = generateListeningKey(
((name: any): string),
passive,
capture,
);
let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get(
listeningName,
);
if (rootEventComponentInstances === undefined) {
rootEventComponentInstances = new Set();
rootEventTypesToEventComponentInstances.set(
listeningName,
rootEventComponentInstances,
);
}
let rootEventTypesSet = eventComponentInstance.rootEventTypes;
if (rootEventTypesSet === null) {
rootEventTypesSet = eventComponentInstance.rootEventTypes = new Set();
}
invariant(
!rootEventTypesSet.has(listeningName),
'addRootEventTypes() found a duplicate root event ' +
'type of "%s". This might be because the event type exists in the event responder "rootEventTypes" ' +
'array or because of a previous addRootEventTypes() using this root event type.',
name,
);
rootEventTypesSet.add(listeningName);
rootEventComponentInstances.add(
((eventComponentInstance: any): ReactEventComponentInstance),
);
}

export function generateListeningKey(
topLevelType: string,
passive: boolean,
capture: boolean,
): string {
// Create a unique name for this event, plus its properties. We'll
// use this to ensure we don't listen to the same event with the same
// properties again.
const passiveKey = passive ? '_passive' : '_active';
const captureKey = capture ? '_capture' : '';
return `${topLevelType}${passiveKey}${captureKey}`;
}
4 changes: 4 additions & 0 deletions packages/react-dom/src/events/ReactDOMEventListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
RESPONDER_EVENT_SYSTEM,
IS_PASSIVE,
IS_ACTIVE,
IS_CAPTURE,
PASSIVE_NOT_SUPPORTED,
} from 'events/EventSystemFlags';

Expand Down Expand Up @@ -189,6 +190,9 @@ export function trapEventForResponderEventSystem(
} else {
eventFlags |= IS_ACTIVE;
}
if (capture) {
eventFlags |= IS_CAPTURE;
}
// Check if interactive and wrap in interactiveUpdates
const listener = dispatchEvent.bind(null, topLevelType, eventFlags);
addEventListener(element, rawEventName, listener, {
Expand Down
Loading

0 comments on commit 8658611

Please sign in to comment.