From 9def56ec0e1e71928ee999f48c00b1803ed8772a Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 14 Feb 2020 08:10:42 +0000 Subject: [PATCH] Refactor DOM plugin system to single module (#18025) --- packages/legacy-events/EventPluginHub.js | 176 -------------- packages/legacy-events/EventPluginRegistry.js | 10 +- packages/legacy-events/EventPropagators.js | 2 +- .../legacy-events/ResponderEventPlugin.js | 16 +- .../EventPluginRegistry-test.internal.js | 2 +- packages/legacy-events/getListener.js | 74 ++++++ ...-test.js => InvalidEventListeners-test.js} | 2 +- .../ReactBrowserEventEmitter-test.internal.js | 6 +- packages/react-dom/src/client/ReactDOM.js | 8 +- .../src/client/ReactDOMClientInjection.js | 27 ++- .../src/events/DOMEventPluginOrder.js | 26 --- .../src/events/DOMEventPluginSystem.js | 215 ++++++++++++++++++ .../src/events/ReactBrowserEventEmitter.js | 8 +- .../src/events/ReactDOMEventListener.js | 150 +----------- .../src/ReactFabricEventEmitter.js | 66 +++++- .../src/ReactNativeBridgeEventPlugin.js | 3 - .../src/ReactNativeEventEmitter.js | 69 +++++- .../src/ReactNativeInjectionShared.js | 9 +- scripts/error-codes/codes.json | 4 +- 19 files changed, 470 insertions(+), 403 deletions(-) delete mode 100644 packages/legacy-events/EventPluginHub.js create mode 100644 packages/legacy-events/getListener.js rename packages/react-dom/src/__tests__/{EventPluginHub-test.js => InvalidEventListeners-test.js} (96%) delete mode 100644 packages/react-dom/src/events/DOMEventPluginOrder.js create mode 100644 packages/react-dom/src/events/DOMEventPluginSystem.js diff --git a/packages/legacy-events/EventPluginHub.js b/packages/legacy-events/EventPluginHub.js deleted file mode 100644 index 407946023028a..0000000000000 --- a/packages/legacy-events/EventPluginHub.js +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * @flow - */ - -import invariant from 'shared/invariant'; - -import { - injectEventPluginOrder, - injectEventPluginsByName, - plugins, -} from './EventPluginRegistry'; -import {getFiberCurrentPropsFromNode} from './EventPluginUtils'; -import accumulateInto from './accumulateInto'; -import {runEventsInBatch} from './EventBatching'; - -import type {PluginModule} from './PluginModuleType'; -import type {ReactSyntheticEvent} from './ReactSyntheticEventType'; -import type {Fiber} from 'react-reconciler/src/ReactFiber'; -import type {AnyNativeEvent} from './PluginModuleType'; -import type {TopLevelType} from './TopLevelEventTypes'; -import type {EventSystemFlags} from 'legacy-events/EventSystemFlags'; - -function isInteractive(tag) { - return ( - tag === 'button' || - tag === 'input' || - tag === 'select' || - tag === 'textarea' - ); -} - -function shouldPreventMouseEvent(name, type, props) { - switch (name) { - case 'onClick': - case 'onClickCapture': - case 'onDoubleClick': - case 'onDoubleClickCapture': - case 'onMouseDown': - case 'onMouseDownCapture': - case 'onMouseMove': - case 'onMouseMoveCapture': - case 'onMouseUp': - case 'onMouseUpCapture': - case 'onMouseEnter': - return !!(props.disabled && isInteractive(type)); - default: - return false; - } -} - -/** - * This is a unified interface for event plugins to be installed and configured. - * - * Event plugins can implement the following properties: - * - * `extractEvents` {function(string, DOMEventTarget, string, object): *} - * Required. When a top-level event is fired, this method is expected to - * extract synthetic events that will in turn be queued and dispatched. - * - * `eventTypes` {object} - * Optional, plugins that fire events must publish a mapping of registration - * names that are used to register listeners. Values of this mapping must - * be objects that contain `registrationName` or `phasedRegistrationNames`. - * - * `executeDispatch` {function(object, function, string)} - * Optional, allows plugins to override how an event gets dispatched. By - * default, the listener is simply invoked. - * - * Each plugin that is injected into `EventsPluginHub` is immediately operable. - * - * @public - */ - -/** - * Methods for injecting dependencies. - */ -export const injection = { - /** - * @param {array} InjectedEventPluginOrder - * @public - */ - injectEventPluginOrder, - - /** - * @param {object} injectedNamesToPlugins Map from names to plugin modules. - */ - injectEventPluginsByName, -}; - -/** - * @param {object} inst The instance, which is the source of events. - * @param {string} registrationName Name of listener (e.g. `onClick`). - * @return {?function} The stored callback. - */ -export function getListener(inst: Fiber, registrationName: string) { - let listener; - - // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not - // live here; needs to be moved to a better place soon - const stateNode = inst.stateNode; - if (!stateNode) { - // Work in progress (ex: onload events in incremental mode). - return null; - } - const props = getFiberCurrentPropsFromNode(stateNode); - if (!props) { - // Work in progress. - return null; - } - listener = props[registrationName]; - if (shouldPreventMouseEvent(registrationName, inst.type, props)) { - return null; - } - invariant( - !listener || typeof listener === 'function', - 'Expected `%s` listener to be a function, instead got a value of `%s` type.', - registrationName, - typeof listener, - ); - return listener; -} - -/** - * Allows registered plugins an opportunity to extract events from top-level - * native browser events. - * - * @return {*} An accumulation of synthetic events. - * @internal - */ -function extractPluginEvents( - topLevelType: TopLevelType, - targetInst: null | Fiber, - nativeEvent: AnyNativeEvent, - nativeEventTarget: null | EventTarget, - eventSystemFlags: EventSystemFlags, -): Array | ReactSyntheticEvent | null { - let events = null; - for (let i = 0; i < plugins.length; i++) { - // Not every plugin in the ordering may be loaded at runtime. - const possiblePlugin: PluginModule = plugins[i]; - if (possiblePlugin) { - const extractedEvents = possiblePlugin.extractEvents( - topLevelType, - targetInst, - nativeEvent, - nativeEventTarget, - eventSystemFlags, - ); - if (extractedEvents) { - events = accumulateInto(events, extractedEvents); - } - } - } - return events; -} - -export function runExtractedPluginEventsInBatch( - topLevelType: TopLevelType, - targetInst: null | Fiber, - nativeEvent: AnyNativeEvent, - nativeEventTarget: null | EventTarget, - eventSystemFlags: EventSystemFlags, -) { - const events = extractPluginEvents( - topLevelType, - targetInst, - nativeEvent, - nativeEventTarget, - eventSystemFlags, - ); - runEventsInBatch(events); -} diff --git a/packages/legacy-events/EventPluginRegistry.js b/packages/legacy-events/EventPluginRegistry.js index 64c2a9a1e03e8..067e260313e73 100644 --- a/packages/legacy-events/EventPluginRegistry.js +++ b/packages/legacy-events/EventPluginRegistry.js @@ -89,7 +89,7 @@ function publishEventForPlugin( ): boolean { invariant( !eventNameDispatchConfigs.hasOwnProperty(eventName), - 'EventPluginHub: More than one plugin attempted to publish the same ' + + 'EventPluginRegistry: More than one plugin attempted to publish the same ' + 'event name, `%s`.', eventName, ); @@ -133,7 +133,7 @@ function publishRegistrationName( ): void { invariant( !registrationNameModules[registrationName], - 'EventPluginHub: More than one plugin attempted to publish the same ' + + 'EventPluginRegistry: More than one plugin attempted to publish the same ' + 'registration name, `%s`.', registrationName, ); @@ -153,8 +153,6 @@ function publishRegistrationName( /** * Registers plugins so that they can extract and dispatch events. - * - * @see {EventPluginHub} */ /** @@ -193,7 +191,6 @@ export const possibleRegistrationNames = __DEV__ ? {} : (null: any); * * @param {array} InjectedEventPluginOrder * @internal - * @see {EventPluginHub.injection.injectEventPluginOrder} */ export function injectEventPluginOrder( injectedEventPluginOrder: EventPluginOrder, @@ -209,14 +206,13 @@ export function injectEventPluginOrder( } /** - * Injects plugins to be used by `EventPluginHub`. The plugin names must be + * Injects plugins to be used by plugin event system. The plugin names must be * in the ordering injected by `injectEventPluginOrder`. * * Plugins can be injected as part of page initialization or on-the-fly. * * @param {object} injectedNamesToPlugins Map from names to plugin modules. * @internal - * @see {EventPluginHub.injection.injectEventPluginsByName} */ export function injectEventPluginsByName( injectedNamesToPlugins: NamesToPlugins, diff --git a/packages/legacy-events/EventPropagators.js b/packages/legacy-events/EventPropagators.js index dfaacdab4ddfc..d916de99a3b4e 100644 --- a/packages/legacy-events/EventPropagators.js +++ b/packages/legacy-events/EventPropagators.js @@ -11,7 +11,7 @@ import { traverseEnterLeave, } from 'shared/ReactTreeTraversal'; -import {getListener} from './EventPluginHub'; +import getListener from 'legacy-events/getListener'; import accumulateInto from './accumulateInto'; import forEachAccumulated from './forEachAccumulated'; diff --git a/packages/legacy-events/ResponderEventPlugin.js b/packages/legacy-events/ResponderEventPlugin.js index a5d4fe0251e53..a1e9ae7f0affa 100644 --- a/packages/legacy-events/ResponderEventPlugin.js +++ b/packages/legacy-events/ResponderEventPlugin.js @@ -282,7 +282,7 @@ to return true:wantsResponderID| | + + */ /** - * A note about event ordering in the `EventPluginHub`. + * A note about event ordering in the `EventPluginRegistry`. * * Suppose plugins are injected in the following order: * @@ -301,7 +301,7 @@ to return true:wantsResponderID| | * - When returned from `extractEvents`, deferred-dispatched events contain an * "accumulation" of deferred dispatches. * - These deferred dispatches are accumulated/collected before they are - * returned, but processed at a later time by the `EventPluginHub` (hence the + * returned, but processed at a later time by the `EventPluginRegistry` (hence the * name deferred). * * In the process of returning their deferred-dispatched events, event plugins @@ -325,9 +325,9 @@ to return true:wantsResponderID| | * - `R`s on-demand events (if any) (dispatched by `R` on-demand) * - `S`s on-demand events (if any) (dispatched by `S` on-demand) * - `C`s on-demand events (if any) (dispatched by `C` on-demand) - * - `R`s extracted events (if any) (dispatched by `EventPluginHub`) - * - `S`s extracted events (if any) (dispatched by `EventPluginHub`) - * - `C`s extracted events (if any) (dispatched by `EventPluginHub`) + * - `R`s extracted events (if any) (dispatched by `EventPluginRegistry`) + * - `S`s extracted events (if any) (dispatched by `EventPluginRegistry`) + * - `C`s extracted events (if any) (dispatched by `EventPluginRegistry`) * * In the case of `ResponderEventPlugin`: If the `startShouldSetResponder` * on-demand dispatch returns `true` (and some other details are satisfied) the @@ -336,9 +336,9 @@ to return true:wantsResponderID| | * will appear as follows: * * - `startShouldSetResponder` (`ResponderEventPlugin` dispatches on-demand) - * - `touchStartCapture` (`EventPluginHub` dispatches as usual) - * - `touchStart` (`EventPluginHub` dispatches as usual) - * - `responderGrant/Reject` (`EventPluginHub` dispatches as usual) + * - `touchStartCapture` (`EventPluginRegistry` dispatches as usual) + * - `touchStart` (`EventPluginRegistry` dispatches as usual) + * - `responderGrant/Reject` (`EventPluginRegistry` dispatches as usual) */ function setResponderAndExtractTransfer( diff --git a/packages/legacy-events/__tests__/EventPluginRegistry-test.internal.js b/packages/legacy-events/__tests__/EventPluginRegistry-test.internal.js index c56f2e5748166..2f30399cd8f0a 100644 --- a/packages/legacy-events/__tests__/EventPluginRegistry-test.internal.js +++ b/packages/legacy-events/__tests__/EventPluginRegistry-test.internal.js @@ -211,7 +211,7 @@ describe('EventPluginRegistry', () => { expect(function() { EventPluginRegistry.injectEventPluginOrder(['one', 'two']); }).toThrowError( - 'EventPluginHub: More than one plugin attempted to publish the same ' + + 'EventPluginRegistry: More than one plugin attempted to publish the same ' + 'registration name, `onPhotoCapture`.', ); }); diff --git a/packages/legacy-events/getListener.js b/packages/legacy-events/getListener.js new file mode 100644 index 0000000000000..8ef609c7b3913 --- /dev/null +++ b/packages/legacy-events/getListener.js @@ -0,0 +1,74 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @flow + */ + +import type {Fiber} from 'react-reconciler/src/ReactFiber'; + +import invariant from 'shared/invariant'; + +import {getFiberCurrentPropsFromNode} from './EventPluginUtils'; + +function isInteractive(tag) { + return ( + tag === 'button' || + tag === 'input' || + tag === 'select' || + tag === 'textarea' + ); +} + +function shouldPreventMouseEvent(name, type, props) { + switch (name) { + case 'onClick': + case 'onClickCapture': + case 'onDoubleClick': + case 'onDoubleClickCapture': + case 'onMouseDown': + case 'onMouseDownCapture': + case 'onMouseMove': + case 'onMouseMoveCapture': + case 'onMouseUp': + case 'onMouseUpCapture': + case 'onMouseEnter': + return !!(props.disabled && isInteractive(type)); + default: + return false; + } +} + +/** + * @param {object} inst The instance, which is the source of events. + * @param {string} registrationName Name of listener (e.g. `onClick`). + * @return {?function} The stored callback. + */ +export default function getListener(inst: Fiber, registrationName: string) { + let listener; + + // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not + // live here; needs to be moved to a better place soon + const stateNode = inst.stateNode; + if (!stateNode) { + // Work in progress (ex: onload events in incremental mode). + return null; + } + const props = getFiberCurrentPropsFromNode(stateNode); + if (!props) { + // Work in progress. + return null; + } + listener = props[registrationName]; + if (shouldPreventMouseEvent(registrationName, inst.type, props)) { + return null; + } + invariant( + !listener || typeof listener === 'function', + 'Expected `%s` listener to be a function, instead got a value of `%s` type.', + registrationName, + typeof listener, + ); + return listener; +} diff --git a/packages/react-dom/src/__tests__/EventPluginHub-test.js b/packages/react-dom/src/__tests__/InvalidEventListeners-test.js similarity index 96% rename from packages/react-dom/src/__tests__/EventPluginHub-test.js rename to packages/react-dom/src/__tests__/InvalidEventListeners-test.js index df7859fa6b175..dc5f0e694fe1e 100644 --- a/packages/react-dom/src/__tests__/EventPluginHub-test.js +++ b/packages/react-dom/src/__tests__/InvalidEventListeners-test.js @@ -11,7 +11,7 @@ jest.mock('../events/isEventSupported'); -describe('EventPluginHub', () => { +describe('InvalidEventListeners', () => { let React; let ReactTestUtils; diff --git a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js index 57348fa556da1..114565d342f9a 100644 --- a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js @@ -9,7 +9,7 @@ 'use strict'; -let EventPluginHub; +let EventPluginGetListener; let EventPluginRegistry; let React; let ReactDOM; @@ -58,7 +58,7 @@ describe('ReactBrowserEventEmitter', () => { LISTENER.mockClear(); // TODO: can we express this test with only public API? - EventPluginHub = require('legacy-events/EventPluginHub'); + EventPluginGetListener = require('legacy-events/getListener').default; EventPluginRegistry = require('legacy-events/EventPluginRegistry'); React = require('react'); ReactDOM = require('react-dom'); @@ -100,7 +100,7 @@ describe('ReactBrowserEventEmitter', () => { getListener = function(node, eventName) { const inst = ReactDOMComponentTree.getInstanceFromNode(node); - return EventPluginHub.getListener(inst, eventName); + return EventPluginGetListener(inst, eventName); }; putListener = function(node, eventName, listener) { switch (node) { diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index f542aa752b1f5..9983f1418c3a3 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -45,9 +45,11 @@ import { enqueueStateRestore, restoreStateIfNeeded, } from 'legacy-events/ReactControlledComponent'; -import {injection as EventPluginHubInjection} from 'legacy-events/EventPluginHub'; import {runEventsInBatch} from 'legacy-events/EventBatching'; -import {eventNameDispatchConfigs} from 'legacy-events/EventPluginRegistry'; +import { + eventNameDispatchConfigs, + injectEventPluginsByName, +} from 'legacy-events/EventPluginRegistry'; import { accumulateTwoPhaseDispatches, accumulateDirectDispatches, @@ -145,7 +147,7 @@ const ReactDOM: Object = { getInstanceFromNode, getNodeFromInstance, getFiberCurrentPropsFromNode, - EventPluginHubInjection.injectEventPluginsByName, + injectEventPluginsByName, eventNameDispatchConfigs, accumulateTwoPhaseDispatches, accumulateDirectDispatches, diff --git a/packages/react-dom/src/client/ReactDOMClientInjection.js b/packages/react-dom/src/client/ReactDOMClientInjection.js index 604c3ff976bb8..4c0ba0d14f939 100644 --- a/packages/react-dom/src/client/ReactDOMClientInjection.js +++ b/packages/react-dom/src/client/ReactDOMClientInjection.js @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import {injection as EventPluginHubInjection} from 'legacy-events/EventPluginHub'; import {setComponentTree} from 'legacy-events/EventPluginUtils'; import { @@ -15,15 +14,35 @@ import { } from './ReactDOMComponentTree'; import BeforeInputEventPlugin from '../events/BeforeInputEventPlugin'; import ChangeEventPlugin from '../events/ChangeEventPlugin'; -import DOMEventPluginOrder from '../events/DOMEventPluginOrder'; import EnterLeaveEventPlugin from '../events/EnterLeaveEventPlugin'; import SelectEventPlugin from '../events/SelectEventPlugin'; import SimpleEventPlugin from '../events/SimpleEventPlugin'; +import { + injectEventPluginOrder, + injectEventPluginsByName, +} from 'legacy-events/EventPluginRegistry'; + +/** + * 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. */ -EventPluginHubInjection.injectEventPluginOrder(DOMEventPluginOrder); +injectEventPluginOrder(DOMEventPluginOrder); setComponentTree( getFiberCurrentPropsFromNode, getInstanceFromNode, @@ -34,7 +53,7 @@ setComponentTree( * Some important event plugins included by default (without having to require * them). */ -EventPluginHubInjection.injectEventPluginsByName({ +injectEventPluginsByName({ SimpleEventPlugin: SimpleEventPlugin, EnterLeaveEventPlugin: EnterLeaveEventPlugin, ChangeEventPlugin: ChangeEventPlugin, diff --git a/packages/react-dom/src/events/DOMEventPluginOrder.js b/packages/react-dom/src/events/DOMEventPluginOrder.js deleted file mode 100644 index add0b12a238ac..0000000000000 --- a/packages/react-dom/src/events/DOMEventPluginOrder.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * 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. - */ - -/** - * Module that is injectable into `EventPluginHub`, that 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', -]; - -export default DOMEventPluginOrder; diff --git a/packages/react-dom/src/events/DOMEventPluginSystem.js b/packages/react-dom/src/events/DOMEventPluginSystem.js new file mode 100644 index 0000000000000..40190f30e3cfe --- /dev/null +++ b/packages/react-dom/src/events/DOMEventPluginSystem.js @@ -0,0 +1,215 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {AnyNativeEvent} from 'legacy-events/PluginModuleType'; +import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; +import type {EventSystemFlags} from 'legacy-events/EventSystemFlags'; +import type {Fiber} from 'react-reconciler/src/ReactFiber'; +import type {PluginModule} from 'legacy-events/PluginModuleType'; +import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType'; +import type {TopLevelType} from 'legacy-events/TopLevelEventTypes'; + +import {HostRoot, HostComponent, HostText} from 'shared/ReactWorkTags'; +import {IS_FIRST_ANCESTOR} from 'legacy-events/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'; + +import getEventTarget from './getEventTarget'; +import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; + +const CALLBACK_BOOKKEEPING_POOL_SIZE = 10; +const callbackBookkeepingPool = []; + +type BookKeepingInstance = {| + topLevelType: DOMTopLevelEventType | null, + eventSystemFlags: EventSystemFlags, + nativeEvent: AnyNativeEvent | null, + targetInst: Fiber | null, + ancestors: Array, +|}; + +function releaseTopLevelCallbackBookKeeping( + instance: BookKeepingInstance, +): void { + instance.topLevelType = null; + instance.nativeEvent = null; + instance.targetInst = null; + instance.ancestors.length = 0; + if (callbackBookkeepingPool.length < CALLBACK_BOOKKEEPING_POOL_SIZE) { + callbackBookkeepingPool.push(instance); + } +} + +// Used to store ancestor hierarchy in top level callback +function getTopLevelCallbackBookKeeping( + topLevelType: DOMTopLevelEventType, + nativeEvent: AnyNativeEvent, + targetInst: Fiber | null, + eventSystemFlags: EventSystemFlags, +): BookKeepingInstance { + if (callbackBookkeepingPool.length) { + const instance = callbackBookkeepingPool.pop(); + instance.topLevelType = topLevelType; + instance.eventSystemFlags = eventSystemFlags; + instance.nativeEvent = nativeEvent; + instance.targetInst = targetInst; + return instance; + } + return { + topLevelType, + eventSystemFlags, + nativeEvent, + targetInst, + ancestors: [], + }; +} + +/** + * Find the deepest React component completely containing the root of the + * passed-in instance (for use when entire React trees are nested within each + * other). If React trees are not nested, returns null. + */ +function findRootContainerNode(inst) { + if (inst.tag === HostRoot) { + return inst.stateNode.containerInfo; + } + // TODO: It may be a good idea to cache this to prevent unnecessary DOM + // traversal, but caching is difficult to do correctly without using a + // mutation observer to listen for all DOM changes. + while (inst.return) { + inst = inst.return; + } + if (inst.tag !== HostRoot) { + // This can happen if we're in a detached tree. + return null; + } + return inst.stateNode.containerInfo; +} + +/** + * Allows registered plugins an opportunity to extract events from top-level + * native browser events. + * + * @return {*} An accumulation of synthetic events. + * @internal + */ +function extractPluginEvents( + topLevelType: TopLevelType, + targetInst: null | Fiber, + nativeEvent: AnyNativeEvent, + nativeEventTarget: null | EventTarget, + eventSystemFlags: EventSystemFlags, +): Array | ReactSyntheticEvent | null { + let events = null; + for (let i = 0; i < plugins.length; i++) { + // Not every plugin in the ordering may be loaded at runtime. + const possiblePlugin: PluginModule = plugins[i]; + if (possiblePlugin) { + const extractedEvents = possiblePlugin.extractEvents( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget, + eventSystemFlags, + ); + if (extractedEvents) { + events = accumulateInto(events, extractedEvents); + } + } + } + return events; +} + +function runExtractedPluginEventsInBatch( + topLevelType: TopLevelType, + targetInst: null | Fiber, + nativeEvent: AnyNativeEvent, + nativeEventTarget: null | EventTarget, + eventSystemFlags: EventSystemFlags, +) { + const events = extractPluginEvents( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget, + eventSystemFlags, + ); + runEventsInBatch(events); +} + +function handleTopLevel(bookKeeping: BookKeepingInstance) { + let targetInst = bookKeeping.targetInst; + + // Loop through the hierarchy, in case there's any nested components. + // It's important that we build the array of ancestors before calling any + // event handlers, because event handlers can modify the DOM, leading to + // inconsistencies with ReactMount's node cache. See #1105. + let ancestor = targetInst; + do { + if (!ancestor) { + const ancestors = bookKeeping.ancestors; + ((ancestors: any): Array).push(ancestor); + break; + } + const root = findRootContainerNode(ancestor); + if (!root) { + break; + } + const tag = ancestor.tag; + if (tag === HostComponent || tag === HostText) { + bookKeeping.ancestors.push(ancestor); + } + ancestor = getClosestInstanceFromNode(root); + } while (ancestor); + + for (let i = 0; i < bookKeeping.ancestors.length; i++) { + targetInst = bookKeeping.ancestors[i]; + const eventTarget = getEventTarget(bookKeeping.nativeEvent); + const topLevelType = ((bookKeeping.topLevelType: any): DOMTopLevelEventType); + const nativeEvent = ((bookKeeping.nativeEvent: any): AnyNativeEvent); + let eventSystemFlags = bookKeeping.eventSystemFlags; + + // If this is the first ancestor, we mark it on the system flags + if (i === 0) { + eventSystemFlags |= IS_FIRST_ANCESTOR; + } + + runExtractedPluginEventsInBatch( + topLevelType, + targetInst, + nativeEvent, + eventTarget, + eventSystemFlags, + ); + } +} + +export function dispatchEventForPluginEventSystem( + topLevelType: DOMTopLevelEventType, + eventSystemFlags: EventSystemFlags, + nativeEvent: AnyNativeEvent, + targetInst: null | Fiber, +): void { + const bookKeeping = getTopLevelCallbackBookKeeping( + topLevelType, + nativeEvent, + targetInst, + eventSystemFlags, + ); + + try { + // Event queue being processed in the same cycle allows + // `preventDefault`. + batchedEventUpdates(handleTopLevel, bookKeeping); + } finally { + releaseTopLevelCallbackBookKeeping(bookKeeping); + } +} diff --git a/packages/react-dom/src/events/ReactBrowserEventEmitter.js b/packages/react-dom/src/events/ReactBrowserEventEmitter.js index 16cd407cba787..3cc7aae2f2ccf 100644 --- a/packages/react-dom/src/events/ReactBrowserEventEmitter.js +++ b/packages/react-dom/src/events/ReactBrowserEventEmitter.js @@ -42,13 +42,13 @@ import isEventSupported from './isEventSupported'; * may be done in the worker thread. * * - Forward these native events (with the associated top-level type used to - * trap it) to `EventPluginHub`, which in turn will ask plugins if they want + * trap it) to `EventPluginRegistry`, which in turn will ask plugins if they want * to extract any synthetic events. * - * - The `EventPluginHub` will then process each event by annotating them with + * - The `EventPluginRegistry` will then process each event by annotating them with * "dispatches", a sequence of listeners and IDs that care about that event. * - * - The `EventPluginHub` then dispatches the events. + * - The `EventPluginRegistry` then dispatches the events. * * Overview of React and the event system: * @@ -65,7 +65,7 @@ import isEventSupported from './isEventSupported'; * | . | |Plugin | * +-----|------+ . v +-----------+ * | | | . +--------------+ +------------+ - * | +-----------.--->|EventPluginHub| | Event | + * | +-----------.--->|PluginRegistry| | Event | * | | . | | +-----------+ | Propagators| * | ReactEvent | . | | |TapEvent | |------------| * | Emitter | . | |<---+|Plugin | |other plugin| diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index d43c201168531..e119fafa48eec 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -8,7 +8,6 @@ */ import type {AnyNativeEvent} from 'legacy-events/PluginModuleType'; -import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot'; import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig'; import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; @@ -18,11 +17,9 @@ import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; import * as Scheduler from 'scheduler'; import { - batchedEventUpdates, discreteUpdates, flushDiscreteUpdatesIfNeeded, } from 'legacy-events/ReactGenericBatching'; -import {runExtractedPluginEventsInBatch} from 'legacy-events/EventPluginHub'; import {DEPRECATED_dispatchEventForResponderEventSystem} from './DeprecatedDOMEventResponderSystem'; import { isReplayableDiscreteEvent, @@ -36,12 +33,7 @@ import { getContainerFromFiber, getSuspenseInstanceFromFiber, } from 'react-reconciler/reflection'; -import { - HostRoot, - SuspenseComponent, - HostComponent, - HostText, -} from 'shared/ReactWorkTags'; +import {HostRoot, SuspenseComponent} from 'shared/ReactWorkTags'; import { type EventSystemFlags, PLUGIN_EVENT_SYSTEM, @@ -49,7 +41,6 @@ import { IS_PASSIVE, IS_ACTIVE, PASSIVE_NOT_SUPPORTED, - IS_FIRST_ANCESTOR, } from 'legacy-events/EventSystemFlags'; import { @@ -69,128 +60,13 @@ import { DiscreteEvent, } from 'shared/ReactTypes'; import {getEventPriorityForPluginSystem} from './DOMEventProperties'; +import {dispatchEventForPluginEventSystem} from './DOMEventPluginSystem'; const { unstable_UserBlockingPriority: UserBlockingPriority, unstable_runWithPriority: runWithPriority, } = Scheduler; -const CALLBACK_BOOKKEEPING_POOL_SIZE = 10; -const callbackBookkeepingPool = []; - -type BookKeepingInstance = {| - topLevelType: DOMTopLevelEventType | null, - eventSystemFlags: EventSystemFlags, - nativeEvent: AnyNativeEvent | null, - targetInst: Fiber | null, - ancestors: Array, -|}; - -/** - * Find the deepest React component completely containing the root of the - * passed-in instance (for use when entire React trees are nested within each - * other). If React trees are not nested, returns null. - */ -function findRootContainerNode(inst) { - if (inst.tag === HostRoot) { - return inst.stateNode.containerInfo; - } - // TODO: It may be a good idea to cache this to prevent unnecessary DOM - // traversal, but caching is difficult to do correctly without using a - // mutation observer to listen for all DOM changes. - while (inst.return) { - inst = inst.return; - } - if (inst.tag !== HostRoot) { - // This can happen if we're in a detached tree. - return null; - } - return inst.stateNode.containerInfo; -} - -// Used to store ancestor hierarchy in top level callback -function getTopLevelCallbackBookKeeping( - topLevelType: DOMTopLevelEventType, - nativeEvent: AnyNativeEvent, - targetInst: Fiber | null, - eventSystemFlags: EventSystemFlags, -): BookKeepingInstance { - if (callbackBookkeepingPool.length) { - const instance = callbackBookkeepingPool.pop(); - instance.topLevelType = topLevelType; - instance.eventSystemFlags = eventSystemFlags; - instance.nativeEvent = nativeEvent; - instance.targetInst = targetInst; - return instance; - } - return { - topLevelType, - eventSystemFlags, - nativeEvent, - targetInst, - ancestors: [], - }; -} - -function releaseTopLevelCallbackBookKeeping( - instance: BookKeepingInstance, -): void { - instance.topLevelType = null; - instance.nativeEvent = null; - instance.targetInst = null; - instance.ancestors.length = 0; - if (callbackBookkeepingPool.length < CALLBACK_BOOKKEEPING_POOL_SIZE) { - callbackBookkeepingPool.push(instance); - } -} - -function handleTopLevel(bookKeeping: BookKeepingInstance) { - let targetInst = bookKeeping.targetInst; - - // Loop through the hierarchy, in case there's any nested components. - // It's important that we build the array of ancestors before calling any - // event handlers, because event handlers can modify the DOM, leading to - // inconsistencies with ReactMount's node cache. See #1105. - let ancestor = targetInst; - do { - if (!ancestor) { - const ancestors = bookKeeping.ancestors; - ((ancestors: any): Array).push(ancestor); - break; - } - const root = findRootContainerNode(ancestor); - if (!root) { - break; - } - const tag = ancestor.tag; - if (tag === HostComponent || tag === HostText) { - bookKeeping.ancestors.push(ancestor); - } - ancestor = getClosestInstanceFromNode(root); - } while (ancestor); - - for (let i = 0; i < bookKeeping.ancestors.length; i++) { - targetInst = bookKeeping.ancestors[i]; - const eventTarget = getEventTarget(bookKeeping.nativeEvent); - const topLevelType = ((bookKeeping.topLevelType: any): DOMTopLevelEventType); - const nativeEvent = ((bookKeeping.nativeEvent: any): AnyNativeEvent); - let eventSystemFlags = bookKeeping.eventSystemFlags; - - // If this is the first ancestor, we mark it on the system flags - if (i === 0) { - eventSystemFlags |= IS_FIRST_ANCESTOR; - } - - runExtractedPluginEventsInBatch( - topLevelType, - targetInst, - nativeEvent, - eventTarget, - eventSystemFlags, - ); - } -} - // TODO: can we stop exporting these? export let _enabled = true; @@ -323,28 +199,6 @@ function dispatchUserBlockingUpdate( ); } -function dispatchEventForPluginEventSystem( - topLevelType: DOMTopLevelEventType, - eventSystemFlags: EventSystemFlags, - nativeEvent: AnyNativeEvent, - targetInst: null | Fiber, -): void { - const bookKeeping = getTopLevelCallbackBookKeeping( - topLevelType, - nativeEvent, - targetInst, - eventSystemFlags, - ); - - try { - // Event queue being processed in the same cycle allows - // `preventDefault`. - batchedEventUpdates(handleTopLevel, bookKeeping); - } finally { - releaseTopLevelCallbackBookKeeping(bookKeeping); - } -} - export function dispatchEvent( topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, diff --git a/packages/react-native-renderer/src/ReactFabricEventEmitter.js b/packages/react-native-renderer/src/ReactFabricEventEmitter.js index 98c1c3c7184c4..959df5bc48732 100644 --- a/packages/react-native-renderer/src/ReactFabricEventEmitter.js +++ b/packages/react-native-renderer/src/ReactFabricEventEmitter.js @@ -7,22 +7,76 @@ * @flow */ +import type {AnyNativeEvent} from 'legacy-events/PluginModuleType'; +import type {EventSystemFlags} from 'legacy-events/EventSystemFlags'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; +import type {PluginModule} from 'legacy-events/PluginModuleType'; +import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType'; +import type {TopLevelType} from 'legacy-events/TopLevelEventTypes'; import {PLUGIN_EVENT_SYSTEM} from 'legacy-events/EventSystemFlags'; -import { - getListener, - runExtractedPluginEventsInBatch, -} from 'legacy-events/EventPluginHub'; import {registrationNameModules} from 'legacy-events/EventPluginRegistry'; import {batchedUpdates} from 'legacy-events/ReactGenericBatching'; +import accumulateInto from 'legacy-events/accumulateInto'; -import type {AnyNativeEvent} from 'legacy-events/PluginModuleType'; import {enableNativeTargetAsInstance} from 'shared/ReactFeatureFlags'; -import type {TopLevelType} from 'legacy-events/TopLevelEventTypes'; +import {plugins} from 'legacy-events/EventPluginRegistry'; +import getListener from 'legacy-events/getListener'; +import {runEventsInBatch} from 'legacy-events/EventBatching'; export {getListener, registrationNameModules as registrationNames}; +/** + * Allows registered plugins an opportunity to extract events from top-level + * native browser events. + * + * @return {*} An accumulation of synthetic events. + * @internal + */ +function extractPluginEvents( + topLevelType: TopLevelType, + targetInst: null | Fiber, + nativeEvent: AnyNativeEvent, + nativeEventTarget: null | EventTarget, + eventSystemFlags: EventSystemFlags, +): Array | ReactSyntheticEvent | null { + let events = null; + for (let i = 0; i < plugins.length; i++) { + // Not every plugin in the ordering may be loaded at runtime. + const possiblePlugin: PluginModule = plugins[i]; + if (possiblePlugin) { + const extractedEvents = possiblePlugin.extractEvents( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget, + eventSystemFlags, + ); + if (extractedEvents) { + events = accumulateInto(events, extractedEvents); + } + } + } + return events; +} + +function runExtractedPluginEventsInBatch( + topLevelType: TopLevelType, + targetInst: null | Fiber, + nativeEvent: AnyNativeEvent, + nativeEventTarget: null | EventTarget, + eventSystemFlags: EventSystemFlags, +) { + const events = extractPluginEvents( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget, + eventSystemFlags, + ); + runEventsInBatch(events); +} + export function dispatchEvent( target: null | Object, topLevelType: TopLevelType, diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index 424f0eae25d22..789a4f25061c5 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -28,9 +28,6 @@ const { const ReactNativeBridgeEventPlugin = { eventTypes: {}, - /** - * @see {EventPluginHub.extractEvents} - */ extractEvents: function( topLevelType: TopLevelType, targetInst: null | Object, diff --git a/packages/react-native-renderer/src/ReactNativeEventEmitter.js b/packages/react-native-renderer/src/ReactNativeEventEmitter.js index 07c62f83edd6f..fe69998e9db45 100644 --- a/packages/react-native-renderer/src/ReactNativeEventEmitter.js +++ b/packages/react-native-renderer/src/ReactNativeEventEmitter.js @@ -7,20 +7,24 @@ * @flow */ +import type {AnyNativeEvent} from 'legacy-events/PluginModuleType'; +import type {EventSystemFlags} from 'legacy-events/EventSystemFlags'; +import type {Fiber} from 'react-reconciler/src/ReactFiber'; +import type {PluginModule} from 'legacy-events/PluginModuleType'; +import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType'; +import type {TopLevelType} from 'legacy-events/TopLevelEventTypes'; + import {PLUGIN_EVENT_SYSTEM} from 'legacy-events/EventSystemFlags'; -import { - getListener, - runExtractedPluginEventsInBatch, -} from 'legacy-events/EventPluginHub'; import {registrationNameModules} from 'legacy-events/EventPluginRegistry'; import {batchedUpdates} from 'legacy-events/ReactGenericBatching'; +import {runEventsInBatch} from 'legacy-events/EventBatching'; import {enableNativeTargetAsInstance} from 'shared/ReactFeatureFlags'; +import {plugins} from 'legacy-events/EventPluginRegistry'; +import getListener from 'legacy-events/getListener'; +import accumulateInto from 'legacy-events/accumulateInto'; import {getInstanceFromNode} from './ReactNativeComponentTree'; -import type {AnyNativeEvent} from 'legacy-events/PluginModuleType'; -import type {TopLevelType} from 'legacy-events/TopLevelEventTypes'; - export {getListener, registrationNameModules as registrationNames}; /** @@ -121,6 +125,57 @@ function _receiveRootNodeIDEvent( // where it would do it. } +/** + * Allows registered plugins an opportunity to extract events from top-level + * native browser events. + * + * @return {*} An accumulation of synthetic events. + * @internal + */ +function extractPluginEvents( + topLevelType: TopLevelType, + targetInst: null | Fiber, + nativeEvent: AnyNativeEvent, + nativeEventTarget: null | EventTarget, + eventSystemFlags: EventSystemFlags, +): Array | ReactSyntheticEvent | null { + let events = null; + for (let i = 0; i < plugins.length; i++) { + // Not every plugin in the ordering may be loaded at runtime. + const possiblePlugin: PluginModule = plugins[i]; + if (possiblePlugin) { + const extractedEvents = possiblePlugin.extractEvents( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget, + eventSystemFlags, + ); + if (extractedEvents) { + events = accumulateInto(events, extractedEvents); + } + } + } + return events; +} + +function runExtractedPluginEventsInBatch( + topLevelType: TopLevelType, + targetInst: null | Fiber, + nativeEvent: AnyNativeEvent, + nativeEventTarget: null | EventTarget, + eventSystemFlags: EventSystemFlags, +) { + const events = extractPluginEvents( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget, + eventSystemFlags, + ); + runEventsInBatch(events); +} + /** * Publicly exposed method on module for native objc to invoke when a top * level event is extracted. diff --git a/packages/react-native-renderer/src/ReactNativeInjectionShared.js b/packages/react-native-renderer/src/ReactNativeInjectionShared.js index 9e97891918812..bfb3b926db57f 100644 --- a/packages/react-native-renderer/src/ReactNativeInjectionShared.js +++ b/packages/react-native-renderer/src/ReactNativeInjectionShared.js @@ -16,8 +16,11 @@ // Module provided by RN: import 'react-native/Libraries/ReactPrivate/ReactNativePrivateInitializeCore'; -import {injection as EventPluginHubInjection} from 'legacy-events/EventPluginHub'; import ResponderEventPlugin from 'legacy-events/ResponderEventPlugin'; +import { + injectEventPluginOrder, + injectEventPluginsByName, +} from 'legacy-events/EventPluginRegistry'; import ReactNativeBridgeEventPlugin from './ReactNativeBridgeEventPlugin'; import ReactNativeEventPluginOrder from './ReactNativeEventPluginOrder'; @@ -25,13 +28,13 @@ import ReactNativeEventPluginOrder from './ReactNativeEventPluginOrder'; /** * Inject module for resolving DOM hierarchy and plugin ordering. */ -EventPluginHubInjection.injectEventPluginOrder(ReactNativeEventPluginOrder); +injectEventPluginOrder(ReactNativeEventPluginOrder); /** * Some important event plugins included by default (without having to require * them). */ -EventPluginHubInjection.injectEventPluginsByName({ +injectEventPluginsByName({ ResponderEventPlugin: ResponderEventPlugin, ReactNativeBridgeEventPlugin: ReactNativeBridgeEventPlugin, }); diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 654b283dc676e..5a2e9b8075e1e 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -98,8 +98,8 @@ "96": "EventPluginRegistry: Cannot inject event plugins that do not exist in the plugin ordering, `%s`.", "97": "EventPluginRegistry: Event plugins must implement an `extractEvents` method, but `%s` does not.", "98": "EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.", - "99": "EventPluginHub: More than one plugin attempted to publish the same event name, `%s`.", - "100": "EventPluginHub: More than one plugin attempted to publish the same registration name, `%s`.", + "99": "EventPluginRegistry: More than one plugin attempted to publish the same event name, `%s`.", + "100": "EventPluginRegistry: More than one plugin attempted to publish the same registration name, `%s`.", "101": "EventPluginRegistry: Cannot inject event plugin ordering more than once. You are likely trying to load more than one copy of React.", "102": "EventPluginRegistry: Cannot inject two different event plugins using the same name, `%s`.", "103": "executeDirectDispatch(...): Invalid `event`.",