Skip to content

Commit acfe4b2

Browse files
authored
[react-interactions] Upgrade passive event listeners to active listeners (#17513)
1 parent 5235d19 commit acfe4b2

File tree

8 files changed

+145
-114
lines changed

8 files changed

+145
-114
lines changed

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

+35-11
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import warning from 'shared/warning';
1414
import {canUseDOM} from 'shared/ExecutionEnvironment';
1515
import warningWithoutStack from 'shared/warningWithoutStack';
1616
import endsWith from 'shared/endsWith';
17-
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
1817
import {setListenToResponderEventTypes} from '../events/DOMEventResponderSystem';
1918

2019
import {
@@ -63,9 +62,12 @@ import {
6362
import {
6463
listenTo,
6564
trapBubbledEvent,
66-
getListeningSetForElement,
65+
getListenerMapForElement,
6766
} from '../events/ReactBrowserEventEmitter';
68-
import {trapEventForResponderEventSystem} from '../events/ReactDOMEventListener.js';
67+
import {
68+
addResponderEventSystemEvent,
69+
removeActiveResponderEventSystemEvent,
70+
} from '../events/ReactDOMEventListener.js';
6971
import {mediaEventTypes} from '../events/DOMTopLevelEventTypes';
7072
import {
7173
createDangerousStringForStyles,
@@ -1307,12 +1309,12 @@ export function restoreControlledState(
13071309

13081310
export function listenToEventResponderEventTypes(
13091311
eventTypes: Array<string>,
1310-
element: Element | Document,
1312+
document: Document,
13111313
): void {
13121314
if (enableFlareAPI) {
1313-
// Get the listening Set for this element. We use this to track
1315+
// Get the listening Map for this element. We use this to track
13141316
// what events we're listening to.
1315-
const listeningSet = getListeningSetForElement(element);
1317+
const listenerMap = getListenerMapForElement(document);
13161318

13171319
// Go through each target event type of the event responder
13181320
for (let i = 0, length = eventTypes.length; i < length; ++i) {
@@ -1322,13 +1324,35 @@ export function listenToEventResponderEventTypes(
13221324
const targetEventType = isPassive
13231325
? eventType
13241326
: eventType.substring(0, eventType.length - 7);
1325-
if (!listeningSet.has(eventKey)) {
1326-
trapEventForResponderEventSystem(
1327-
element,
1328-
((targetEventType: any): DOMTopLevelEventType),
1327+
if (!listenerMap.has(eventKey)) {
1328+
if (isPassive) {
1329+
const activeKey = targetEventType + '_active';
1330+
// If we have an active event listener, do not register
1331+
// a passive event listener. We use the same active event
1332+
// listener.
1333+
if (listenerMap.has(activeKey)) {
1334+
continue;
1335+
}
1336+
} else {
1337+
// If we have a passive event listener, remove the
1338+
// existing passive event listener before we add the
1339+
// active event listener.
1340+
const passiveKey = targetEventType + '_passive';
1341+
const passiveListener = listenerMap.get(passiveKey);
1342+
if (passiveListener != null) {
1343+
removeActiveResponderEventSystemEvent(
1344+
document,
1345+
targetEventType,
1346+
passiveListener,
1347+
);
1348+
}
1349+
}
1350+
const eventListener = addResponderEventSystemEvent(
1351+
document,
1352+
targetEventType,
13291353
isPassive,
13301354
);
1331-
listeningSet.add(eventKey);
1355+
listenerMap.set(eventKey, eventListener);
13321356
}
13331357
}
13341358
}

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

+28-11
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,6 @@ function createDOMResponderEvent(
290290
nativeEvent: AnyNativeEvent,
291291
nativeEventTarget: Element | Document,
292292
passive: boolean,
293-
passiveSupported: boolean,
294293
): ReactDOMResponderEvent {
295294
const {buttons, pointerType} = (nativeEvent: any);
296295
let eventPointerType = '';
@@ -308,7 +307,6 @@ function createDOMResponderEvent(
308307
return {
309308
nativeEvent: nativeEvent,
310309
passive,
311-
passiveSupported,
312310
pointerType: eventPointerType,
313311
target: nativeEventTarget,
314312
type: topLevelType,
@@ -318,9 +316,11 @@ function createDOMResponderEvent(
318316
function responderEventTypesContainType(
319317
eventTypes: Array<string>,
320318
type: string,
319+
isPassive: boolean,
321320
): boolean {
322321
for (let i = 0, len = eventTypes.length; i < len; i++) {
323-
if (eventTypes[i] === type) {
322+
const eventType = eventTypes[i];
323+
if (eventType === type || (!isPassive && eventType === type + '_active')) {
324324
return true;
325325
}
326326
}
@@ -330,11 +330,16 @@ function responderEventTypesContainType(
330330
function validateResponderTargetEventTypes(
331331
eventType: string,
332332
responder: ReactDOMEventResponder,
333+
isPassive: boolean,
333334
): boolean {
334335
const {targetEventTypes} = responder;
335336
// Validate the target event type exists on the responder
336337
if (targetEventTypes !== null) {
337-
return responderEventTypesContainType(targetEventTypes, eventType);
338+
return responderEventTypesContainType(
339+
targetEventTypes,
340+
eventType,
341+
isPassive,
342+
);
338343
}
339344
return false;
340345
}
@@ -349,7 +354,6 @@ function traverseAndHandleEventResponderInstances(
349354
const isPassiveEvent = (eventSystemFlags & IS_PASSIVE) !== 0;
350355
const isPassiveSupported = (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0;
351356
const isPassive = isPassiveEvent || !isPassiveSupported;
352-
const eventType = isPassive ? topLevelType : topLevelType + '_active';
353357

354358
// Trigger event responders in this order:
355359
// - Bubble target responder phase
@@ -361,7 +365,6 @@ function traverseAndHandleEventResponderInstances(
361365
nativeEvent,
362366
nativeEventTarget,
363367
isPassiveEvent,
364-
isPassiveSupported,
365368
);
366369
let node = targetFiber;
367370
let insidePortal = false;
@@ -381,7 +384,11 @@ function traverseAndHandleEventResponderInstances(
381384
const {props, responder, state} = responderInstance;
382385
if (
383386
!visitedResponders.has(responder) &&
384-
validateResponderTargetEventTypes(eventType, responder) &&
387+
validateResponderTargetEventTypes(
388+
topLevelType,
389+
responder,
390+
isPassive,
391+
) &&
385392
(!insidePortal || responder.targetPortalPropagation)
386393
) {
387394
visitedResponders.add(responder);
@@ -401,10 +408,20 @@ function traverseAndHandleEventResponderInstances(
401408
node = node.return;
402409
}
403410
// Root phase
404-
const rootEventResponderInstances = rootEventTypesToEventResponderInstances.get(
405-
eventType,
406-
);
407-
if (rootEventResponderInstances !== undefined) {
411+
const passive = rootEventTypesToEventResponderInstances.get(topLevelType);
412+
const rootEventResponderInstances = [];
413+
if (passive !== undefined) {
414+
rootEventResponderInstances.push(...Array.from(passive));
415+
}
416+
if (!isPassive) {
417+
const active = rootEventTypesToEventResponderInstances.get(
418+
topLevelType + '_active',
419+
);
420+
if (active !== undefined) {
421+
rootEventResponderInstances.push(...Array.from(active));
422+
}
423+
}
424+
if (rootEventResponderInstances.length > 0) {
408425
const responderInstances = Array.from(rootEventResponderInstances);
409426

410427
for (let i = 0; i < responderInstances.length; i++) {

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

+17-17
Original file line numberDiff line numberDiff line change
@@ -86,22 +86,22 @@ import isEventSupported from './isEventSupported';
8686
*/
8787

8888
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
89-
const elementListeningSets:
89+
const elementListenerMap:
9090
| WeakMap
9191
| Map<
9292
Document | Element | Node,
93-
Set<DOMTopLevelEventType | string>,
93+
Map<DOMTopLevelEventType | string, null | (any => void)>,
9494
> = new PossiblyWeakMap();
9595

96-
export function getListeningSetForElement(
96+
export function getListenerMapForElement(
9797
element: Document | Element | Node,
98-
): Set<DOMTopLevelEventType | string> {
99-
let listeningSet = elementListeningSets.get(element);
100-
if (listeningSet === undefined) {
101-
listeningSet = new Set();
102-
elementListeningSets.set(element, listeningSet);
98+
): Map<DOMTopLevelEventType | string, null | (any => void)> {
99+
let listenerMap = elementListenerMap.get(element);
100+
if (listenerMap === undefined) {
101+
listenerMap = new Map();
102+
elementListenerMap.set(element, listenerMap);
103103
}
104-
return listeningSet;
104+
return listenerMap;
105105
}
106106

107107
/**
@@ -129,7 +129,7 @@ export function listenTo(
129129
registrationName: string,
130130
mountAt: Document | Element | Node,
131131
): void {
132-
const listeningSet = getListeningSetForElement(mountAt);
132+
const listeningSet = getListenerMapForElement(mountAt);
133133
const dependencies = registrationNameDependencies[registrationName];
134134

135135
for (let i = 0; i < dependencies.length; i++) {
@@ -141,9 +141,9 @@ export function listenTo(
141141
export function listenToTopLevel(
142142
topLevelType: DOMTopLevelEventType,
143143
mountAt: Document | Element | Node,
144-
listeningSet: Set<DOMTopLevelEventType | string>,
144+
listenerMap: Map<DOMTopLevelEventType | string, null | (any => void)>,
145145
): void {
146-
if (!listeningSet.has(topLevelType)) {
146+
if (!listenerMap.has(topLevelType)) {
147147
switch (topLevelType) {
148148
case TOP_SCROLL:
149149
trapCapturedEvent(TOP_SCROLL, mountAt);
@@ -154,8 +154,8 @@ export function listenToTopLevel(
154154
trapCapturedEvent(TOP_BLUR, mountAt);
155155
// We set the flag for a single dependency later in this function,
156156
// but this ensures we mark both as attached rather than just one.
157-
listeningSet.add(TOP_BLUR);
158-
listeningSet.add(TOP_FOCUS);
157+
listenerMap.set(TOP_BLUR, null);
158+
listenerMap.set(TOP_FOCUS, null);
159159
break;
160160
case TOP_CANCEL:
161161
case TOP_CLOSE:
@@ -178,20 +178,20 @@ export function listenToTopLevel(
178178
}
179179
break;
180180
}
181-
listeningSet.add(topLevelType);
181+
listenerMap.set(topLevelType, null);
182182
}
183183
}
184184

185185
export function isListeningToAllDependencies(
186186
registrationName: string,
187187
mountAt: Document | Element,
188188
): boolean {
189-
const listeningSet = getListeningSetForElement(mountAt);
189+
const listenerMap = getListenerMapForElement(mountAt);
190190
const dependencies = registrationNameDependencies[registrationName];
191191

192192
for (let i = 0; i < dependencies.length; i++) {
193193
const dependency = dependencies[i];
194-
if (!listeningSet.has(dependency)) {
194+
if (!listenerMap.has(dependency)) {
195195
return false;
196196
}
197197
}

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

+49-32
Original file line numberDiff line numberDiff line change
@@ -211,42 +211,59 @@ export function trapCapturedEvent(
211211
trapEventForPluginEventSystem(element, topLevelType, true);
212212
}
213213

214-
export function trapEventForResponderEventSystem(
215-
element: Document | Element | Node,
216-
topLevelType: DOMTopLevelEventType,
214+
export function addResponderEventSystemEvent(
215+
document: Document,
216+
topLevelType: string,
217217
passive: boolean,
218-
): void {
219-
if (enableFlareAPI) {
220-
const rawEventName = getRawEventName(topLevelType);
221-
let eventFlags = RESPONDER_EVENT_SYSTEM;
222-
223-
// If passive option is not supported, then the event will be
224-
// active and not passive, but we flag it as using not being
225-
// supported too. This way the responder event plugins know,
226-
// and can provide polyfills if needed.
227-
if (passive) {
228-
if (passiveBrowserEventsSupported) {
229-
eventFlags |= IS_PASSIVE;
230-
} else {
231-
eventFlags |= IS_ACTIVE;
232-
eventFlags |= PASSIVE_NOT_SUPPORTED;
233-
passive = false;
234-
}
235-
} else {
236-
eventFlags |= IS_ACTIVE;
237-
}
238-
// Check if interactive and wrap in discreteUpdates
239-
const listener = dispatchEvent.bind(null, topLevelType, eventFlags);
218+
): any => void {
219+
let eventFlags = RESPONDER_EVENT_SYSTEM;
220+
221+
// If passive option is not supported, then the event will be
222+
// active and not passive, but we flag it as using not being
223+
// supported too. This way the responder event plugins know,
224+
// and can provide polyfills if needed.
225+
if (passive) {
240226
if (passiveBrowserEventsSupported) {
241-
addEventCaptureListenerWithPassiveFlag(
242-
element,
243-
rawEventName,
244-
listener,
245-
passive,
246-
);
227+
eventFlags |= IS_PASSIVE;
247228
} else {
248-
addEventCaptureListener(element, rawEventName, listener);
229+
eventFlags |= IS_ACTIVE;
230+
eventFlags |= PASSIVE_NOT_SUPPORTED;
231+
passive = false;
249232
}
233+
} else {
234+
eventFlags |= IS_ACTIVE;
235+
}
236+
// Check if interactive and wrap in discreteUpdates
237+
const listener = dispatchEvent.bind(
238+
null,
239+
((topLevelType: any): DOMTopLevelEventType),
240+
eventFlags,
241+
);
242+
if (passiveBrowserEventsSupported) {
243+
addEventCaptureListenerWithPassiveFlag(
244+
document,
245+
topLevelType,
246+
listener,
247+
passive,
248+
);
249+
} else {
250+
addEventCaptureListener(document, topLevelType, listener);
251+
}
252+
return listener;
253+
}
254+
255+
export function removeActiveResponderEventSystemEvent(
256+
document: Document,
257+
topLevelType: string,
258+
listener: any => void,
259+
) {
260+
if (passiveBrowserEventsSupported) {
261+
document.removeEventListener(topLevelType, listener, {
262+
capture: true,
263+
passive: false,
264+
});
265+
} else {
266+
document.removeEventListener(topLevelType, listener, true);
250267
}
251268
}
252269

0 commit comments

Comments
 (0)