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
Dominic Gannaway committed
Mar 15, 2019 at 09:39 UTC
371bbf36bba53dd25fcab665bd43e7cebeecf6d3
11 files changed
+240
-101
packages/events/EventSystemFlags.js
new
+16
@@ -0,0 +1,16 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export type EventSystemFlags = number;
11
+
12
+export const PLUGIN_EVENT_SYSTEM = 1;
13
+export const RESPONDER_EVENT_SYSTEM = 1 << 1;
14
+export const IS_PASSIVE = 1 << 2;
15
+export const IS_ACTIVE = 1 << 3;
16
+export const PASSIVE_NOT_SUPPORTED = 1 << 4;
packages/events/ReactGenericBatching.js
+4
-4
@@ -20,8 +20,8 @@ import {
20
let _batchedUpdatesImpl = function(fn, bookkeeping) {
21
return fn(bookkeeping);
22
};
23
-let _interactiveUpdatesImpl = function(fn, a, b) {
24
- return fn(a, b);
23
+let _interactiveUpdatesImpl = function(fn, a, b, c) {
24
+ return fn(a, b, c);
25
};
26
let _flushInteractiveUpdatesImpl = function() {};
27
@@ -52,8 +52,8 @@ export function batchedUpdates(fn, bookkeeping) {
52
}
53
}
54
55
-export function interactiveUpdates(fn, a, b) {
56
- return _interactiveUpdatesImpl(fn, a, b);
55
+export function interactiveUpdates(fn, a, b, c) {
56
+ return _interactiveUpdatesImpl(fn, a, b, c);
57
}
58
59
export function flushInteractiveUpdates() {
packages/react-dom/src/client/ReactDOMComponent.js
+4
-1
@@ -253,7 +253,10 @@ if (__DEV__) {
253
};
254
}
255
256
-function ensureListeningTo(rootContainerElement, registrationName) {
256
+function ensureListeningTo(
257
+ rootContainerElement: Element | Node,
258
+ registrationName: string,
259
+): void {
260
const isDocumentOrFragment =
261
rootContainerElement.nodeType === DOCUMENT_NODE ||
262
rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
packages/react-dom/src/events/EventListener.js
+11
-2
@@ -8,7 +8,7 @@
8
*/
9
10
export function addEventBubbleListener(
11
- element: Document | Element,
11
+ element: Document | Element | Node,
12
eventType: string,
13
listener: Function,
14
): void {
@@ -16,9 +16,18 @@ export function addEventBubbleListener(
16
}
17
18
export function addEventCaptureListener(
19
- element: Document | Element,
19
+ element: Document | Element | Node,
20
eventType: string,
21
listener: Function,
22
): void {
23
element.addEventListener(eventType, listener, true);
24
}
25
+
26
+export function addEventListener(
27
+ element: Document | Element | Node,
28
+ eventType: string,
29
+ listener: Function,
30
+ options: {passive: boolean},
31
+): void {
32
+ element.addEventListener(eventType, listener, (options: any));
33
+}
packages/react-dom/src/events/ReactBrowserEventEmitter.js
+27
-24
@@ -8,6 +8,7 @@
8
*/
9
10
import {registrationNameDependencies} from 'events/EventPluginRegistry';
11
+import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
12
import {
13
TOP_BLUR,
14
TOP_CANCEL,
@@ -84,22 +85,23 @@ import isEventSupported from './isEventSupported';
85
* React Core . General Purpose Event Plugin System
86
*/
87
87
-const alreadyListeningTo = {};
88
-let reactTopListenersCounter = 0;
88
+const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
89
+const elementListeningSets:
90
+ | WeakMap
91
+ | Map<
92
+ Document | Element | Node,
93
+ Set<DOMTopLevelEventType>,
94
+ > = new PossiblyWeakMap();
95
90
-/**
91
- * To ensure no conflicts with other potential React instances on the page
92
- */
93
-const topListenersIDKey = '_reactListenersID' + ('' + Math.random()).slice(2);
94
-
95
-function getListeningForDocument(mountAt: any) {
96
- // In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty`
97
- // directly.
98
- if (!Object.prototype.hasOwnProperty.call(mountAt, topListenersIDKey)) {
99
- mountAt[topListenersIDKey] = reactTopListenersCounter++;
100
- alreadyListeningTo[mountAt[topListenersIDKey]] = {};
96
+function getListeningSetForElement(
97
+ element: Document | Element | Node,
98
+): Set<DOMTopLevelEventType> {
99
+ let listeningSet = elementListeningSets.get(element);
100
+ if (listeningSet === undefined) {
101
+ listeningSet = new Set();
102
+ elementListeningSets.set(element, listeningSet);
103
}
102
- return alreadyListeningTo[mountAt[topListenersIDKey]];
104
+ return listeningSet;
105
}
106
107
/**
@@ -125,14 +127,14 @@ function getListeningForDocument(mountAt: any) {
127
*/
128
export function listenTo(
129
registrationName: string,
128
- mountAt: Document | Element,
129
-) {
130
- const isListening = getListeningForDocument(mountAt);
130
+ mountAt: Document | Element | Node,
131
+): void {
132
+ const listeningSet = getListeningSetForElement(mountAt);
133
const dependencies = registrationNameDependencies[registrationName];
134
135
for (let i = 0; i < dependencies.length; i++) {
136
const dependency = dependencies[i];
135
- if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
137
+ if (!listeningSet.has(dependency)) {
138
switch (dependency) {
139
case TOP_SCROLL:
140
trapCapturedEvent(TOP_SCROLL, mountAt);
@@ -143,8 +145,8 @@ export function listenTo(
145
trapCapturedEvent(TOP_BLUR, mountAt);
146
// We set the flag for a single dependency later in this function,
147
// but this ensures we mark both as attached rather than just one.
146
- isListening[TOP_BLUR] = true;
147
- isListening[TOP_FOCUS] = true;
148
+ listeningSet.add(TOP_BLUR);
149
+ listeningSet.add(TOP_FOCUS);
150
break;
151
case TOP_CANCEL:
152
case TOP_CLOSE:
@@ -167,7 +169,7 @@ export function listenTo(
169
}
170
break;
171
}
170
- isListening[dependency] = true;
172
+ listeningSet.add(dependency);
173
}
174
}
175
}
@@ -175,12 +177,13 @@ export function listenTo(
177
export function isListeningToAllDependencies(
178
registrationName: string,
179
mountAt: Document | Element,
178
-) {
179
- const isListening = getListeningForDocument(mountAt);
180
+): boolean {
181
+ const listeningSet = getListeningSetForElement(mountAt);
182
const dependencies = registrationNameDependencies[registrationName];
183
+
184
for (let i = 0; i < dependencies.length; i++) {
185
const dependency = dependencies[i];
183
- if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
186
+ if (!listeningSet.has(dependency)) {
187
return false;
188
}
189
}
packages/react-dom/src/events/ReactDOMEventListener.js
+115
-65
@@ -15,18 +15,41 @@ import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching';
15
import {runExtractedEventsInBatch} from 'events/EventPluginHub';
16
import {isFiberMounted} from 'react-reconciler/reflection';
17
import {HostRoot} from 'shared/ReactWorkTags';
18
+import {
19
+ type EventSystemFlags,
20
+ PLUGIN_EVENT_SYSTEM,
21
+ RESPONDER_EVENT_SYSTEM,
22
+ IS_PASSIVE,
23
+ IS_ACTIVE,
24
+ PASSIVE_NOT_SUPPORTED,
25
+} from 'events/EventSystemFlags';
26
19
-import {addEventBubbleListener, addEventCaptureListener} from './EventListener';
27
+import {
28
+ addEventBubbleListener,
29
+ addEventCaptureListener,
30
+ addEventListener,
31
+} from './EventListener';
32
import getEventTarget from './getEventTarget';
33
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
34
import SimpleEventPlugin from './SimpleEventPlugin';
35
import {getRawEventName} from './DOMTopLevelEventTypes';
36
+import {passiveBrowserEventsSupported} from './checkPassiveEvents';
37
+
38
+import {enableEventAPI} from 'shared/ReactFeatureFlags';
39
40
const {isInteractiveTopLevelEventType} = SimpleEventPlugin;
41
42
const CALLBACK_BOOKKEEPING_POOL_SIZE = 10;
43
const callbackBookkeepingPool = [];
44
45
+type BookKeepingInstance = {
46
+ topLevelType: DOMTopLevelEventType | null,
47
+ nativeEvent: AnyNativeEvent | null,
48
+ targetInst: Fiber | null,
49
+ ancestors: Array<Fiber | null>,
50
+ eventSystemFlags: EventSystemFlags,
51
+};
52
+
53
/**
54
* Find the deepest React component completely containing the root of the
55
* passed-in instance (for use when entire React trees are nested within each
@@ -48,20 +71,17 @@ function findRootContainerNode(inst) {
71
72
// Used to store ancestor hierarchy in top level callback
73
function getTopLevelCallbackBookKeeping(
51
- topLevelType,
52
- nativeEvent,
53
- targetInst,
54
-): {
55
- topLevelType: ?DOMTopLevelEventType,
56
- nativeEvent: ?AnyNativeEvent,
74
+ topLevelType: DOMTopLevelEventType,
75
+ nativeEvent: AnyNativeEvent,
76
targetInst: Fiber | null,
58
- ancestors: Array<Fiber>,
59
-} {
77
+ eventSystemFlags: EventSystemFlags,
78
+): BookKeepingInstance {
79
if (callbackBookkeepingPool.length) {
80
const instance = callbackBookkeepingPool.pop();
81
instance.topLevelType = topLevelType;
82
instance.nativeEvent = nativeEvent;
83
instance.targetInst = targetInst;
84
+ instance.eventSystemFlags = eventSystemFlags;
85
return instance;
86
}
87
return {
@@ -69,20 +89,24 @@ function getTopLevelCallbackBookKeeping(
89
nativeEvent,
90
targetInst,
91
ancestors: [],
92
+ eventSystemFlags,
93
};
94
}
95
75
-function releaseTopLevelCallbackBookKeeping(instance) {
96
+function releaseTopLevelCallbackBookKeeping(
97
+ instance: BookKeepingInstance,
98
+): void {
99
instance.topLevelType = null;
100
instance.nativeEvent = null;
101
instance.targetInst = null;
102
instance.ancestors.length = 0;
103
+ instance.eventSystemFlags = 0;
104
if (callbackBookkeepingPool.length < CALLBACK_BOOKKEEPING_POOL_SIZE) {
105
callbackBookkeepingPool.push(instance);
106
}
107
}
108
85
-function handleTopLevel(bookKeeping) {
109
+function handleTopLevel(bookKeeping: BookKeepingInstance) {
110
let targetInst = bookKeeping.targetInst;
111
112
// Loop through the hierarchy, in case there's any nested components.
@@ -92,7 +116,8 @@ function handleTopLevel(bookKeeping) {
116
let ancestor = targetInst;
117
do {
118
if (!ancestor) {
95
- bookKeeping.ancestors.push(ancestor);
119
+ const ancestors = bookKeeping.ancestors;
120
+ ((ancestors: any): Array<Fiber | null>).push(ancestor);
121
break;
122
}
123
const root = findRootContainerNode(ancestor);
@@ -105,12 +130,17 @@ function handleTopLevel(bookKeeping) {
130
131
for (let i = 0; i < bookKeeping.ancestors.length; i++) {
132
targetInst = bookKeeping.ancestors[i];
108
- runExtractedEventsInBatch(
109
- bookKeeping.topLevelType,
110
- targetInst,
111
- bookKeeping.nativeEvent,
112
- getEventTarget(bookKeeping.nativeEvent),
113
- );
133
+ if (bookKeeping.eventSystemFlags === PLUGIN_EVENT_SYSTEM) {
134
+ runExtractedEventsInBatch(
135
+ ((bookKeeping.topLevelType: any): DOMTopLevelEventType),
136
+ targetInst,
137
+ ((bookKeeping.nativeEvent: any): AnyNativeEvent),
138
+ getEventTarget(bookKeeping.nativeEvent),
139
+ );
140
+ } else {
141
+ // RESPONDER_EVENT_SYSTEM
142
+ // TODO: Add implementation
143
+ }
144
}
145
}
146
@@ -125,70 +155,89 @@ export function isEnabled() {
155
return _enabled;
156
}
157
128
-/**
129
- * Traps top-level events by using event bubbling.
130
- *
131
- * @param {number} topLevelType Number from `TopLevelEventTypes`.
132
- * @param {object} element Element on which to attach listener.
133
- * @return {?object} An object with a remove function which will forcefully
134
- * remove the listener.
135
- * @internal
136
- */
158
export function trapBubbledEvent(
159
topLevelType: DOMTopLevelEventType,
139
- element: Document | Element,
140
-) {
141
- if (!element) {
142
- return null;
143
- }
144
- const dispatch = isInteractiveTopLevelEventType(topLevelType)
145
- ? dispatchInteractiveEvent
146
- : dispatchEvent;
147
-
148
- addEventBubbleListener(
149
- element,
150
- getRawEventName(topLevelType),
151
- // Check if interactive and wrap in interactiveUpdates
152
- dispatch.bind(null, topLevelType),
153
- );
160
+ element: Document | Element | Node,
161
+): void {
162
+ trapEventForPluginEventSystem(element, topLevelType, false);
163
}
164
156
-/**
157
- * Traps a top-level event by using event capturing.
158
- *
159
- * @param {number} topLevelType Number from `TopLevelEventTypes`.
160
- * @param {object} element Element on which to attach listener.
161
- * @return {?object} An object with a remove function which will forcefully
162
- * remove the listener.
163
- * @internal
164
- */
165
export function trapCapturedEvent(
166
topLevelType: DOMTopLevelEventType,
167
- element: Document | Element,
168
-) {
169
- if (!element) {
170
- return null;
167
+ element: Document | Element | Node,
168
+): void {
169
+ trapEventForPluginEventSystem(element, topLevelType, true);
170
+}
171
+
172
+export function trapEventForResponderEventSystem(
173
+ element: Document | Element | Node,
174
+ topLevelType: DOMTopLevelEventType,
175
+ capture: boolean,
176
+ passive: boolean,
177
+): void {
178
+ if (enableEventAPI) {
179
+ const dispatch = isInteractiveTopLevelEventType(topLevelType)
180
+ ? dispatchInteractiveEvent
181
+ : dispatchEvent;
182
+ const rawEventName = getRawEventName(topLevelType);
183
+ let eventFlags = RESPONDER_EVENT_SYSTEM;
184
+
185
+ // If passive option is not supported, then the event will be
186
+ // active and not passive, but we flag it as using not being
187
+ // supported too. This way the responder event plugins know,
188
+ // and can provide polyfills if needed.
189
+ if (passive) {
190
+ if (passiveBrowserEventsSupported) {
191
+ eventFlags |= IS_ACTIVE;
192
+ eventFlags |= PASSIVE_NOT_SUPPORTED;
193
+ passive = false;
194
+ } else {
195
+ eventFlags |= IS_PASSIVE;
196
+ }
197
+ } else {
198
+ eventFlags |= IS_ACTIVE;
199
+ }
200
+ // Check if interactive and wrap in interactiveUpdates
201
+ const listener = dispatch.bind(null, topLevelType, eventFlags);
202
+ addEventListener(element, rawEventName, listener, {
203
+ capture,
204
+ passive,
205
+ });
206
}
207
+}
208
+
209
+function trapEventForPluginEventSystem(
210
+ element: Document | Element | Node,
211
+ topLevelType: DOMTopLevelEventType,
212
+ capture: boolean,
213
+): void {
214
const dispatch = isInteractiveTopLevelEventType(topLevelType)
215
? dispatchInteractiveEvent
216
: dispatchEvent;
175
-
176
- addEventCaptureListener(
177
- element,
178
- getRawEventName(topLevelType),
179
- // Check if interactive and wrap in interactiveUpdates
180
- dispatch.bind(null, topLevelType),
181
- );
217
+ const rawEventName = getRawEventName(topLevelType);
218
+ // Check if interactive and wrap in interactiveUpdates
219
+ const listener = dispatch.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM);
220
+ if (capture) {
221
+ addEventCaptureListener(element, rawEventName, listener);
222
+ } else {
223
+ addEventBubbleListener(element, rawEventName, listener);
224
+ }
225
}
226
184
-function dispatchInteractiveEvent(topLevelType, nativeEvent) {
185
- interactiveUpdates(dispatchEvent, topLevelType, nativeEvent);
227
+function dispatchInteractiveEvent(topLevelType, eventSystemFlags, nativeEvent) {
228
+ interactiveUpdates(
229
+ dispatchEvent,
230
+ topLevelType,
231
+ eventSystemFlags,
232
+ nativeEvent,
233
+ );
234
}
235
236
export function dispatchEvent(
237
topLevelType: DOMTopLevelEventType,
238
+ eventSystemFlags: EventSystemFlags,
239
nativeEvent: AnyNativeEvent,
191
-) {
240
+): void {
241
if (!_enabled) {
242
return;
243
}
@@ -211,6 +260,7 @@ export function dispatchEvent(
260
topLevelType,
261
nativeEvent,
262
targetInst,
263
+ eventSystemFlags,
264
);
265
266
try {
packages/react-dom/src/events/checkPassiveEvents.js
new
+29
@@ -0,0 +1,29 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import {canUseDOM} from 'shared/ExecutionEnvironment';
11
+import {enableEventAPI} from 'shared/ReactFeatureFlags';
12
+
13
+export let passiveBrowserEventsSupported = false;
14
+
15
+// Check if browser support events with passive listeners
16
+// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support
17
+if (enableEventAPI && canUseDOM) {
18
+ try {
19
+ const options = {
20
+ get passive() {
21
+ passiveBrowserEventsSupported = true;
22
+ },
23
+ };
24
+ window.addEventListener('test', options, options);
25
+ window.removeEventListener('test', options, options);
26
+ } catch (e) {
27
+ passiveBrowserEventsSupported = false;
28
+ }
29
+}
packages/react-dom/src/events/forks/EventListener-www.js
+17
@@ -12,6 +12,8 @@ const EventListenerWWW = require('EventListener');
12
import typeof * as EventListenerType from '../EventListener';
13
import typeof * as EventListenerShimType from './EventListener-www';
14
15
+const NORMAL_PRIORITY = 0;
16
+
17
export function addEventBubbleListener(
18
element: Element,
19
eventType: string,
@@ -28,6 +30,21 @@ export function addEventCaptureListener(
30
EventListenerWWW.capture(element, eventType, listener);
31
}
32
33
+export function addEventListener(
34
+ element: Element,
35
+ eventType: string,
36
+ listener: Function,
37
+ options: {passive: boolean},
38
+): void {
39
+ EventListenerWWW.listen(
40
+ element,
41
+ eventType,
42
+ listener,
43
+ NORMAL_PRIORITY,
44
+ options,
45
+ );
46
+}
47
+
48
// Flow magic to verify the exports of this file match the original version.
49
// eslint-disable-next-line no-unused-vars
50
type Check<_X, Y: _X, X: Y = _X> = null;
packages/react-dom/src/test-utils/ReactTestUtils.js
+2
-1
@@ -21,6 +21,7 @@ import lowPriorityWarning from 'shared/lowPriorityWarning';
21
import warningWithoutStack from 'shared/warningWithoutStack';
22
import {ELEMENT_NODE} from '../shared/HTMLNodeType';
23
import * as DOMTopLevelEventTypes from '../events/DOMTopLevelEventTypes';
24
+import {PLUGIN_EVENT_SYSTEM} from 'events/EventSystemFlags';
25
26
// for .act's return value
27
type Thenable = {
@@ -63,7 +64,7 @@ let hasWarnedAboutDeprecatedMockComponent = false;
64
*/
65
function simulateNativeEventOnNode(topLevelType, node, fakeNativeEvent) {
66
fakeNativeEvent.target = node;
66
- dispatchEvent(topLevelType, fakeNativeEvent);
67
+ dispatchEvent(topLevelType, PLUGIN_EVENT_SYSTEM, fakeNativeEvent);
68
}
69
70
/**
packages/react-reconciler/src/ReactFiberScheduler.js
+8
-3
@@ -2523,9 +2523,14 @@ function flushSync<A, R>(fn: (a: A) => R, a: A): R {
2523
}
2524
}
2525
2526
-function interactiveUpdates<A, B, R>(fn: (A, B) => R, a: A, b: B): R {
2526
+function interactiveUpdates<A, B, C, R>(
2527
+ fn: (A, B, C) => R,
2528
+ a: A,
2529
+ b: B,
2530
+ c: C,
2531
+): R {
2532
if (isBatchingInteractiveUpdates) {
2528
- return fn(a, b);
2533
+ return fn(a, b, c);
2534
}
2535
// If there are any pending interactive updates, synchronously flush them.
2536
// 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 {
2550
isBatchingInteractiveUpdates = true;
2551
isBatchingUpdates = true;
2552
try {
2548
- return fn(a, b);
2553
+ return fn(a, b, c);
2554
} finally {
2555
isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
2556
isBatchingUpdates = previousIsBatchingUpdates;
scripts/flow/environment.js
+7
-1
@@ -31,7 +31,13 @@ declare module 'ReactFiberErrorDialog' {
31
// EventListener www fork
32
declare module 'EventListener' {
33
declare module.exports: {
34
- listen: (target: Element, type: string, callback: Function) => mixed,
34
+ listen: (
35
+ target: Element,
36
+ type: string,
37
+ callback: Function,
38
+ priority?: number,
39
+ options?: {passive: boolean},
40
+ ) => mixed,
41
capture: (target: Element, type: string, callback: Function) => mixed,
42
};
43
}