Add infrastructure for passive/non-passive event support for future API exploration (#15036)

* Add infrastructure for passive/non-passive event support for future event API experimentation
This commit is contained in:
Dominic Gannaway
2019-03-15 09:39:43 +00:00
committed by GitHub
parent ab5fe174c6
commit 371bbf36bb
11 changed files with 240 additions and 101 deletions

View File

@@ -0,0 +1,16 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export type EventSystemFlags = number;
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;

View File

@@ -20,8 +20,8 @@ import {
let _batchedUpdatesImpl = function(fn, bookkeeping) {
return fn(bookkeeping);
};
let _interactiveUpdatesImpl = function(fn, a, b) {
return fn(a, b);
let _interactiveUpdatesImpl = function(fn, a, b, c) {
return fn(a, b, c);
};
let _flushInteractiveUpdatesImpl = function() {};
@@ -52,8 +52,8 @@ export function batchedUpdates(fn, bookkeeping) {
}
}
export function interactiveUpdates(fn, a, b) {
return _interactiveUpdatesImpl(fn, a, b);
export function interactiveUpdates(fn, a, b, c) {
return _interactiveUpdatesImpl(fn, a, b, c);
}
export function flushInteractiveUpdates() {

View File

@@ -253,7 +253,10 @@ if (__DEV__) {
};
}
function ensureListeningTo(rootContainerElement, registrationName) {
function ensureListeningTo(
rootContainerElement: Element | Node,
registrationName: string,
): void {
const isDocumentOrFragment =
rootContainerElement.nodeType === DOCUMENT_NODE ||
rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;

View File

@@ -8,7 +8,7 @@
*/
export function addEventBubbleListener(
element: Document | Element,
element: Document | Element | Node,
eventType: string,
listener: Function,
): void {
@@ -16,9 +16,18 @@ export function addEventBubbleListener(
}
export function addEventCaptureListener(
element: Document | Element,
element: Document | Element | Node,
eventType: string,
listener: Function,
): void {
element.addEventListener(eventType, listener, true);
}
export function addEventListener(
element: Document | Element | Node,
eventType: string,
listener: Function,
options: {passive: boolean},
): void {
element.addEventListener(eventType, listener, (options: any));
}

View File

@@ -8,6 +8,7 @@
*/
import {registrationNameDependencies} from 'events/EventPluginRegistry';
import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
import {
TOP_BLUR,
TOP_CANCEL,
@@ -84,22 +85,23 @@ import isEventSupported from './isEventSupported';
* React Core . General Purpose Event Plugin System
*/
const alreadyListeningTo = {};
let reactTopListenersCounter = 0;
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
const elementListeningSets:
| WeakMap
| Map<
Document | Element | Node,
Set<DOMTopLevelEventType>,
> = new PossiblyWeakMap();
/**
* To ensure no conflicts with other potential React instances on the page
*/
const topListenersIDKey = '_reactListenersID' + ('' + Math.random()).slice(2);
function getListeningForDocument(mountAt: any) {
// In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty`
// directly.
if (!Object.prototype.hasOwnProperty.call(mountAt, topListenersIDKey)) {
mountAt[topListenersIDKey] = reactTopListenersCounter++;
alreadyListeningTo[mountAt[topListenersIDKey]] = {};
function getListeningSetForElement(
element: Document | Element | Node,
): Set<DOMTopLevelEventType> {
let listeningSet = elementListeningSets.get(element);
if (listeningSet === undefined) {
listeningSet = new Set();
elementListeningSets.set(element, listeningSet);
}
return alreadyListeningTo[mountAt[topListenersIDKey]];
return listeningSet;
}
/**
@@ -125,14 +127,14 @@ function getListeningForDocument(mountAt: any) {
*/
export function listenTo(
registrationName: string,
mountAt: Document | Element,
) {
const isListening = getListeningForDocument(mountAt);
mountAt: Document | Element | Node,
): void {
const listeningSet = getListeningSetForElement(mountAt);
const dependencies = registrationNameDependencies[registrationName];
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
if (!listeningSet.has(dependency)) {
switch (dependency) {
case TOP_SCROLL:
trapCapturedEvent(TOP_SCROLL, mountAt);
@@ -143,8 +145,8 @@ export function listenTo(
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.
isListening[TOP_BLUR] = true;
isListening[TOP_FOCUS] = true;
listeningSet.add(TOP_BLUR);
listeningSet.add(TOP_FOCUS);
break;
case TOP_CANCEL:
case TOP_CLOSE:
@@ -167,7 +169,7 @@ export function listenTo(
}
break;
}
isListening[dependency] = true;
listeningSet.add(dependency);
}
}
}
@@ -175,12 +177,13 @@ export function listenTo(
export function isListeningToAllDependencies(
registrationName: string,
mountAt: Document | Element,
) {
const isListening = getListeningForDocument(mountAt);
): boolean {
const listeningSet = getListeningSetForElement(mountAt);
const dependencies = registrationNameDependencies[registrationName];
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
if (!listeningSet.has(dependency)) {
return false;
}
}

View File

@@ -15,18 +15,41 @@ import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching';
import {runExtractedEventsInBatch} from 'events/EventPluginHub';
import {isFiberMounted} from 'react-reconciler/reflection';
import {HostRoot} from 'shared/ReactWorkTags';
import {
type EventSystemFlags,
PLUGIN_EVENT_SYSTEM,
RESPONDER_EVENT_SYSTEM,
IS_PASSIVE,
IS_ACTIVE,
PASSIVE_NOT_SUPPORTED,
} from 'events/EventSystemFlags';
import {addEventBubbleListener, addEventCaptureListener} from './EventListener';
import {
addEventBubbleListener,
addEventCaptureListener,
addEventListener,
} from './EventListener';
import getEventTarget from './getEventTarget';
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
import SimpleEventPlugin from './SimpleEventPlugin';
import {getRawEventName} from './DOMTopLevelEventTypes';
import {passiveBrowserEventsSupported} from './checkPassiveEvents';
import {enableEventAPI} from 'shared/ReactFeatureFlags';
const {isInteractiveTopLevelEventType} = SimpleEventPlugin;
const CALLBACK_BOOKKEEPING_POOL_SIZE = 10;
const callbackBookkeepingPool = [];
type BookKeepingInstance = {
topLevelType: DOMTopLevelEventType | null,
nativeEvent: AnyNativeEvent | null,
targetInst: Fiber | null,
ancestors: Array<Fiber | null>,
eventSystemFlags: EventSystemFlags,
};
/**
* Find the deepest React component completely containing the root of the
* passed-in instance (for use when entire React trees are nested within each
@@ -48,20 +71,17 @@ function findRootContainerNode(inst) {
// Used to store ancestor hierarchy in top level callback
function getTopLevelCallbackBookKeeping(
topLevelType,
nativeEvent,
targetInst,
): {
topLevelType: ?DOMTopLevelEventType,
nativeEvent: ?AnyNativeEvent,
topLevelType: DOMTopLevelEventType,
nativeEvent: AnyNativeEvent,
targetInst: Fiber | null,
ancestors: Array<Fiber>,
} {
eventSystemFlags: EventSystemFlags,
): BookKeepingInstance {
if (callbackBookkeepingPool.length) {
const instance = callbackBookkeepingPool.pop();
instance.topLevelType = topLevelType;
instance.nativeEvent = nativeEvent;
instance.targetInst = targetInst;
instance.eventSystemFlags = eventSystemFlags;
return instance;
}
return {
@@ -69,20 +89,24 @@ function getTopLevelCallbackBookKeeping(
nativeEvent,
targetInst,
ancestors: [],
eventSystemFlags,
};
}
function releaseTopLevelCallbackBookKeeping(instance) {
function releaseTopLevelCallbackBookKeeping(
instance: BookKeepingInstance,
): void {
instance.topLevelType = null;
instance.nativeEvent = null;
instance.targetInst = null;
instance.ancestors.length = 0;
instance.eventSystemFlags = 0;
if (callbackBookkeepingPool.length < CALLBACK_BOOKKEEPING_POOL_SIZE) {
callbackBookkeepingPool.push(instance);
}
}
function handleTopLevel(bookKeeping) {
function handleTopLevel(bookKeeping: BookKeepingInstance) {
let targetInst = bookKeeping.targetInst;
// Loop through the hierarchy, in case there's any nested components.
@@ -92,7 +116,8 @@ function handleTopLevel(bookKeeping) {
let ancestor = targetInst;
do {
if (!ancestor) {
bookKeeping.ancestors.push(ancestor);
const ancestors = bookKeeping.ancestors;
((ancestors: any): Array<Fiber | null>).push(ancestor);
break;
}
const root = findRootContainerNode(ancestor);
@@ -105,12 +130,17 @@ function handleTopLevel(bookKeeping) {
for (let i = 0; i < bookKeeping.ancestors.length; i++) {
targetInst = bookKeeping.ancestors[i];
runExtractedEventsInBatch(
bookKeeping.topLevelType,
targetInst,
bookKeeping.nativeEvent,
getEventTarget(bookKeeping.nativeEvent),
);
if (bookKeeping.eventSystemFlags === PLUGIN_EVENT_SYSTEM) {
runExtractedEventsInBatch(
((bookKeeping.topLevelType: any): DOMTopLevelEventType),
targetInst,
((bookKeeping.nativeEvent: any): AnyNativeEvent),
getEventTarget(bookKeeping.nativeEvent),
);
} else {
// RESPONDER_EVENT_SYSTEM
// TODO: Add implementation
}
}
}
@@ -125,70 +155,89 @@ export function isEnabled() {
return _enabled;
}
/**
* Traps top-level events by using event bubbling.
*
* @param {number} topLevelType Number from `TopLevelEventTypes`.
* @param {object} element Element on which to attach listener.
* @return {?object} An object with a remove function which will forcefully
* remove the listener.
* @internal
*/
export function trapBubbledEvent(
topLevelType: DOMTopLevelEventType,
element: Document | Element,
) {
if (!element) {
return null;
}
const dispatch = isInteractiveTopLevelEventType(topLevelType)
? dispatchInteractiveEvent
: dispatchEvent;
addEventBubbleListener(
element,
getRawEventName(topLevelType),
// Check if interactive and wrap in interactiveUpdates
dispatch.bind(null, topLevelType),
);
element: Document | Element | Node,
): void {
trapEventForPluginEventSystem(element, topLevelType, false);
}
/**
* Traps a top-level event by using event capturing.
*
* @param {number} topLevelType Number from `TopLevelEventTypes`.
* @param {object} element Element on which to attach listener.
* @return {?object} An object with a remove function which will forcefully
* remove the listener.
* @internal
*/
export function trapCapturedEvent(
topLevelType: DOMTopLevelEventType,
element: Document | Element,
) {
if (!element) {
return null;
element: Document | Element | Node,
): void {
trapEventForPluginEventSystem(element, topLevelType, true);
}
export function trapEventForResponderEventSystem(
element: Document | Element | Node,
topLevelType: DOMTopLevelEventType,
capture: boolean,
passive: boolean,
): void {
if (enableEventAPI) {
const dispatch = isInteractiveTopLevelEventType(topLevelType)
? dispatchInteractiveEvent
: dispatchEvent;
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_ACTIVE;
eventFlags |= PASSIVE_NOT_SUPPORTED;
passive = false;
} else {
eventFlags |= IS_PASSIVE;
}
} else {
eventFlags |= IS_ACTIVE;
}
// Check if interactive and wrap in interactiveUpdates
const listener = dispatch.bind(null, topLevelType, eventFlags);
addEventListener(element, rawEventName, listener, {
capture,
passive,
});
}
}
function trapEventForPluginEventSystem(
element: Document | Element | Node,
topLevelType: DOMTopLevelEventType,
capture: boolean,
): void {
const dispatch = isInteractiveTopLevelEventType(topLevelType)
? dispatchInteractiveEvent
: dispatchEvent;
addEventCaptureListener(
element,
getRawEventName(topLevelType),
// Check if interactive and wrap in interactiveUpdates
dispatch.bind(null, topLevelType),
);
const rawEventName = getRawEventName(topLevelType);
// Check if interactive and wrap in interactiveUpdates
const listener = dispatch.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM);
if (capture) {
addEventCaptureListener(element, rawEventName, listener);
} else {
addEventBubbleListener(element, rawEventName, listener);
}
}
function dispatchInteractiveEvent(topLevelType, nativeEvent) {
interactiveUpdates(dispatchEvent, topLevelType, nativeEvent);
function dispatchInteractiveEvent(topLevelType, eventSystemFlags, nativeEvent) {
interactiveUpdates(
dispatchEvent,
topLevelType,
eventSystemFlags,
nativeEvent,
);
}
export function dispatchEvent(
topLevelType: DOMTopLevelEventType,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
) {
): void {
if (!_enabled) {
return;
}
@@ -211,6 +260,7 @@ export function dispatchEvent(
topLevelType,
nativeEvent,
targetInst,
eventSystemFlags,
);
try {

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import {canUseDOM} from 'shared/ExecutionEnvironment';
import {enableEventAPI} from 'shared/ReactFeatureFlags';
export let passiveBrowserEventsSupported = false;
// Check if browser support events with passive listeners
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support
if (enableEventAPI && canUseDOM) {
try {
const options = {
get passive() {
passiveBrowserEventsSupported = true;
},
};
window.addEventListener('test', options, options);
window.removeEventListener('test', options, options);
} catch (e) {
passiveBrowserEventsSupported = false;
}
}

View File

@@ -12,6 +12,8 @@ const EventListenerWWW = require('EventListener');
import typeof * as EventListenerType from '../EventListener';
import typeof * as EventListenerShimType from './EventListener-www';
const NORMAL_PRIORITY = 0;
export function addEventBubbleListener(
element: Element,
eventType: string,
@@ -28,6 +30,21 @@ export function addEventCaptureListener(
EventListenerWWW.capture(element, eventType, listener);
}
export function addEventListener(
element: Element,
eventType: string,
listener: Function,
options: {passive: boolean},
): void {
EventListenerWWW.listen(
element,
eventType,
listener,
NORMAL_PRIORITY,
options,
);
}
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@@ -21,6 +21,7 @@ import lowPriorityWarning from 'shared/lowPriorityWarning';
import warningWithoutStack from 'shared/warningWithoutStack';
import {ELEMENT_NODE} from '../shared/HTMLNodeType';
import * as DOMTopLevelEventTypes from '../events/DOMTopLevelEventTypes';
import {PLUGIN_EVENT_SYSTEM} from 'events/EventSystemFlags';
// for .act's return value
type Thenable = {
@@ -63,7 +64,7 @@ let hasWarnedAboutDeprecatedMockComponent = false;
*/
function simulateNativeEventOnNode(topLevelType, node, fakeNativeEvent) {
fakeNativeEvent.target = node;
dispatchEvent(topLevelType, fakeNativeEvent);
dispatchEvent(topLevelType, PLUGIN_EVENT_SYSTEM, fakeNativeEvent);
}
/**

View File

@@ -2523,9 +2523,14 @@ function flushSync<A, R>(fn: (a: A) => R, a: A): R {
}
}
function interactiveUpdates<A, B, R>(fn: (A, B) => R, a: A, b: B): R {
function interactiveUpdates<A, B, C, R>(
fn: (A, B, C) => R,
a: A,
b: B,
c: C,
): R {
if (isBatchingInteractiveUpdates) {
return fn(a, b);
return fn(a, b, c);
}
// If there are any pending interactive updates, synchronously flush them.
// This needs to happen before we read any handlers, because the effect of
@@ -2545,7 +2550,7 @@ function interactiveUpdates<A, B, R>(fn: (A, B) => R, a: A, b: B): R {
isBatchingInteractiveUpdates = true;
isBatchingUpdates = true;
try {
return fn(a, b);
return fn(a, b, c);
} finally {
isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
isBatchingUpdates = previousIsBatchingUpdates;

View File

@@ -31,7 +31,13 @@ declare module 'ReactFiberErrorDialog' {
// EventListener www fork
declare module 'EventListener' {
declare module.exports: {
listen: (target: Element, type: string, callback: Function) => mixed,
listen: (
target: Element,
type: string,
callback: Function,
priority?: number,
options?: {passive: boolean},
) => mixed,
capture: (target: Element, type: string, callback: Function) => mixed,
};
}