From 5ceb334c31ac8eca3fc6071e6bde125ae362493b Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 6 Apr 2020 12:17:36 +0100 Subject: [PATCH] React Event System: cleanup plugins + break out update batching logic Include changes Fix bunch of issues Cleanup Fix --- packages/legacy-events/EventPluginRegistry.js | 17 +++ .../legacy-events/ReactGenericBatching.js | 59 +------- packages/react-dom/src/client/ReactDOM.js | 14 +- .../src/client/ReactDOMClientInjection.js | 84 ++++++----- .../react-dom/src/events/ChangeEventPlugin.js | 16 +- .../src/events/DOMLegacyEventPluginSystem.js | 2 +- .../src/events/DOMModernPluginEventSystem.js | 31 ++-- .../DeprecatedDOMEventResponderSystem.js | 4 +- .../events/ReactDOMControlledComponent.js} | 7 +- .../src/events/ReactDOMEventListener.js | 8 +- .../src/events/ReactDOMUpdateBatching.js | 140 ++++++++++++++++++ .../src/test-utils/ReactTestUtils.js | 92 +++++++++++- .../src/test-utils/ReactTestUtilsAct.js | 1 - 13 files changed, 346 insertions(+), 129 deletions(-) rename packages/{legacy-events/ReactControlledComponent.js => react-dom/src/events/ReactDOMControlledComponent.js} (93%) create mode 100644 packages/react-dom/src/events/ReactDOMUpdateBatching.js diff --git a/packages/legacy-events/EventPluginRegistry.js b/packages/legacy-events/EventPluginRegistry.js index 067e260313e7..f720c0af370d 100644 --- a/packages/legacy-events/EventPluginRegistry.js +++ b/packages/legacy-events/EventPluginRegistry.js @@ -241,3 +241,20 @@ export function injectEventPluginsByName( recomputePluginOrdering(); } } + +export function injectEventPlugins( + eventPlugins: [PluginModule], +): void { + for (let i = 0; i < eventPlugins.length; i++) { + const pluginModule = eventPlugins[i]; + plugins.push(pluginModule); + const publishedEvents = pluginModule.eventTypes; + for (const eventName in publishedEvents) { + publishEventForPlugin( + publishedEvents[eventName], + pluginModule, + eventName, + ); + } + } +} diff --git a/packages/legacy-events/ReactGenericBatching.js b/packages/legacy-events/ReactGenericBatching.js index f3c8d4d768fb..2681c30cba52 100644 --- a/packages/legacy-events/ReactGenericBatching.js +++ b/packages/legacy-events/ReactGenericBatching.js @@ -5,14 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import { - needsStateRestore, - restoreStateIfNeeded, -} from './ReactControlledComponent'; - -import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags'; -import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils'; - // Used as a way to call batchedUpdates when we don't have a reference to // the renderer. Such as when we're dispatching events or if third party // libraries need to call batchedUpdates. Eventually, this API will go away when @@ -32,21 +24,6 @@ let batchedEventUpdatesImpl = batchedUpdatesImpl; let isInsideEventHandler = false; let isBatchingEventUpdates = false; -function finishEventHandler() { - // Here we wait until all updates have propagated, which is important - // when using controlled components within layers: - // https://github.com/facebook/react/issues/1698 - // Then we restore state of any controlled component. - const controlledComponentsHavePendingUpdates = needsStateRestore(); - if (controlledComponentsHavePendingUpdates) { - // If a controlled event was fired, we may need to restore the state of - // the DOM node back to the controlled value. This is necessary when React - // bails out of the update without touching the DOM. - flushDiscreteUpdatesImpl(); - restoreStateIfNeeded(); - } -} - export function batchedUpdates(fn, bookkeeping) { if (isInsideEventHandler) { // If we are currently inside another batch, we need to wait until it @@ -58,7 +35,6 @@ export function batchedUpdates(fn, bookkeeping) { return batchedUpdatesImpl(fn, bookkeeping); } finally { isInsideEventHandler = false; - finishEventHandler(); } } @@ -73,19 +49,6 @@ export function batchedEventUpdates(fn, a, b) { return batchedEventUpdatesImpl(fn, a, b); } finally { isBatchingEventUpdates = false; - finishEventHandler(); - } -} - -// This is for the React Flare event system -export function executeUserEventHandler(fn: any => void, value: any): void { - const previouslyInEventHandler = isInsideEventHandler; - try { - isInsideEventHandler = true; - const type = typeof value === 'object' && value !== null ? value.type : ''; - invokeGuardedCallbackAndCatchFirstError(type, fn, undefined, value); - } finally { - isInsideEventHandler = previouslyInEventHandler; } } @@ -97,32 +60,12 @@ export function discreteUpdates(fn, a, b, c, d) { } finally { isInsideEventHandler = prevIsInsideEventHandler; if (!isInsideEventHandler) { - finishEventHandler(); } } } -let lastFlushedEventTimeStamp = 0; export function flushDiscreteUpdatesIfNeeded(timeStamp: number) { - // event.timeStamp isn't overly reliable due to inconsistencies in - // how different browsers have historically provided the time stamp. - // Some browsers provide high-resolution time stamps for all events, - // some provide low-resolution time stamps for all events. FF < 52 - // even mixes both time stamps together. Some browsers even report - // negative time stamps or time stamps that are 0 (iOS9) in some cases. - // Given we are only comparing two time stamps with equality (!==), - // we are safe from the resolution differences. If the time stamp is 0 - // we bail-out of preventing the flush, which can affect semantics, - // such as if an earlier flush removes or adds event listeners that - // are fired in the subsequent flush. However, this is the same - // behaviour as we had before this change, so the risks are low. - if ( - !isInsideEventHandler && - (!enableDeprecatedFlareAPI || - timeStamp === 0 || - lastFlushedEventTimeStamp !== timeStamp) - ) { - lastFlushedEventTimeStamp = timeStamp; + if (!isInsideEventHandler) { flushDiscreteUpdatesImpl(); } } diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 5de914e34513..0afaa882da85 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -39,13 +39,6 @@ import { } from 'react-reconciler/src/ReactFiberReconciler'; import {createPortal as createPortalImpl} from 'react-reconciler/src/ReactPortal'; import {canUseDOM} from 'shared/ExecutionEnvironment'; -import {setBatchingImplementation} from 'legacy-events/ReactGenericBatching'; -import { - setRestoreImplementation, - enqueueStateRestore, - restoreStateIfNeeded, -} from 'legacy-events/ReactControlledComponent'; -import {runEventsInBatch} from 'legacy-events/EventBatching'; import { eventNameDispatchConfigs, injectEventPluginsByName, @@ -69,6 +62,12 @@ import { setAttemptHydrationAtCurrentPriority, queueExplicitHydrationTarget, } from '../events/ReactDOMEventReplaying'; +import {setBatchingImplementation} from '../events/ReactDOMUpdateBatching'; +import { + setRestoreImplementation, + enqueueStateRestore, + restoreStateIfNeeded, +} from '../events/ReactDOMControlledComponent'; setAttemptSynchronousHydration(attemptSynchronousHydration); setAttemptUserBlockingHydration(attemptUserBlockingHydration); @@ -183,7 +182,6 @@ const Internals = { enqueueStateRestore, restoreStateIfNeeded, dispatchEvent, - runEventsInBatch, flushPassiveEffects, IsThisRendererActing, ], diff --git a/packages/react-dom/src/client/ReactDOMClientInjection.js b/packages/react-dom/src/client/ReactDOMClientInjection.js index 4c0ba0d14f93..a3608b85dac4 100644 --- a/packages/react-dom/src/client/ReactDOMClientInjection.js +++ b/packages/react-dom/src/client/ReactDOMClientInjection.js @@ -20,43 +20,55 @@ import SimpleEventPlugin from '../events/SimpleEventPlugin'; import { injectEventPluginOrder, injectEventPluginsByName, + injectEventPlugins, } from 'legacy-events/EventPluginRegistry'; +import {enableModernEventSystem} from 'shared/ReactFeatureFlags'; -/** - * Specifies a deterministic ordering of `EventPlugin`s. A convenient way to - * reason about plugins, without having to package every one of them. This - * is better than having plugins be ordered in the same order that they - * are injected because that ordering would be influenced by the packaging order. - * `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that - * preventing default on events is convenient in `SimpleEventPlugin` handlers. - */ -const DOMEventPluginOrder = [ - 'ResponderEventPlugin', - 'SimpleEventPlugin', - 'EnterLeaveEventPlugin', - 'ChangeEventPlugin', - 'SelectEventPlugin', - 'BeforeInputEventPlugin', -]; +if (enableModernEventSystem) { + injectEventPlugins([ + SimpleEventPlugin, + EnterLeaveEventPlugin, + ChangeEventPlugin, + SelectEventPlugin, + BeforeInputEventPlugin, + ]); +} else { + /** + * Specifies a deterministic ordering of `EventPlugin`s. A convenient way to + * reason about plugins, without having to package every one of them. This + * is better than having plugins be ordered in the same order that they + * are injected because that ordering would be influenced by the packaging order. + * `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that + * preventing default on events is convenient in `SimpleEventPlugin` handlers. + */ + const DOMEventPluginOrder = [ + 'ResponderEventPlugin', + 'SimpleEventPlugin', + 'EnterLeaveEventPlugin', + 'ChangeEventPlugin', + 'SelectEventPlugin', + 'BeforeInputEventPlugin', + ]; -/** - * Inject modules for resolving DOM hierarchy and plugin ordering. - */ -injectEventPluginOrder(DOMEventPluginOrder); -setComponentTree( - getFiberCurrentPropsFromNode, - getInstanceFromNode, - getNodeFromInstance, -); + /** + * Inject modules for resolving DOM hierarchy and plugin ordering. + */ + injectEventPluginOrder(DOMEventPluginOrder); + setComponentTree( + getFiberCurrentPropsFromNode, + getInstanceFromNode, + getNodeFromInstance, + ); -/** - * Some important event plugins included by default (without having to require - * them). - */ -injectEventPluginsByName({ - SimpleEventPlugin: SimpleEventPlugin, - EnterLeaveEventPlugin: EnterLeaveEventPlugin, - ChangeEventPlugin: ChangeEventPlugin, - SelectEventPlugin: SelectEventPlugin, - BeforeInputEventPlugin: BeforeInputEventPlugin, -}); + /** + * Some important event plugins included by default (without having to require + * them). + */ + injectEventPluginsByName({ + SimpleEventPlugin: SimpleEventPlugin, + EnterLeaveEventPlugin: EnterLeaveEventPlugin, + ChangeEventPlugin: ChangeEventPlugin, + SelectEventPlugin: SelectEventPlugin, + BeforeInputEventPlugin: BeforeInputEventPlugin, + }); +} diff --git a/packages/react-dom/src/events/ChangeEventPlugin.js b/packages/react-dom/src/events/ChangeEventPlugin.js index 372af140fba0..11a3fec191c0 100644 --- a/packages/react-dom/src/events/ChangeEventPlugin.js +++ b/packages/react-dom/src/events/ChangeEventPlugin.js @@ -6,8 +6,6 @@ */ import {runEventsInBatch} from 'legacy-events/EventBatching'; -import {enqueueStateRestore} from 'legacy-events/ReactControlledComponent'; -import {batchedUpdates} from 'legacy-events/ReactGenericBatching'; import SyntheticEvent from 'legacy-events/SyntheticEvent'; import isTextInputElement from './isTextInputElement'; import {canUseDOM} from 'shared/ExecutionEnvironment'; @@ -27,9 +25,15 @@ import isEventSupported from './isEventSupported'; import {getNodeFromInstance} from '../client/ReactDOMComponentTree'; import {updateValueIfChanged} from '../client/inputValueTracking'; import {setDefaultValue} from '../client/ReactDOMInput'; +import {enqueueStateRestore} from './ReactDOMControlledComponent'; -import {disableInputAttributeSyncing} from 'shared/ReactFeatureFlags'; +import { + disableInputAttributeSyncing, + enableModernEventSystem, +} from 'shared/ReactFeatureFlags'; import accumulateTwoPhaseListeners from './accumulateTwoPhaseListeners'; +import {batchedUpdates} from './ReactDOMUpdateBatching'; +import {dispatchEventsInBatch} from './DOMModernPluginEventSystem'; const eventTypes = { change: { @@ -101,7 +105,11 @@ function manualDispatchChangeEvent(nativeEvent) { } function runEventInBatch(event) { - runEventsInBatch(event); + if (enableModernEventSystem) { + dispatchEventsInBatch([event]); + } else { + runEventsInBatch(event); + } } function getInstIfValueChanged(targetInst) { diff --git a/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js b/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js index e1e554a69a11..080f5545236e 100644 --- a/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js +++ b/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js @@ -22,7 +22,6 @@ import { HostText, } from 'react-reconciler/src/ReactWorkTags'; import {IS_FIRST_ANCESTOR, PLUGIN_EVENT_SYSTEM} from './EventSystemFlags'; -import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching'; import {runEventsInBatch} from 'legacy-events/EventBatching'; import {plugins} from 'legacy-events/EventPluginRegistry'; import accumulateInto from 'legacy-events/accumulateInto'; @@ -45,6 +44,7 @@ import { mediaEventTypes, } from './DOMTopLevelEventTypes'; import {addTrappedEventListener} from './ReactDOMEventListener'; +import {batchedEventUpdates} from './ReactDOMUpdateBatching'; /** * Summary of `DOMEventPluginSystem` event handling: diff --git a/packages/react-dom/src/events/DOMModernPluginEventSystem.js b/packages/react-dom/src/events/DOMModernPluginEventSystem.js index 3bc74b86d97f..fb1155c8d5b1 100644 --- a/packages/react-dom/src/events/DOMModernPluginEventSystem.js +++ b/packages/react-dom/src/events/DOMModernPluginEventSystem.js @@ -24,7 +24,6 @@ import type { import type {ReactDOMListener} from '../shared/ReactDOMTypes'; import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry'; -import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching'; import {plugins} from 'legacy-events/EventPluginRegistry'; import { PLUGIN_EVENT_SYSTEM, @@ -86,12 +85,16 @@ import { } from '../client/ReactDOMComponentTree'; import {COMMENT_NODE} from '../shared/HTMLNodeType'; import {topLevelEventsToDispatchConfig} from './DOMEventProperties'; +import {batchedEventUpdates} from './ReactDOMUpdateBatching'; import { enableLegacyFBSupport, enableUseEventAPI, } from 'shared/ReactFeatureFlags'; -import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils'; +import { + invokeGuardedCallbackAndCatchFirstError, + rethrowCaughtError, +} from 'shared/ReactErrorUtils'; const capturePhaseEvents = new Set([ TOP_FOCUS, @@ -213,6 +216,21 @@ function executeDispatchesInOrder(event: ReactSyntheticEvent): void { event._dispatchCurrentTargets = null; } +export function dispatchEventsInBatch( + events: Array, +): void { + for (let i = 0; i < events.length; i++) { + const syntheticEvent = events[i]; + executeDispatchesInOrder(syntheticEvent); + // Release the event from the pool if needed + if (!syntheticEvent.isPersistent()) { + syntheticEvent.constructor.release(syntheticEvent); + } + } + // This would be a good time to rethrow if any of the event handlers threw. + rethrowCaughtError(); +} + function dispatchEventsForPlugins( topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, @@ -244,14 +262,7 @@ function dispatchEventsForPlugins( } } } - for (let i = 0; i < syntheticEvents.length; i++) { - const syntheticEvent = syntheticEvents[i]; - executeDispatchesInOrder(syntheticEvent); - // Release the event from the pool if needed - if (!syntheticEvent.isPersistent()) { - syntheticEvent.constructor.release(syntheticEvent); - } - } + dispatchEventsInBatch(syntheticEvents); } function shouldUpgradeListener( diff --git a/packages/react-dom/src/events/DeprecatedDOMEventResponderSystem.js b/packages/react-dom/src/events/DeprecatedDOMEventResponderSystem.js index 47eb5ca68f09..8194aa3fb69d 100644 --- a/packages/react-dom/src/events/DeprecatedDOMEventResponderSystem.js +++ b/packages/react-dom/src/events/DeprecatedDOMEventResponderSystem.js @@ -30,13 +30,13 @@ import { discreteUpdates, flushDiscreteUpdatesIfNeeded, executeUserEventHandler, -} from 'legacy-events/ReactGenericBatching'; -import {enqueueStateRestore} from 'legacy-events/ReactControlledComponent'; +} from './ReactDOMUpdateBatching'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; +import {enqueueStateRestore} from './ReactDOMControlledComponent'; import { ContinuousEvent, UserBlockingEvent, diff --git a/packages/legacy-events/ReactControlledComponent.js b/packages/react-dom/src/events/ReactDOMControlledComponent.js similarity index 93% rename from packages/legacy-events/ReactControlledComponent.js rename to packages/react-dom/src/events/ReactDOMControlledComponent.js index 830f2a1b1cf4..5b4dc5923290 100644 --- a/packages/legacy-events/ReactControlledComponent.js +++ b/packages/react-dom/src/events/ReactDOMControlledComponent.js @@ -8,11 +8,10 @@ */ import invariant from 'shared/invariant'; - import { getInstanceFromNode, getFiberCurrentPropsFromNode, -} from './EventPluginUtils'; +} from '../client/ReactDOMComponentTree'; // Use to restore controlled state after a change event has fired. @@ -20,7 +19,7 @@ let restoreImpl = null; let restoreTarget = null; let restoreQueue = null; -function restoreStateOfTarget(target) { +function restoreStateOfTarget(target: Node) { // We perform this translation at the end of the event loop so that we // always receive the correct fiber here const internalInstance = getInstanceFromNode(target); @@ -47,7 +46,7 @@ export function setRestoreImplementation( restoreImpl = impl; } -export function enqueueStateRestore(target: EventTarget): void { +export function enqueueStateRestore(target: Node): void { if (restoreTarget) { if (restoreQueue) { restoreQueue.push(target); diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index af3d59da55c9..8db675c6707b 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -17,10 +17,6 @@ import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; // CommonJS interop named imports. import * as Scheduler from 'scheduler'; -import { - discreteUpdates, - flushDiscreteUpdatesIfNeeded, -} from 'legacy-events/ReactGenericBatching'; import {DEPRECATED_dispatchEventForResponderEventSystem} from './DeprecatedDOMEventResponderSystem'; import { isReplayableDiscreteEvent, @@ -70,6 +66,10 @@ import { import {getEventPriorityForPluginSystem} from './DOMEventProperties'; import {dispatchEventForLegacyPluginEventSystem} from './DOMLegacyEventPluginSystem'; import {dispatchEventForPluginEventSystem} from './DOMModernPluginEventSystem'; +import { + flushDiscreteUpdatesIfNeeded, + discreteUpdates, +} from './ReactDOMUpdateBatching'; const { unstable_UserBlockingPriority: UserBlockingPriority, diff --git a/packages/react-dom/src/events/ReactDOMUpdateBatching.js b/packages/react-dom/src/events/ReactDOMUpdateBatching.js new file mode 100644 index 000000000000..b4fbc9d1d84f --- /dev/null +++ b/packages/react-dom/src/events/ReactDOMUpdateBatching.js @@ -0,0 +1,140 @@ +/** + * 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. + */ + +import { + needsStateRestore, + restoreStateIfNeeded, +} from './ReactDOMControlledComponent'; + +import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags'; +import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils'; + +// Used as a way to call batchedUpdates when we don't have a reference to +// the renderer. Such as when we're dispatching events or if third party +// libraries need to call batchedUpdates. Eventually, this API will go away when +// everything is batched by default. We'll then have a similar API to opt-out of +// scheduled work and instead do synchronous work. + +// Defaults +let batchedUpdatesImpl = function(fn, bookkeeping) { + return fn(bookkeeping); +}; +let discreteUpdatesImpl = function(fn, a, b, c, d) { + return fn(a, b, c, d); +}; +let flushDiscreteUpdatesImpl = function() {}; +let batchedEventUpdatesImpl = batchedUpdatesImpl; + +let isInsideEventHandler = false; +let isBatchingEventUpdates = false; + +function finishEventHandler() { + // Here we wait until all updates have propagated, which is important + // when using controlled components within layers: + // https://github.com/facebook/react/issues/1698 + // Then we restore state of any controlled component. + const controlledComponentsHavePendingUpdates = needsStateRestore(); + if (controlledComponentsHavePendingUpdates) { + // If a controlled event was fired, we may need to restore the state of + // the DOM node back to the controlled value. This is necessary when React + // bails out of the update without touching the DOM. + flushDiscreteUpdatesImpl(); + restoreStateIfNeeded(); + } +} + +export function batchedUpdates(fn, bookkeeping) { + if (isInsideEventHandler) { + // If we are currently inside another batch, we need to wait until it + // fully completes before restoring state. + return fn(bookkeeping); + } + isInsideEventHandler = true; + try { + return batchedUpdatesImpl(fn, bookkeeping); + } finally { + isInsideEventHandler = false; + finishEventHandler(); + } +} + +export function batchedEventUpdates(fn, a, b) { + if (isBatchingEventUpdates) { + // If we are currently inside another batch, we need to wait until it + // fully completes before restoring state. + return fn(a, b); + } + isBatchingEventUpdates = true; + try { + return batchedEventUpdatesImpl(fn, a, b); + } finally { + isBatchingEventUpdates = false; + finishEventHandler(); + } +} + +// This is for the React Flare event system +export function executeUserEventHandler(fn: any => void, value: any): void { + const previouslyInEventHandler = isInsideEventHandler; + try { + isInsideEventHandler = true; + const type = typeof value === 'object' && value !== null ? value.type : ''; + invokeGuardedCallbackAndCatchFirstError(type, fn, undefined, value); + } finally { + isInsideEventHandler = previouslyInEventHandler; + } +} + +export function discreteUpdates(fn, a, b, c, d) { + const prevIsInsideEventHandler = isInsideEventHandler; + isInsideEventHandler = true; + try { + return discreteUpdatesImpl(fn, a, b, c, d); + } finally { + isInsideEventHandler = prevIsInsideEventHandler; + if (!isInsideEventHandler) { + finishEventHandler(); + } + } +} + +let lastFlushedEventTimeStamp = 0; +export function flushDiscreteUpdatesIfNeeded(timeStamp: number) { + // event.timeStamp isn't overly reliable due to inconsistencies in + // how different browsers have historically provided the time stamp. + // Some browsers provide high-resolution time stamps for all events, + // some provide low-resolution time stamps for all events. FF < 52 + // even mixes both time stamps together. Some browsers even report + // negative time stamps or time stamps that are 0 (iOS9) in some cases. + // Given we are only comparing two time stamps with equality (!==), + // we are safe from the resolution differences. If the time stamp is 0 + // we bail-out of preventing the flush, which can affect semantics, + // such as if an earlier flush removes or adds event listeners that + // are fired in the subsequent flush. However, this is the same + // behaviour as we had before this change, so the risks are low. + if ( + !isInsideEventHandler && + (!enableDeprecatedFlareAPI || + timeStamp === 0 || + lastFlushedEventTimeStamp !== timeStamp) + ) { + lastFlushedEventTimeStamp = timeStamp; + flushDiscreteUpdatesImpl(); + } +} + +export function setBatchingImplementation( + _batchedUpdatesImpl, + _discreteUpdatesImpl, + _flushDiscreteUpdatesImpl, + _batchedEventUpdatesImpl, +) { + batchedUpdatesImpl = _batchedUpdatesImpl; + discreteUpdatesImpl = _discreteUpdatesImpl; + flushDiscreteUpdatesImpl = _flushDiscreteUpdatesImpl; + batchedEventUpdatesImpl = _batchedEventUpdatesImpl; +} diff --git a/packages/react-dom/src/test-utils/ReactTestUtils.js b/packages/react-dom/src/test-utils/ReactTestUtils.js index 2348a047ba9f..b751ce1362bd 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtils.js +++ b/packages/react-dom/src/test-utils/ReactTestUtils.js @@ -23,6 +23,10 @@ import act from './ReactTestUtilsAct'; import forEachAccumulated from 'legacy-events/forEachAccumulated'; import accumulateInto from 'legacy-events/accumulateInto'; import {enableModernEventSystem} from 'shared/ReactFeatureFlags'; +import { + rethrowCaughtError, + invokeGuardedCallbackAndCatchFirstError, +} from 'shared/ReactErrorUtils'; const {findDOMNode} = ReactDOM; // Keep in sync with ReactDOMUnstableNativeDependencies.js @@ -38,7 +42,6 @@ const [ enqueueStateRestore, restoreStateIfNeeded, dispatchEvent, - runEventsInBatch, /* eslint-disable no-unused-vars */ flushPassiveEffects, IsThisRendererActing, @@ -354,6 +357,93 @@ function nativeTouchData(x, y) { // Start of inline: the below functions were inlined from // EventPropagator.js, as they deviated from ReactDOM's newer // implementations. + +/** + * Dispatch the event to the listener. + * @param {SyntheticEvent} event SyntheticEvent to handle + * @param {function} listener Application-level callback + * @param {*} inst Internal component instance + */ +function executeDispatch(event, listener, inst) { + const type = event.type || 'unknown-event'; + event.currentTarget = getNodeFromInstance(inst); + invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event); + event.currentTarget = null; +} + +/** + * Standard/simple iteration through an event's collected dispatches. + */ +function executeDispatchesInOrder(event) { + const dispatchListeners = event._dispatchListeners; + const dispatchInstances = event._dispatchInstances; + if (Array.isArray(dispatchListeners)) { + for (let i = 0; i < dispatchListeners.length; i++) { + if (event.isPropagationStopped()) { + break; + } + // Listeners and Instances are two parallel arrays that are always in sync. + executeDispatch(event, dispatchListeners[i], dispatchInstances[i]); + } + } else if (dispatchListeners) { + executeDispatch(event, dispatchListeners, dispatchInstances); + } + event._dispatchListeners = null; + event._dispatchInstances = null; +} + +/** + * Internal queue of events that have accumulated their dispatches and are + * waiting to have their dispatches executed. + */ +let eventQueue: ?(Array | ReactSyntheticEvent) = null; + +/** + * Dispatches an event and releases it back into the pool, unless persistent. + * + * @param {?object} event Synthetic event to be dispatched. + * @private + */ +const executeDispatchesAndRelease = function(event: ReactSyntheticEvent) { + if (event) { + executeDispatchesInOrder(event); + + if (!event.isPersistent()) { + event.constructor.release(event); + } + } +}; + +const executeDispatchesAndReleaseTopLevel = function(e) { + return executeDispatchesAndRelease(e); +}; + +function runEventsInBatch( + events: Array | ReactSyntheticEvent | null, +) { + if (events !== null) { + eventQueue = accumulateInto(eventQueue, events); + } + + // Set `eventQueue` to null before processing it so that we can tell if more + // events get enqueued while processing. + const processingEventQueue = eventQueue; + eventQueue = null; + + if (!processingEventQueue) { + return; + } + + forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel); + invariant( + !eventQueue, + 'processEventQueue(): Additional events were enqueued while processing ' + + 'an event queue. Support for this has not yet been implemented.', + ); + // This would be a good time to rethrow if any of the event handlers threw. + rethrowCaughtError(); +} + function isInteractive(tag) { return ( tag === 'button' || diff --git a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js index c1bab1c869c3..e50595fae9b8 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js +++ b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js @@ -26,7 +26,6 @@ const [ enqueueStateRestore, restoreStateIfNeeded, dispatchEvent, - runEventsInBatch, /* eslint-enable no-unused-vars */ flushPassiveEffects, IsThisRendererActing,