Skip to content

Commit

Permalink
[react-interactions] Upgrade passive event listeners to active listen…
Browse files Browse the repository at this point in the history
…ers (#17513)
  • Loading branch information
trueadm committed Dec 4, 2019
1 parent 5235d19 commit acfe4b2
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 114 deletions.
46 changes: 35 additions & 11 deletions packages/react-dom/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import warning from 'shared/warning';
import {canUseDOM} from 'shared/ExecutionEnvironment';
import warningWithoutStack from 'shared/warningWithoutStack';
import endsWith from 'shared/endsWith';
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
import {setListenToResponderEventTypes} from '../events/DOMEventResponderSystem';

import {
Expand Down Expand Up @@ -63,9 +62,12 @@ import {
import {
listenTo,
trapBubbledEvent,
getListeningSetForElement,
getListenerMapForElement,
} from '../events/ReactBrowserEventEmitter';
import {trapEventForResponderEventSystem} from '../events/ReactDOMEventListener.js';
import {
addResponderEventSystemEvent,
removeActiveResponderEventSystemEvent,
} from '../events/ReactDOMEventListener.js';
import {mediaEventTypes} from '../events/DOMTopLevelEventTypes';
import {
createDangerousStringForStyles,
Expand Down Expand Up @@ -1307,12 +1309,12 @@ export function restoreControlledState(

export function listenToEventResponderEventTypes(
eventTypes: Array<string>,
element: Element | Document,
document: Document,
): void {
if (enableFlareAPI) {
// Get the listening Set for this element. We use this to track
// Get the listening Map for this element. We use this to track
// what events we're listening to.
const listeningSet = getListeningSetForElement(element);
const listenerMap = getListenerMapForElement(document);

// Go through each target event type of the event responder
for (let i = 0, length = eventTypes.length; i < length; ++i) {
Expand All @@ -1322,13 +1324,35 @@ export function listenToEventResponderEventTypes(
const targetEventType = isPassive
? eventType
: eventType.substring(0, eventType.length - 7);
if (!listeningSet.has(eventKey)) {
trapEventForResponderEventSystem(
element,
((targetEventType: any): DOMTopLevelEventType),
if (!listenerMap.has(eventKey)) {
if (isPassive) {
const activeKey = targetEventType + '_active';
// If we have an active event listener, do not register
// a passive event listener. We use the same active event
// listener.
if (listenerMap.has(activeKey)) {
continue;
}
} else {
// If we have a passive event listener, remove the
// existing passive event listener before we add the
// active event listener.
const passiveKey = targetEventType + '_passive';
const passiveListener = listenerMap.get(passiveKey);
if (passiveListener != null) {
removeActiveResponderEventSystemEvent(
document,
targetEventType,
passiveListener,
);
}
}
const eventListener = addResponderEventSystemEvent(
document,
targetEventType,
isPassive,
);
listeningSet.add(eventKey);
listenerMap.set(eventKey, eventListener);
}
}
}
Expand Down
39 changes: 28 additions & 11 deletions packages/react-dom/src/events/DOMEventResponderSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,6 @@ function createDOMResponderEvent(
nativeEvent: AnyNativeEvent,
nativeEventTarget: Element | Document,
passive: boolean,
passiveSupported: boolean,
): ReactDOMResponderEvent {
const {buttons, pointerType} = (nativeEvent: any);
let eventPointerType = '';
Expand All @@ -308,7 +307,6 @@ function createDOMResponderEvent(
return {
nativeEvent: nativeEvent,
passive,
passiveSupported,
pointerType: eventPointerType,
target: nativeEventTarget,
type: topLevelType,
Expand All @@ -318,9 +316,11 @@ function createDOMResponderEvent(
function responderEventTypesContainType(
eventTypes: Array<string>,
type: string,
isPassive: boolean,
): boolean {
for (let i = 0, len = eventTypes.length; i < len; i++) {
if (eventTypes[i] === type) {
const eventType = eventTypes[i];
if (eventType === type || (!isPassive && eventType === type + '_active')) {
return true;
}
}
Expand All @@ -330,11 +330,16 @@ function responderEventTypesContainType(
function validateResponderTargetEventTypes(
eventType: string,
responder: ReactDOMEventResponder,
isPassive: boolean,
): boolean {
const {targetEventTypes} = responder;
// Validate the target event type exists on the responder
if (targetEventTypes !== null) {
return responderEventTypesContainType(targetEventTypes, eventType);
return responderEventTypesContainType(
targetEventTypes,
eventType,
isPassive,
);
}
return false;
}
Expand All @@ -349,7 +354,6 @@ function traverseAndHandleEventResponderInstances(
const isPassiveEvent = (eventSystemFlags & IS_PASSIVE) !== 0;
const isPassiveSupported = (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0;
const isPassive = isPassiveEvent || !isPassiveSupported;
const eventType = isPassive ? topLevelType : topLevelType + '_active';

// Trigger event responders in this order:
// - Bubble target responder phase
Expand All @@ -361,7 +365,6 @@ function traverseAndHandleEventResponderInstances(
nativeEvent,
nativeEventTarget,
isPassiveEvent,
isPassiveSupported,
);
let node = targetFiber;
let insidePortal = false;
Expand All @@ -381,7 +384,11 @@ function traverseAndHandleEventResponderInstances(
const {props, responder, state} = responderInstance;
if (
!visitedResponders.has(responder) &&
validateResponderTargetEventTypes(eventType, responder) &&
validateResponderTargetEventTypes(
topLevelType,
responder,
isPassive,
) &&
(!insidePortal || responder.targetPortalPropagation)
) {
visitedResponders.add(responder);
Expand All @@ -401,10 +408,20 @@ function traverseAndHandleEventResponderInstances(
node = node.return;
}
// Root phase
const rootEventResponderInstances = rootEventTypesToEventResponderInstances.get(
eventType,
);
if (rootEventResponderInstances !== undefined) {
const passive = rootEventTypesToEventResponderInstances.get(topLevelType);
const rootEventResponderInstances = [];
if (passive !== undefined) {
rootEventResponderInstances.push(...Array.from(passive));
}
if (!isPassive) {
const active = rootEventTypesToEventResponderInstances.get(
topLevelType + '_active',
);
if (active !== undefined) {
rootEventResponderInstances.push(...Array.from(active));
}
}
if (rootEventResponderInstances.length > 0) {
const responderInstances = Array.from(rootEventResponderInstances);

for (let i = 0; i < responderInstances.length; i++) {
Expand Down
34 changes: 17 additions & 17 deletions packages/react-dom/src/events/ReactBrowserEventEmitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,22 +86,22 @@ import isEventSupported from './isEventSupported';
*/

const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
const elementListeningSets:
const elementListenerMap:
| WeakMap
| Map<
Document | Element | Node,
Set<DOMTopLevelEventType | string>,
Map<DOMTopLevelEventType | string, null | (any => void)>,
> = new PossiblyWeakMap();

export function getListeningSetForElement(
export function getListenerMapForElement(
element: Document | Element | Node,
): Set<DOMTopLevelEventType | string> {
let listeningSet = elementListeningSets.get(element);
if (listeningSet === undefined) {
listeningSet = new Set();
elementListeningSets.set(element, listeningSet);
): Map<DOMTopLevelEventType | string, null | (any => void)> {
let listenerMap = elementListenerMap.get(element);
if (listenerMap === undefined) {
listenerMap = new Map();
elementListenerMap.set(element, listenerMap);
}
return listeningSet;
return listenerMap;
}

/**
Expand Down Expand Up @@ -129,7 +129,7 @@ export function listenTo(
registrationName: string,
mountAt: Document | Element | Node,
): void {
const listeningSet = getListeningSetForElement(mountAt);
const listeningSet = getListenerMapForElement(mountAt);
const dependencies = registrationNameDependencies[registrationName];

for (let i = 0; i < dependencies.length; i++) {
Expand All @@ -141,9 +141,9 @@ export function listenTo(
export function listenToTopLevel(
topLevelType: DOMTopLevelEventType,
mountAt: Document | Element | Node,
listeningSet: Set<DOMTopLevelEventType | string>,
listenerMap: Map<DOMTopLevelEventType | string, null | (any => void)>,
): void {
if (!listeningSet.has(topLevelType)) {
if (!listenerMap.has(topLevelType)) {
switch (topLevelType) {
case TOP_SCROLL:
trapCapturedEvent(TOP_SCROLL, mountAt);
Expand All @@ -154,8 +154,8 @@ export function listenToTopLevel(
trapCapturedEvent(TOP_BLUR, mountAt);
// We set the flag for a single dependency later in this function,
// but this ensures we mark both as attached rather than just one.
listeningSet.add(TOP_BLUR);
listeningSet.add(TOP_FOCUS);
listenerMap.set(TOP_BLUR, null);
listenerMap.set(TOP_FOCUS, null);
break;
case TOP_CANCEL:
case TOP_CLOSE:
Expand All @@ -178,20 +178,20 @@ export function listenToTopLevel(
}
break;
}
listeningSet.add(topLevelType);
listenerMap.set(topLevelType, null);
}
}

export function isListeningToAllDependencies(
registrationName: string,
mountAt: Document | Element,
): boolean {
const listeningSet = getListeningSetForElement(mountAt);
const listenerMap = getListenerMapForElement(mountAt);
const dependencies = registrationNameDependencies[registrationName];

for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
if (!listeningSet.has(dependency)) {
if (!listenerMap.has(dependency)) {
return false;
}
}
Expand Down
81 changes: 49 additions & 32 deletions packages/react-dom/src/events/ReactDOMEventListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,42 +211,59 @@ export function trapCapturedEvent(
trapEventForPluginEventSystem(element, topLevelType, true);
}

export function trapEventForResponderEventSystem(
element: Document | Element | Node,
topLevelType: DOMTopLevelEventType,
export function addResponderEventSystemEvent(
document: Document,
topLevelType: string,
passive: boolean,
): void {
if (enableFlareAPI) {
const rawEventName = getRawEventName(topLevelType);
let eventFlags = RESPONDER_EVENT_SYSTEM;

// If passive option is not supported, then the event will be
// active and not passive, but we flag it as using not being
// supported too. This way the responder event plugins know,
// and can provide polyfills if needed.
if (passive) {
if (passiveBrowserEventsSupported) {
eventFlags |= IS_PASSIVE;
} else {
eventFlags |= IS_ACTIVE;
eventFlags |= PASSIVE_NOT_SUPPORTED;
passive = false;
}
} else {
eventFlags |= IS_ACTIVE;
}
// Check if interactive and wrap in discreteUpdates
const listener = dispatchEvent.bind(null, topLevelType, eventFlags);
): any => void {
let eventFlags = RESPONDER_EVENT_SYSTEM;

// If passive option is not supported, then the event will be
// active and not passive, but we flag it as using not being
// supported too. This way the responder event plugins know,
// and can provide polyfills if needed.
if (passive) {
if (passiveBrowserEventsSupported) {
addEventCaptureListenerWithPassiveFlag(
element,
rawEventName,
listener,
passive,
);
eventFlags |= IS_PASSIVE;
} else {
addEventCaptureListener(element, rawEventName, listener);
eventFlags |= IS_ACTIVE;
eventFlags |= PASSIVE_NOT_SUPPORTED;
passive = false;
}
} else {
eventFlags |= IS_ACTIVE;
}
// Check if interactive and wrap in discreteUpdates
const listener = dispatchEvent.bind(
null,
((topLevelType: any): DOMTopLevelEventType),
eventFlags,
);
if (passiveBrowserEventsSupported) {
addEventCaptureListenerWithPassiveFlag(
document,
topLevelType,
listener,
passive,
);
} else {
addEventCaptureListener(document, topLevelType, listener);
}
return listener;
}

export function removeActiveResponderEventSystemEvent(
document: Document,
topLevelType: string,
listener: any => void,
) {
if (passiveBrowserEventsSupported) {
document.removeEventListener(topLevelType, listener, {
capture: true,
passive: false,
});
} else {
document.removeEventListener(topLevelType, listener, true);
}
}

Expand Down
Loading

0 comments on commit acfe4b2

Please sign in to comment.