React Event System: cleanup plugins + break out update batching logic (#18503)
Dominic Gannaway committed
Apr 6, 2020 at 22:01 UTC
a3875663f6e5bb8bdc28c9cf71cf1312615c9c84
13 files changed
+346
-129
packages/legacy-events/EventPluginRegistry.js
+17
@@ -241,3 +241,20 @@ export function injectEventPluginsByName(
241
recomputePluginOrdering();
242
}
243
}
244
+
245
+export function injectEventPlugins(
246
+ eventPlugins: [PluginModule<AnyNativeEvent>],
247
+): void {
248
+ for (let i = 0; i < eventPlugins.length; i++) {
249
+ const pluginModule = eventPlugins[i];
250
+ plugins.push(pluginModule);
251
+ const publishedEvents = pluginModule.eventTypes;
252
+ for (const eventName in publishedEvents) {
253
+ publishEventForPlugin(
254
+ publishedEvents[eventName],
255
+ pluginModule,
256
+ eventName,
257
+ );
258
+ }
259
+ }
260
+}
packages/legacy-events/ReactGenericBatching.js
+1
-58
@@ -5,14 +5,6 @@
5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import {
9
- needsStateRestore,
10
- restoreStateIfNeeded,
11
-} from './ReactControlledComponent';
12
-
13
-import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags';
14
-import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils';
15
-
8
// Used as a way to call batchedUpdates when we don't have a reference to
9
// the renderer. Such as when we're dispatching events or if third party
10
// libraries need to call batchedUpdates. Eventually, this API will go away when
@@ -32,21 +24,6 @@ let batchedEventUpdatesImpl = batchedUpdatesImpl;
24
let isInsideEventHandler = false;
25
let isBatchingEventUpdates = false;
26
35
-function finishEventHandler() {
36
- // Here we wait until all updates have propagated, which is important
37
- // when using controlled components within layers:
38
- // https://github.com/facebook/react/issues/1698
39
- // Then we restore state of any controlled component.
40
- const controlledComponentsHavePendingUpdates = needsStateRestore();
41
- if (controlledComponentsHavePendingUpdates) {
42
- // If a controlled event was fired, we may need to restore the state of
43
- // the DOM node back to the controlled value. This is necessary when React
44
- // bails out of the update without touching the DOM.
45
- flushDiscreteUpdatesImpl();
46
- restoreStateIfNeeded();
47
- }
48
-}
49
-
27
export function batchedUpdates(fn, bookkeeping) {
28
if (isInsideEventHandler) {
29
// If we are currently inside another batch, we need to wait until it
@@ -58,7 +35,6 @@ export function batchedUpdates(fn, bookkeeping) {
35
return batchedUpdatesImpl(fn, bookkeeping);
36
} finally {
37
isInsideEventHandler = false;
61
- finishEventHandler();
38
}
39
}
40
@@ -73,19 +49,6 @@ export function batchedEventUpdates(fn, a, b) {
49
return batchedEventUpdatesImpl(fn, a, b);
50
} finally {
51
isBatchingEventUpdates = false;
76
- finishEventHandler();
77
- }
78
-}
79
-
80
-// This is for the React Flare event system
81
-export function executeUserEventHandler(fn: any => void, value: any): void {
82
- const previouslyInEventHandler = isInsideEventHandler;
83
- try {
84
- isInsideEventHandler = true;
85
- const type = typeof value === 'object' && value !== null ? value.type : '';
86
- invokeGuardedCallbackAndCatchFirstError(type, fn, undefined, value);
87
- } finally {
88
- isInsideEventHandler = previouslyInEventHandler;
52
}
53
}
54
@@ -97,32 +60,12 @@ export function discreteUpdates(fn, a, b, c, d) {
60
} finally {
61
isInsideEventHandler = prevIsInsideEventHandler;
62
if (!isInsideEventHandler) {
100
- finishEventHandler();
63
}
64
}
65
}
66
105
-let lastFlushedEventTimeStamp = 0;
67
export function flushDiscreteUpdatesIfNeeded(timeStamp: number) {
107
- // event.timeStamp isn't overly reliable due to inconsistencies in
108
- // how different browsers have historically provided the time stamp.
109
- // Some browsers provide high-resolution time stamps for all events,
110
- // some provide low-resolution time stamps for all events. FF < 52
111
- // even mixes both time stamps together. Some browsers even report
112
- // negative time stamps or time stamps that are 0 (iOS9) in some cases.
113
- // Given we are only comparing two time stamps with equality (!==),
114
- // we are safe from the resolution differences. If the time stamp is 0
115
- // we bail-out of preventing the flush, which can affect semantics,
116
- // such as if an earlier flush removes or adds event listeners that
117
- // are fired in the subsequent flush. However, this is the same
118
- // behaviour as we had before this change, so the risks are low.
119
- if (
120
- !isInsideEventHandler &&
121
- (!enableDeprecatedFlareAPI ||
122
- timeStamp === 0 ||
123
- lastFlushedEventTimeStamp !== timeStamp)
124
- ) {
125
- lastFlushedEventTimeStamp = timeStamp;
68
+ if (!isInsideEventHandler) {
69
flushDiscreteUpdatesImpl();
70
}
71
}
packages/react-dom/src/client/ReactDOM.js
+6
-8
@@ -39,13 +39,6 @@ import {
39
} from 'react-reconciler/src/ReactFiberReconciler';
40
import {createPortal as createPortalImpl} from 'react-reconciler/src/ReactPortal';
41
import {canUseDOM} from 'shared/ExecutionEnvironment';
42
-import {setBatchingImplementation} from 'legacy-events/ReactGenericBatching';
43
-import {
44
- setRestoreImplementation,
45
- enqueueStateRestore,
46
- restoreStateIfNeeded,
47
-} from 'legacy-events/ReactControlledComponent';
48
-import {runEventsInBatch} from 'legacy-events/EventBatching';
42
import {
43
eventNameDispatchConfigs,
44
injectEventPluginsByName,
@@ -69,6 +62,12 @@ import {
62
setAttemptHydrationAtCurrentPriority,
63
queueExplicitHydrationTarget,
64
} from '../events/ReactDOMEventReplaying';
65
+import {setBatchingImplementation} from '../events/ReactDOMUpdateBatching';
66
+import {
67
+ setRestoreImplementation,
68
+ enqueueStateRestore,
69
+ restoreStateIfNeeded,
70
+} from '../events/ReactDOMControlledComponent';
71
72
setAttemptSynchronousHydration(attemptSynchronousHydration);
73
setAttemptUserBlockingHydration(attemptUserBlockingHydration);
@@ -183,7 +182,6 @@ const Internals = {
182
enqueueStateRestore,
183
restoreStateIfNeeded,
184
dispatchEvent,
186
- runEventsInBatch,
185
flushPassiveEffects,
186
IsThisRendererActing,
187
],
packages/react-dom/src/client/ReactDOMClientInjection.js
+48
-36
@@ -20,43 +20,55 @@ import SimpleEventPlugin from '../events/SimpleEventPlugin';
20
import {
21
injectEventPluginOrder,
22
injectEventPluginsByName,
23
+ injectEventPlugins,
24
} from 'legacy-events/EventPluginRegistry';
25
+import {enableModernEventSystem} from 'shared/ReactFeatureFlags';
26
25
-/**
26
- * Specifies a deterministic ordering of `EventPlugin`s. A convenient way to
27
- * reason about plugins, without having to package every one of them. This
28
- * is better than having plugins be ordered in the same order that they
29
- * are injected because that ordering would be influenced by the packaging order.
30
- * `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that
31
- * preventing default on events is convenient in `SimpleEventPlugin` handlers.
32
- */
33
-const DOMEventPluginOrder = [
34
- 'ResponderEventPlugin',
35
- 'SimpleEventPlugin',
36
- 'EnterLeaveEventPlugin',
37
- 'ChangeEventPlugin',
38
- 'SelectEventPlugin',
39
- 'BeforeInputEventPlugin',
40
-];
27
+if (enableModernEventSystem) {
28
+ injectEventPlugins([
29
+ SimpleEventPlugin,
30
+ EnterLeaveEventPlugin,
31
+ ChangeEventPlugin,
32
+ SelectEventPlugin,
33
+ BeforeInputEventPlugin,
34
+ ]);
35
+} else {
36
+ /**
37
+ * Specifies a deterministic ordering of `EventPlugin`s. A convenient way to
38
+ * reason about plugins, without having to package every one of them. This
39
+ * is better than having plugins be ordered in the same order that they
40
+ * are injected because that ordering would be influenced by the packaging order.
41
+ * `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that
42
+ * preventing default on events is convenient in `SimpleEventPlugin` handlers.
43
+ */
44
+ const DOMEventPluginOrder = [
45
+ 'ResponderEventPlugin',
46
+ 'SimpleEventPlugin',
47
+ 'EnterLeaveEventPlugin',
48
+ 'ChangeEventPlugin',
49
+ 'SelectEventPlugin',
50
+ 'BeforeInputEventPlugin',
51
+ ];
52
42
-/**
43
- * Inject modules for resolving DOM hierarchy and plugin ordering.
44
- */
45
-injectEventPluginOrder(DOMEventPluginOrder);
46
-setComponentTree(
47
- getFiberCurrentPropsFromNode,
48
- getInstanceFromNode,
49
- getNodeFromInstance,
50
-);
53
+ /**
54
+ * Inject modules for resolving DOM hierarchy and plugin ordering.
55
+ */
56
+ injectEventPluginOrder(DOMEventPluginOrder);
57
+ setComponentTree(
58
+ getFiberCurrentPropsFromNode,
59
+ getInstanceFromNode,
60
+ getNodeFromInstance,
61
+ );
62
52
-/**
53
- * Some important event plugins included by default (without having to require
54
- * them).
55
- */
56
-injectEventPluginsByName({
57
- SimpleEventPlugin: SimpleEventPlugin,
58
- EnterLeaveEventPlugin: EnterLeaveEventPlugin,
59
- ChangeEventPlugin: ChangeEventPlugin,
60
- SelectEventPlugin: SelectEventPlugin,
61
- BeforeInputEventPlugin: BeforeInputEventPlugin,
62
-});
63
+ /**
64
+ * Some important event plugins included by default (without having to require
65
+ * them).
66
+ */
67
+ injectEventPluginsByName({
68
+ SimpleEventPlugin: SimpleEventPlugin,
69
+ EnterLeaveEventPlugin: EnterLeaveEventPlugin,
70
+ ChangeEventPlugin: ChangeEventPlugin,
71
+ SelectEventPlugin: SelectEventPlugin,
72
+ BeforeInputEventPlugin: BeforeInputEventPlugin,
73
+ });
74
+}
packages/react-dom/src/events/ChangeEventPlugin.js
+12
-4
@@ -6,8 +6,6 @@
6
*/
7
8
import {runEventsInBatch} from 'legacy-events/EventBatching';
9
-import {enqueueStateRestore} from 'legacy-events/ReactControlledComponent';
10
-import {batchedUpdates} from 'legacy-events/ReactGenericBatching';
9
import SyntheticEvent from 'legacy-events/SyntheticEvent';
10
import isTextInputElement from './isTextInputElement';
11
import {canUseDOM} from 'shared/ExecutionEnvironment';
@@ -27,9 +25,15 @@ import isEventSupported from './isEventSupported';
25
import {getNodeFromInstance} from '../client/ReactDOMComponentTree';
26
import {updateValueIfChanged} from '../client/inputValueTracking';
27
import {setDefaultValue} from '../client/ReactDOMInput';
28
+import {enqueueStateRestore} from './ReactDOMControlledComponent';
29
31
-import {disableInputAttributeSyncing} from 'shared/ReactFeatureFlags';
30
+import {
31
+ disableInputAttributeSyncing,
32
+ enableModernEventSystem,
33
+} from 'shared/ReactFeatureFlags';
34
import accumulateTwoPhaseListeners from './accumulateTwoPhaseListeners';
35
+import {batchedUpdates} from './ReactDOMUpdateBatching';
36
+import {dispatchEventsInBatch} from './DOMModernPluginEventSystem';
37
38
const eventTypes = {
39
change: {
@@ -101,7 +105,11 @@ function manualDispatchChangeEvent(nativeEvent) {
105
}
106
107
function runEventInBatch(event) {
104
- runEventsInBatch(event);
108
+ if (enableModernEventSystem) {
109
+ dispatchEventsInBatch([event]);
110
+ } else {
111
+ runEventsInBatch(event);
112
+ }
113
}
114
115
function getInstIfValueChanged(targetInst) {
packages/react-dom/src/events/DOMLegacyEventPluginSystem.js
+1
-1
@@ -22,7 +22,6 @@ import {
22
HostText,
23
} from 'react-reconciler/src/ReactWorkTags';
24
import {IS_FIRST_ANCESTOR, PLUGIN_EVENT_SYSTEM} from './EventSystemFlags';
25
-import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching';
25
import {runEventsInBatch} from 'legacy-events/EventBatching';
26
import {plugins} from 'legacy-events/EventPluginRegistry';
27
import accumulateInto from 'legacy-events/accumulateInto';
@@ -45,6 +44,7 @@ import {
44
mediaEventTypes,
45
} from './DOMTopLevelEventTypes';
46
import {addTrappedEventListener} from './ReactDOMEventListener';
47
+import {batchedEventUpdates} from './ReactDOMUpdateBatching';
48
49
/**
50
* Summary of `DOMEventPluginSystem` event handling:
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+21
-10
@@ -24,7 +24,6 @@ import type {
24
import type {ReactDOMListener} from '../shared/ReactDOMTypes';
25
26
import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';
27
-import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching';
27
import {plugins} from 'legacy-events/EventPluginRegistry';
28
import {
29
PLUGIN_EVENT_SYSTEM,
@@ -86,12 +85,16 @@ import {
85
} from '../client/ReactDOMComponentTree';
86
import {COMMENT_NODE} from '../shared/HTMLNodeType';
87
import {topLevelEventsToDispatchConfig} from './DOMEventProperties';
88
+import {batchedEventUpdates} from './ReactDOMUpdateBatching';
89
90
import {
91
enableLegacyFBSupport,
92
enableUseEventAPI,
93
} from 'shared/ReactFeatureFlags';
94
-import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils';
94
+import {
95
+ invokeGuardedCallbackAndCatchFirstError,
96
+ rethrowCaughtError,
97
+} from 'shared/ReactErrorUtils';
98
99
const capturePhaseEvents = new Set([
100
TOP_FOCUS,
@@ -213,6 +216,21 @@ function executeDispatchesInOrder(event: ReactSyntheticEvent): void {
216
event._dispatchCurrentTargets = null;
217
}
218
219
+export function dispatchEventsInBatch(
220
+ events: Array<ReactSyntheticEvent>,
221
+): void {
222
+ for (let i = 0; i < events.length; i++) {
223
+ const syntheticEvent = events[i];
224
+ executeDispatchesInOrder(syntheticEvent);
225
+ // Release the event from the pool if needed
226
+ if (!syntheticEvent.isPersistent()) {
227
+ syntheticEvent.constructor.release(syntheticEvent);
228
+ }
229
+ }
230
+ // This would be a good time to rethrow if any of the event handlers threw.
231
+ rethrowCaughtError();
232
+}
233
+
234
function dispatchEventsForPlugins(
235
topLevelType: DOMTopLevelEventType,
236
eventSystemFlags: EventSystemFlags,
@@ -244,14 +262,7 @@ function dispatchEventsForPlugins(
262
}
263
}
264
}
247
- for (let i = 0; i < syntheticEvents.length; i++) {
248
- const syntheticEvent = syntheticEvents[i];
249
- executeDispatchesInOrder(syntheticEvent);
250
- // Release the event from the pool if needed
251
- if (!syntheticEvent.isPersistent()) {
252
- syntheticEvent.constructor.release(syntheticEvent);
253
- }
254
- }
265
+ dispatchEventsInBatch(syntheticEvents);
266
}
267
268
function shouldUpgradeListener(
packages/react-dom/src/events/DeprecatedDOMEventResponderSystem.js
+2
-2
@@ -30,13 +30,13 @@ import {
30
discreteUpdates,
31
flushDiscreteUpdatesIfNeeded,
32
executeUserEventHandler,
33
-} from 'legacy-events/ReactGenericBatching';
34
-import {enqueueStateRestore} from 'legacy-events/ReactControlledComponent';
33
+} from './ReactDOMUpdateBatching';
34
import type {Fiber} from 'react-reconciler/src/ReactFiber';
35
import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags';
36
import invariant from 'shared/invariant';
37
38
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
39
+import {enqueueStateRestore} from './ReactDOMControlledComponent';
40
import {
41
ContinuousEvent,
42
UserBlockingEvent,
packages/react-dom/src/events/ReactDOMControlledComponent.js
renamed
+3
-4
@@ -8,11 +8,10 @@
8
*/
9
10
import invariant from 'shared/invariant';
11
-
11
import {
12
getInstanceFromNode,
13
getFiberCurrentPropsFromNode,
15
-} from './EventPluginUtils';
14
+} from '../client/ReactDOMComponentTree';
15
16
// Use to restore controlled state after a change event has fired.
17
@@ -20,7 +19,7 @@ let restoreImpl = null;
19
let restoreTarget = null;
20
let restoreQueue = null;
21
23
-function restoreStateOfTarget(target) {
22
+function restoreStateOfTarget(target: Node) {
23
// We perform this translation at the end of the event loop so that we
24
// always receive the correct fiber here
25
const internalInstance = getInstanceFromNode(target);
@@ -47,7 +46,7 @@ export function setRestoreImplementation(
46
restoreImpl = impl;
47
}
48
50
-export function enqueueStateRestore(target: EventTarget): void {
49
+export function enqueueStateRestore(target: Node): void {
50
if (restoreTarget) {
51
if (restoreQueue) {
52
restoreQueue.push(target);
packages/react-dom/src/events/ReactDOMEventListener.js
+4
-4
@@ -17,10 +17,6 @@ import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
17
// CommonJS interop named imports.
18
import * as Scheduler from 'scheduler';
19
20
-import {
21
- discreteUpdates,
22
- flushDiscreteUpdatesIfNeeded,
23
-} from 'legacy-events/ReactGenericBatching';
20
import {DEPRECATED_dispatchEventForResponderEventSystem} from './DeprecatedDOMEventResponderSystem';
21
import {
22
isReplayableDiscreteEvent,
@@ -70,6 +66,10 @@ import {
66
import {getEventPriorityForPluginSystem} from './DOMEventProperties';
67
import {dispatchEventForLegacyPluginEventSystem} from './DOMLegacyEventPluginSystem';
68
import {dispatchEventForPluginEventSystem} from './DOMModernPluginEventSystem';
69
+import {
70
+ flushDiscreteUpdatesIfNeeded,
71
+ discreteUpdates,
72
+} from './ReactDOMUpdateBatching';
73
74
const {
75
unstable_UserBlockingPriority: UserBlockingPriority,
packages/react-dom/src/events/ReactDOMUpdateBatching.js
new
+140
@@ -0,0 +1,140 @@
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
+
8
+import {
9
+ needsStateRestore,
10
+ restoreStateIfNeeded,
11
+} from './ReactDOMControlledComponent';
12
+
13
+import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags';
14
+import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils';
15
+
16
+// Used as a way to call batchedUpdates when we don't have a reference to
17
+// the renderer. Such as when we're dispatching events or if third party
18
+// libraries need to call batchedUpdates. Eventually, this API will go away when
19
+// everything is batched by default. We'll then have a similar API to opt-out of
20
+// scheduled work and instead do synchronous work.
21
+
22
+// Defaults
23
+let batchedUpdatesImpl = function(fn, bookkeeping) {
24
+ return fn(bookkeeping);
25
+};
26
+let discreteUpdatesImpl = function(fn, a, b, c, d) {
27
+ return fn(a, b, c, d);
28
+};
29
+let flushDiscreteUpdatesImpl = function() {};
30
+let batchedEventUpdatesImpl = batchedUpdatesImpl;
31
+
32
+let isInsideEventHandler = false;
33
+let isBatchingEventUpdates = false;
34
+
35
+function finishEventHandler() {
36
+ // Here we wait until all updates have propagated, which is important
37
+ // when using controlled components within layers:
38
+ // https://github.com/facebook/react/issues/1698
39
+ // Then we restore state of any controlled component.
40
+ const controlledComponentsHavePendingUpdates = needsStateRestore();
41
+ if (controlledComponentsHavePendingUpdates) {
42
+ // If a controlled event was fired, we may need to restore the state of
43
+ // the DOM node back to the controlled value. This is necessary when React
44
+ // bails out of the update without touching the DOM.
45
+ flushDiscreteUpdatesImpl();
46
+ restoreStateIfNeeded();
47
+ }
48
+}
49
+
50
+export function batchedUpdates(fn, bookkeeping) {
51
+ if (isInsideEventHandler) {
52
+ // If we are currently inside another batch, we need to wait until it
53
+ // fully completes before restoring state.
54
+ return fn(bookkeeping);
55
+ }
56
+ isInsideEventHandler = true;
57
+ try {
58
+ return batchedUpdatesImpl(fn, bookkeeping);
59
+ } finally {
60
+ isInsideEventHandler = false;
61
+ finishEventHandler();
62
+ }
63
+}
64
+
65
+export function batchedEventUpdates(fn, a, b) {
66
+ if (isBatchingEventUpdates) {
67
+ // If we are currently inside another batch, we need to wait until it
68
+ // fully completes before restoring state.
69
+ return fn(a, b);
70
+ }
71
+ isBatchingEventUpdates = true;
72
+ try {
73
+ return batchedEventUpdatesImpl(fn, a, b);
74
+ } finally {
75
+ isBatchingEventUpdates = false;
76
+ finishEventHandler();
77
+ }
78
+}
79
+
80
+// This is for the React Flare event system
81
+export function executeUserEventHandler(fn: any => void, value: any): void {
82
+ const previouslyInEventHandler = isInsideEventHandler;
83
+ try {
84
+ isInsideEventHandler = true;
85
+ const type = typeof value === 'object' && value !== null ? value.type : '';
86
+ invokeGuardedCallbackAndCatchFirstError(type, fn, undefined, value);
87
+ } finally {
88
+ isInsideEventHandler = previouslyInEventHandler;
89
+ }
90
+}
91
+
92
+export function discreteUpdates(fn, a, b, c, d) {
93
+ const prevIsInsideEventHandler = isInsideEventHandler;
94
+ isInsideEventHandler = true;
95
+ try {
96
+ return discreteUpdatesImpl(fn, a, b, c, d);
97
+ } finally {
98
+ isInsideEventHandler = prevIsInsideEventHandler;
99
+ if (!isInsideEventHandler) {
100
+ finishEventHandler();
101
+ }
102
+ }
103
+}
104
+
105
+let lastFlushedEventTimeStamp = 0;
106
+export function flushDiscreteUpdatesIfNeeded(timeStamp: number) {
107
+ // event.timeStamp isn't overly reliable due to inconsistencies in
108
+ // how different browsers have historically provided the time stamp.
109
+ // Some browsers provide high-resolution time stamps for all events,
110
+ // some provide low-resolution time stamps for all events. FF < 52
111
+ // even mixes both time stamps together. Some browsers even report
112
+ // negative time stamps or time stamps that are 0 (iOS9) in some cases.
113
+ // Given we are only comparing two time stamps with equality (!==),
114
+ // we are safe from the resolution differences. If the time stamp is 0
115
+ // we bail-out of preventing the flush, which can affect semantics,
116
+ // such as if an earlier flush removes or adds event listeners that
117
+ // are fired in the subsequent flush. However, this is the same
118
+ // behaviour as we had before this change, so the risks are low.
119
+ if (
120
+ !isInsideEventHandler &&
121
+ (!enableDeprecatedFlareAPI ||
122
+ timeStamp === 0 ||
123
+ lastFlushedEventTimeStamp !== timeStamp)
124
+ ) {
125
+ lastFlushedEventTimeStamp = timeStamp;
126
+ flushDiscreteUpdatesImpl();
127
+ }
128
+}
129
+
130
+export function setBatchingImplementation(
131
+ _batchedUpdatesImpl,
132
+ _discreteUpdatesImpl,
133
+ _flushDiscreteUpdatesImpl,
134
+ _batchedEventUpdatesImpl,
135
+) {
136
+ batchedUpdatesImpl = _batchedUpdatesImpl;
137
+ discreteUpdatesImpl = _discreteUpdatesImpl;
138
+ flushDiscreteUpdatesImpl = _flushDiscreteUpdatesImpl;
139
+ batchedEventUpdatesImpl = _batchedEventUpdatesImpl;
140
+}
packages/react-dom/src/test-utils/ReactTestUtils.js
+91
-1
@@ -23,6 +23,10 @@ import act from './ReactTestUtilsAct';
23
import forEachAccumulated from 'legacy-events/forEachAccumulated';
24
import accumulateInto from 'legacy-events/accumulateInto';
25
import {enableModernEventSystem} from 'shared/ReactFeatureFlags';
26
+import {
27
+ rethrowCaughtError,
28
+ invokeGuardedCallbackAndCatchFirstError,
29
+} from 'shared/ReactErrorUtils';
30
31
const {findDOMNode} = ReactDOM;
32
// Keep in sync with ReactDOMUnstableNativeDependencies.js
@@ -38,7 +42,6 @@ const [
42
enqueueStateRestore,
43
restoreStateIfNeeded,
44
dispatchEvent,
41
- runEventsInBatch,
45
/* eslint-disable no-unused-vars */
46
flushPassiveEffects,
47
IsThisRendererActing,
@@ -354,6 +357,93 @@ function nativeTouchData(x, y) {
357
// Start of inline: the below functions were inlined from
358
// EventPropagator.js, as they deviated from ReactDOM's newer
359
// implementations.
360
+
361
+/**
362
+ * Dispatch the event to the listener.
363
+ * @param {SyntheticEvent} event SyntheticEvent to handle
364
+ * @param {function} listener Application-level callback
365
+ * @param {*} inst Internal component instance
366
+ */
367
+function executeDispatch(event, listener, inst) {
368
+ const type = event.type || 'unknown-event';
369
+ event.currentTarget = getNodeFromInstance(inst);
370
+ invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
371
+ event.currentTarget = null;
372
+}
373
+
374
+/**
375
+ * Standard/simple iteration through an event's collected dispatches.
376
+ */
377
+function executeDispatchesInOrder(event) {
378
+ const dispatchListeners = event._dispatchListeners;
379
+ const dispatchInstances = event._dispatchInstances;
380
+ if (Array.isArray(dispatchListeners)) {
381
+ for (let i = 0; i < dispatchListeners.length; i++) {
382
+ if (event.isPropagationStopped()) {
383
+ break;
384
+ }
385
+ // Listeners and Instances are two parallel arrays that are always in sync.
386
+ executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
387
+ }
388
+ } else if (dispatchListeners) {
389
+ executeDispatch(event, dispatchListeners, dispatchInstances);
390
+ }
391
+ event._dispatchListeners = null;
392
+ event._dispatchInstances = null;
393
+}
394
+
395
+/**
396
+ * Internal queue of events that have accumulated their dispatches and are
397
+ * waiting to have their dispatches executed.
398
+ */
399
+let eventQueue: ?(Array<ReactSyntheticEvent> | ReactSyntheticEvent) = null;
400
+
401
+/**
402
+ * Dispatches an event and releases it back into the pool, unless persistent.
403
+ *
404
+ * @param {?object} event Synthetic event to be dispatched.
405
+ * @private
406
+ */
407
+const executeDispatchesAndRelease = function(event: ReactSyntheticEvent) {
408
+ if (event) {
409
+ executeDispatchesInOrder(event);
410
+
411
+ if (!event.isPersistent()) {
412
+ event.constructor.release(event);
413
+ }
414
+ }
415
+};
416
+
417
+const executeDispatchesAndReleaseTopLevel = function(e) {
418
+ return executeDispatchesAndRelease(e);
419
+};
420
+
421
+function runEventsInBatch(
422
+ events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null,
423
+) {
424
+ if (events !== null) {
425
+ eventQueue = accumulateInto(eventQueue, events);
426
+ }
427
+
428
+ // Set `eventQueue` to null before processing it so that we can tell if more
429
+ // events get enqueued while processing.
430
+ const processingEventQueue = eventQueue;
431
+ eventQueue = null;
432
+
433
+ if (!processingEventQueue) {
434
+ return;
435
+ }
436
+
437
+ forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
438
+ invariant(
439
+ !eventQueue,
440
+ 'processEventQueue(): Additional events were enqueued while processing ' +
441
+ 'an event queue. Support for this has not yet been implemented.',
442
+ );
443
+ // This would be a good time to rethrow if any of the event handlers threw.
444
+ rethrowCaughtError();
445
+}
446
+
447
function isInteractive(tag) {
448
return (
449
tag === 'button' ||
packages/react-dom/src/test-utils/ReactTestUtilsAct.js
-1
@@ -26,7 +26,6 @@ const [
26
enqueueStateRestore,
27
restoreStateIfNeeded,
28
dispatchEvent,
29
- runEventsInBatch,
29
/* eslint-enable no-unused-vars */
30
flushPassiveEffects,
31
IsThisRendererActing,