@samitouri / QOS-React / commits / 371bbf36bb

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 }