@samitouri / QOS-React-2 / commits / acfe4b21b2

[react-interactions] Upgrade passive event listeners to active listeners (#17513)

Dominic Gannaway committed Dec 4, 2019 at 19:30 UTC acfe4b21b2621da3618f9096c8be06b32d6247ec
8 files changed +145 -114
packages/react-dom/src/client/ReactDOMComponent.js
+35 -11
@@ -14,7 +14,6 @@ import warning from 'shared/warning';
14 import {canUseDOM} from 'shared/ExecutionEnvironment';
15 import warningWithoutStack from 'shared/warningWithoutStack';
16 import endsWith from 'shared/endsWith';
17 -import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
17 import {setListenToResponderEventTypes} from '../events/DOMEventResponderSystem';
18
19 import {
@@ -63,9 +62,12 @@ import {
62 import {
63 listenTo,
64 trapBubbledEvent,
66 - getListeningSetForElement,
65 + getListenerMapForElement,
66 } from '../events/ReactBrowserEventEmitter';
68 -import {trapEventForResponderEventSystem} from '../events/ReactDOMEventListener.js';
67 +import {
68 + addResponderEventSystemEvent,
69 + removeActiveResponderEventSystemEvent,
70 +} from '../events/ReactDOMEventListener.js';
71 import {mediaEventTypes} from '../events/DOMTopLevelEventTypes';
72 import {
73 createDangerousStringForStyles,
@@ -1307,12 +1309,12 @@ export function restoreControlledState(
1309
1310 export function listenToEventResponderEventTypes(
1311 eventTypes: Array<string>,
1310 - element: Element | Document,
1312 + document: Document,
1313 ): void {
1314 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
1316 // what events we're listening to.
1315 - const listeningSet = getListeningSetForElement(element);
1317 + const listenerMap = getListenerMapForElement(document);
1318
1319 // Go through each target event type of the event responder
1320 for (let i = 0, length = eventTypes.length; i < length; ++i) {
@@ -1322,13 +1324,35 @@ export function listenToEventResponderEventTypes(
1324 const targetEventType = isPassive
1325 ? eventType
1326 : 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,
1353 isPassive,
1354 );
1331 - listeningSet.add(eventKey);
1355 + listenerMap.set(eventKey, eventListener);
1356 }
1357 }
1358 }
packages/react-dom/src/events/DOMEventResponderSystem.js
+28 -11
@@ -290,7 +290,6 @@ function createDOMResponderEvent(
290 nativeEvent: AnyNativeEvent,
291 nativeEventTarget: Element | Document,
292 passive: boolean,
293 - passiveSupported: boolean,
293 ): ReactDOMResponderEvent {
294 const {buttons, pointerType} = (nativeEvent: any);
295 let eventPointerType = '';
@@ -308,7 +307,6 @@ function createDOMResponderEvent(
307 return {
308 nativeEvent: nativeEvent,
309 passive,
311 - passiveSupported,
310 pointerType: eventPointerType,
311 target: nativeEventTarget,
312 type: topLevelType,
@@ -318,9 +316,11 @@ function createDOMResponderEvent(
316 function responderEventTypesContainType(
317 eventTypes: Array<string>,
318 type: string,
319 + isPassive: boolean,
320 ): boolean {
321 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')) {
324 return true;
325 }
326 }
@@ -330,11 +330,16 @@ function responderEventTypesContainType(
330 function validateResponderTargetEventTypes(
331 eventType: string,
332 responder: ReactDOMEventResponder,
333 + isPassive: boolean,
334 ): boolean {
335 const {targetEventTypes} = responder;
336 // Validate the target event type exists on the responder
337 if (targetEventTypes !== null) {
337 - return responderEventTypesContainType(targetEventTypes, eventType);
338 + return responderEventTypesContainType(
339 + targetEventTypes,
340 + eventType,
341 + isPassive,
342 + );
343 }
344 return false;
345 }
@@ -349,7 +354,6 @@ function traverseAndHandleEventResponderInstances(
354 const isPassiveEvent = (eventSystemFlags & IS_PASSIVE) !== 0;
355 const isPassiveSupported = (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0;
356 const isPassive = isPassiveEvent || !isPassiveSupported;
352 - const eventType = isPassive ? topLevelType : topLevelType + '_active';
357
358 // Trigger event responders in this order:
359 // - Bubble target responder phase
@@ -361,7 +365,6 @@ function traverseAndHandleEventResponderInstances(
365 nativeEvent,
366 nativeEventTarget,
367 isPassiveEvent,
364 - isPassiveSupported,
368 );
369 let node = targetFiber;
370 let insidePortal = false;
@@ -381,7 +384,11 @@ function traverseAndHandleEventResponderInstances(
384 const {props, responder, state} = responderInstance;
385 if (
386 !visitedResponders.has(responder) &&
384 - validateResponderTargetEventTypes(eventType, responder) &&
387 + validateResponderTargetEventTypes(
388 + topLevelType,
389 + responder,
390 + isPassive,
391 + ) &&
392 (!insidePortal || responder.targetPortalPropagation)
393 ) {
394 visitedResponders.add(responder);
@@ -401,10 +408,20 @@ function traverseAndHandleEventResponderInstances(
408 node = node.return;
409 }
410 // 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) {
425 const responderInstances = Array.from(rootEventResponderInstances);
426
427 for (let i = 0; i < responderInstances.length; i++) {
packages/react-dom/src/events/ReactBrowserEventEmitter.js
+17 -17
@@ -86,22 +86,22 @@ import isEventSupported from './isEventSupported';
86 */
87
88 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
89 -const elementListeningSets:
89 +const elementListenerMap:
90 | WeakMap
91 | Map<
92 Document | Element | Node,
93 - Set<DOMTopLevelEventType | string>,
93 + Map<DOMTopLevelEventType | string, null | (any => void)>,
94 > = new PossiblyWeakMap();
95
96 -export function getListeningSetForElement(
96 +export function getListenerMapForElement(
97 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);
103 }
104 - return listeningSet;
104 + return listenerMap;
105 }
106
107 /**
@@ -129,7 +129,7 @@ export function listenTo(
129 registrationName: string,
130 mountAt: Document | Element | Node,
131 ): void {
132 - const listeningSet = getListeningSetForElement(mountAt);
132 + const listeningSet = getListenerMapForElement(mountAt);
133 const dependencies = registrationNameDependencies[registrationName];
134
135 for (let i = 0; i < dependencies.length; i++) {
@@ -141,9 +141,9 @@ export function listenTo(
141 export function listenToTopLevel(
142 topLevelType: DOMTopLevelEventType,
143 mountAt: Document | Element | Node,
144 - listeningSet: Set<DOMTopLevelEventType | string>,
144 + listenerMap: Map<DOMTopLevelEventType | string, null | (any => void)>,
145 ): void {
146 - if (!listeningSet.has(topLevelType)) {
146 + if (!listenerMap.has(topLevelType)) {
147 switch (topLevelType) {
148 case TOP_SCROLL:
149 trapCapturedEvent(TOP_SCROLL, mountAt);
@@ -154,8 +154,8 @@ export function listenToTopLevel(
154 trapCapturedEvent(TOP_BLUR, mountAt);
155 // We set the flag for a single dependency later in this function,
156 // 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);
159 break;
160 case TOP_CANCEL:
161 case TOP_CLOSE:
@@ -178,7 +178,7 @@ export function listenToTopLevel(
178 }
179 break;
180 }
181 - listeningSet.add(topLevelType);
181 + listenerMap.set(topLevelType, null);
182 }
183 }
184
@@ -186,12 +186,12 @@ export function isListeningToAllDependencies(
186 registrationName: string,
187 mountAt: Document | Element,
188 ): boolean {
189 - const listeningSet = getListeningSetForElement(mountAt);
189 + const listenerMap = getListenerMapForElement(mountAt);
190 const dependencies = registrationNameDependencies[registrationName];
191
192 for (let i = 0; i < dependencies.length; i++) {
193 const dependency = dependencies[i];
194 - if (!listeningSet.has(dependency)) {
194 + if (!listenerMap.has(dependency)) {
195 return false;
196 }
197 }
packages/react-dom/src/events/ReactDOMEventListener.js
+49 -32
@@ -211,42 +211,59 @@ export function trapCapturedEvent(
211 trapEventForPluginEventSystem(element, topLevelType, true);
212 }
213
214 -export function trapEventForResponderEventSystem(
215 - element: Document | Element | Node,
216 - topLevelType: DOMTopLevelEventType,
214 +export function addResponderEventSystemEvent(
215 + document: Document,
216 + topLevelType: string,
217 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) {
226 if (passiveBrowserEventsSupported) {
241 - addEventCaptureListenerWithPassiveFlag(
242 - element,
243 - rawEventName,
244 - listener,
245 - passive,
246 - );
227 + eventFlags |= IS_PASSIVE;
228 } else {
248 - addEventCaptureListener(element, rawEventName, listener);
229 + eventFlags |= IS_ACTIVE;
230 + eventFlags |= PASSIVE_NOT_SUPPORTED;
231 + passive = false;
232 }
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);
267 }
268 }
269
packages/react-dom/src/events/ReactDOMEventReplaying.js
+16 -24
@@ -30,10 +30,10 @@ import {
30 } from 'react-reconciler/reflection';
31 import {
32 attemptToDispatchEvent,
33 - trapEventForResponderEventSystem,
33 + addResponderEventSystemEvent,
34 } from './ReactDOMEventListener';
35 import {
36 - getListeningSetForElement,
36 + getListenerMapForElement,
37 listenToTopLevel,
38 } from './ReactBrowserEventEmitter';
39 import {
@@ -214,41 +214,33 @@ export function isReplayableDiscreteEvent(
214 function trapReplayableEvent(
215 topLevelType: DOMTopLevelEventType,
216 document: Document,
217 - listeningSet: Set<DOMTopLevelEventType | string>,
217 + listenerMap: Map<DOMTopLevelEventType | string, null | (any => void)>,
218 ) {
219 - listenToTopLevel(topLevelType, document, listeningSet);
219 + listenToTopLevel(topLevelType, document, listenerMap);
220 if (enableFlareAPI) {
221 // Trap events for the responder system.
222 - const passiveEventKey =
223 - unsafeCastDOMTopLevelTypeToString(topLevelType) + '_passive';
224 - if (!listeningSet.has(passiveEventKey)) {
225 - trapEventForResponderEventSystem(document, topLevelType, true);
226 - listeningSet.add(passiveEventKey);
227 - }
228 - // TODO: This listens to all events as active which might have
229 - // undesirable effects. It's also unnecessary to have both
230 - // passive and active listeners. Instead, we could start with
231 - // a passive and upgrade it to an active one if needed.
232 - // For replaying purposes the active is never needed since we
233 - // currently don't preventDefault.
234 - const activeEventKey =
235 - unsafeCastDOMTopLevelTypeToString(topLevelType) + '_active';
236 - if (!listeningSet.has(activeEventKey)) {
237 - trapEventForResponderEventSystem(document, topLevelType, false);
238 - listeningSet.add(activeEventKey);
222 + const topLevelTypeString = unsafeCastDOMTopLevelTypeToString(topLevelType);
223 + const passiveEventKey = topLevelTypeString + '_passive';
224 + if (!listenerMap.has(passiveEventKey)) {
225 + const listener = addResponderEventSystemEvent(
226 + document,
227 + topLevelTypeString,
228 + true,
229 + );
230 + listenerMap.set(passiveEventKey, listener);
231 }
232 }
233 }
234
235 export function eagerlyTrapReplayableEvents(document: Document) {
244 - const listeningSet = getListeningSetForElement(document);
236 + const listenerMap = getListenerMapForElement(document);
237 // Discrete
238 discreteReplayableEvents.forEach(topLevelType => {
247 - trapReplayableEvent(topLevelType, document, listeningSet);
239 + trapReplayableEvent(topLevelType, document, listenerMap);
240 });
241 // Continuous
242 continuousReplayableEvents.forEach(topLevelType => {
251 - trapReplayableEvent(topLevelType, document, listeningSet);
243 + trapReplayableEvent(topLevelType, document, listenerMap);
244 });
245 }
246
packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
-17
@@ -156,7 +156,6 @@ describe('DOMEventResponderSystem', () => {
156 eventLog.push({
157 name: event.type,
158 passive: event.passive,
159 - passiveSupported: event.passiveSupported,
159 phase: 'bubble',
160 });
161 },
@@ -185,7 +184,6 @@ describe('DOMEventResponderSystem', () => {
184 {
185 name: 'click',
186 passive: false,
188 - passiveSupported: false,
187 phase: 'bubble',
188 },
189 ]);
@@ -216,7 +214,6 @@ describe('DOMEventResponderSystem', () => {
214 eventLog.push({
215 name: event.type,
216 passive: event.passive,
219 - passiveSupported: event.passiveSupported,
217 phase: 'bubble',
218 });
219 },
@@ -242,7 +239,6 @@ describe('DOMEventResponderSystem', () => {
239 {
240 name: 'click',
241 passive: true,
245 - passiveSupported: true,
242 phase: 'bubble',
243 },
244 ]);
@@ -260,7 +256,6 @@ describe('DOMEventResponderSystem', () => {
256 eventLog.push({
257 name: event.type,
258 passive: event.passive,
263 - passiveSupported: event.passiveSupported,
259 phase: 'bubble',
260 });
261 },
@@ -296,7 +291,6 @@ describe('DOMEventResponderSystem', () => {
291 {
292 name: 'click',
293 passive: false,
299 - passiveSupported: false,
294 phase: 'bubble',
295 },
296 ]);
@@ -327,7 +321,6 @@ describe('DOMEventResponderSystem', () => {
321 {
322 name: 'click',
323 passive: false,
330 - passiveSupported: false,
324 phase: 'bubble',
325 },
326 ]);
@@ -634,7 +627,6 @@ describe('DOMEventResponderSystem', () => {
627 eventLog.push({
628 name: event.type,
629 passive: event.passive,
637 - passiveSupported: event.passiveSupported,
630 phase: 'root',
631 });
632 },
@@ -656,7 +648,6 @@ describe('DOMEventResponderSystem', () => {
648 {
649 name: 'click',
650 passive: false,
659 - passiveSupported: false,
651 phase: 'root',
652 },
653 ]);
@@ -675,7 +666,6 @@ describe('DOMEventResponderSystem', () => {
666 eventLog.push({
667 name: event.type,
668 passive: event.passive,
678 - passiveSupported: event.passiveSupported,
669 });
670 },
671 });
@@ -687,7 +677,6 @@ describe('DOMEventResponderSystem', () => {
677 eventLog.push({
678 name: event.type,
679 passive: event.passive,
690 - passiveSupported: event.passiveSupported,
680 });
681 },
682 });
@@ -717,12 +706,10 @@ describe('DOMEventResponderSystem', () => {
706 {
707 name: 'click',
708 passive: false,
720 - passiveSupported: false,
709 },
710 {
711 name: 'click',
712 passive: false,
725 - passiveSupported: true,
713 },
714 ]);
715 });
@@ -739,7 +726,6 @@ describe('DOMEventResponderSystem', () => {
726 eventLog.push({
727 name: event.type,
728 passive: event.passive,
742 - passiveSupported: event.passiveSupported,
729 });
730 },
731 });
@@ -751,7 +737,6 @@ describe('DOMEventResponderSystem', () => {
737 eventLog.push({
738 name: event.type,
739 passive: event.passive,
754 - passiveSupported: event.passiveSupported,
740 });
741 },
742 });
@@ -778,12 +763,10 @@ describe('DOMEventResponderSystem', () => {
763 {
764 name: 'click',
765 passive: false,
781 - passiveSupported: false,
766 },
767 {
768 name: 'click',
769 passive: false,
786 - passiveSupported: true,
770 },
771 ]);
772
packages/react-interactions/events/README.md
-1
@@ -28,7 +28,6 @@ type ResponderEvent = {|
28 pointerType: string,
29 type: string,
30 passive: boolean,
31 - passiveSupported: boolean,
31 |};
32
33 type CustomEvent = {
packages/shared/ReactDOMTypes.js
-1
@@ -27,7 +27,6 @@ export type PointerType =
27 export type ReactDOMResponderEvent = {
28 nativeEvent: AnyNativeEvent,
29 passive: boolean,
30 - passiveSupported: boolean,
30 pointerType: PointerType,
31 target: Element | Document,
32 type: string,