Further cleanup of plugin event system (#18056)

This commit is contained in:
Dominic Gannaway
2020-02-18 13:31:59 +00:00
committed by GitHub
parent d533229fba
commit f48a5e64e8
9 changed files with 221 additions and 227 deletions
@@ -14,7 +14,8 @@ let EventPluginRegistry;
let React;
let ReactDOM;
let ReactDOMComponentTree;
let ReactBrowserEventEmitter;
let DOMEventPluginSystem;
let ReactDOMEventListener;
let ReactTestUtils;
let idCallOrder;
@@ -52,18 +53,21 @@ function registerSimpleTestHandler() {
return getListener(CHILD, ON_CLICK_KEY);
}
// We should probably remove this file at some point, it's just full of
// internal API usage. ReactBrowserEventEmitter was refactored out in
// #18056 too. The majority of this code lives in DOMEventPluginSystem.
describe('ReactBrowserEventEmitter', () => {
beforeEach(() => {
jest.resetModules();
LISTENER.mockClear();
// TODO: can we express this test with only public API?
EventPluginGetListener = require('legacy-events/getListener').default;
EventPluginRegistry = require('legacy-events/EventPluginRegistry');
React = require('react');
ReactDOM = require('react-dom');
ReactDOMComponentTree = require('../client/ReactDOMComponentTree');
ReactBrowserEventEmitter = require('../events/ReactBrowserEventEmitter');
DOMEventPluginSystem = require('../events/DOMEventPluginSystem');
ReactDOMEventListener = require('../events/ReactDOMEventListener');
ReactTestUtils = require('react-dom/test-utils');
container = document.createElement('div');
@@ -177,12 +181,12 @@ describe('ReactBrowserEventEmitter', () => {
expect(LISTENER).toHaveBeenCalledTimes(1);
});
it('should not invoke handlers if ReactBrowserEventEmitter is disabled', () => {
it('should not invoke handlers if ReactDOMEventListener is disabled', () => {
registerSimpleTestHandler();
ReactBrowserEventEmitter.setEnabled(false);
ReactDOMEventListener.setEnabled(false);
CHILD.click();
expect(LISTENER).toHaveBeenCalledTimes(0);
ReactBrowserEventEmitter.setEnabled(true);
ReactDOMEventListener.setEnabled(true);
CHILD.click();
expect(LISTENER).toHaveBeenCalledTimes(1);
});
@@ -346,15 +350,15 @@ describe('ReactBrowserEventEmitter', () => {
it('should listen to events only once', () => {
spyOnDevAndProd(EventTarget.prototype, 'addEventListener');
ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document);
ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document);
DOMEventPluginSystem.listenToEvent(ON_CLICK_KEY, document);
DOMEventPluginSystem.listenToEvent(ON_CLICK_KEY, document);
expect(EventTarget.prototype.addEventListener).toHaveBeenCalledTimes(1);
});
it('should work with event plugins without dependencies', () => {
spyOnDevAndProd(EventTarget.prototype, 'addEventListener');
ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document);
DOMEventPluginSystem.listenToEvent(ON_CLICK_KEY, document);
expect(EventTarget.prototype.addEventListener.calls.argsFor(0)[0]).toBe(
'click',
@@ -364,7 +368,7 @@ describe('ReactBrowserEventEmitter', () => {
it('should work with event plugins with dependencies', () => {
spyOnDevAndProd(EventTarget.prototype, 'addEventListener');
ReactBrowserEventEmitter.listenTo(ON_CHANGE_KEY, document);
DOMEventPluginSystem.listenToEvent(ON_CHANGE_KEY, document);
const setEventListeners = [];
const listenCalls = EventTarget.prototype.addEventListener.calls.allArgs();
+4 -6
View File
@@ -57,14 +57,11 @@ import {
TOP_SUBMIT,
TOP_TOGGLE,
} from '../events/DOMTopLevelEventTypes';
import {
listenTo,
trapBubbledEvent,
getListenerMapForElement,
} from '../events/ReactBrowserEventEmitter';
import {getListenerMapForElement} from '../events/DOMEventListenerMap';
import {
addResponderEventSystemEvent,
removeActiveResponderEventSystemEvent,
trapBubbledEvent,
} from '../events/ReactDOMEventListener.js';
import {mediaEventTypes} from '../events/DOMTopLevelEventTypes';
import {
@@ -90,6 +87,7 @@ import {
enableDeprecatedFlareAPI,
enableTrustedTypesIntegration,
} from 'shared/ReactFeatureFlags';
import {listenToEvent} from '../events/DOMEventPluginSystem';
let didWarnInvalidHydration = false;
let didWarnShadyDOM = false;
@@ -274,7 +272,7 @@ function ensureListeningTo(
const doc = isDocumentOrFragment
? rootContainerElement
: rootContainerElement.ownerDocument;
listenTo(registrationName, doc);
listenToEvent(registrationName, doc);
}
function getOwnerDocumentFromRootContainer(
+1 -1
View File
@@ -10,7 +10,7 @@
import {addUserTimingListener} from 'shared/ReactFeatureFlags';
import ReactDOM from './ReactDOM';
import {isEnabled} from '../events/ReactBrowserEventEmitter';
import {isEnabled} from '../events/ReactDOMEventListener';
import {getClosestInstanceFromNode} from './ReactDOMComponentTree';
if (__EXPERIMENTAL__) {
+1 -1
View File
@@ -34,7 +34,7 @@ import {validateDOMNesting, updatedAncestorInfo} from './validateDOMNesting';
import {
isEnabled as ReactBrowserEventEmitterIsEnabled,
setEnabled as ReactBrowserEventEmitterSetEnabled,
} from '../events/ReactBrowserEventEmitter';
} from '../events/ReactDOMEventListener';
import {getChildNamespace} from '../shared/DOMNamespaces';
import {
ELEMENT_NODE,
+31
View File
@@ -0,0 +1,31 @@
/**
* 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 {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
// prettier-ignore
const elementListenerMap:
// $FlowFixMe Work around Flow bug
| WeakMap
| Map<
Document | Element | Node,
Map<DOMTopLevelEventType | string, null | (any => void)>,
> = new PossiblyWeakMap();
export function getListenerMapForElement(
element: Document | Element | Node,
): Map<DOMTopLevelEventType | string, null | (any => void)> {
let listenerMap = elementListenerMap.get(element);
if (listenerMap === undefined) {
listenerMap = new Map();
elementListenerMap.set(element, listenerMap);
}
return listenerMap;
}
+166
View File
@@ -21,9 +21,81 @@ 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 {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';
import getEventTarget from './getEventTarget';
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
import {trapCapturedEvent, trapBubbledEvent} from './ReactDOMEventListener';
import {getListenerMapForElement} from './DOMEventListenerMap';
import isEventSupported from './isEventSupported';
import {
TOP_BLUR,
TOP_CANCEL,
TOP_CLOSE,
TOP_FOCUS,
TOP_INVALID,
TOP_RESET,
TOP_SCROLL,
TOP_SUBMIT,
getRawEventName,
mediaEventTypes,
} from './DOMTopLevelEventTypes';
/**
* Summary of `DOMEventPluginSystem` event handling:
*
* - Top-level delegation is used to trap most native browser events. This
* may only occur in the main thread and is the responsibility of
* ReactDOMEventListener, which is injected and can therefore support
* pluggable event sources. This is the only work that occurs in the main
* thread.
*
* - We normalize and de-duplicate events to account for browser quirks. This
* may be done in the worker thread.
*
* - Forward these native events (with the associated top-level type used to
* trap it) to `EventPluginRegistry`, which in turn will ask plugins if they want
* to extract any synthetic events.
*
* - The `EventPluginRegistry` will then process each event by annotating them with
* "dispatches", a sequence of listeners and IDs that care about that event.
*
* - The `EventPluginRegistry` then dispatches the events.
*
* Overview of React and the event system:
*
* +------------+ .
* | DOM | .
* +------------+ .
* | .
* v .
* +------------+ .
* | ReactEvent | .
* | Listener | .
* +------------+ . +-----------+
* | . +--------+|SimpleEvent|
* | . | |Plugin |
* +-----|------+ . v +-----------+
* | | | . +--------------+ +------------+
* | +-----------.--->|PluginRegistry| | Event |
* | | . | | +-----------+ | Propagators|
* | ReactEvent | . | | |TapEvent | |------------|
* | Emitter | . | |<---+|Plugin | |other plugin|
* | | . | | +-----------+ | utilities |
* | +-----------.--->| | +------------+
* | | | . +--------------+
* +-----|------+ . ^ +-----------+
* | . | |Enter/Leave|
* + . +-------+|Plugin |
* +-------------+ . +-----------+
* | application | .
* |-------------| .
* | | .
* | | .
* +-------------+ .
* .
* React Core . General Purpose Event Plugin System
*/
const CALLBACK_BOOKKEEPING_POOL_SIZE = 10;
const callbackBookkeepingPool = [];
@@ -213,3 +285,97 @@ export function dispatchEventForPluginEventSystem(
releaseTopLevelCallbackBookKeeping(bookKeeping);
}
}
/**
* We listen for bubbled touch events on the document object.
*
* Firefox v8.01 (and possibly others) exhibited strange behavior when
* mounting `onmousemove` events at some node that was not the document
* element. The symptoms were that if your mouse is not moving over something
* contained within that mount point (for example on the background) the
* top-level listeners for `onmousemove` won't be called. However, if you
* register the `mousemove` on the document object, then it will of course
* catch all `mousemove`s. This along with iOS quirks, justifies restricting
* top-level listeners to the document object only, at least for these
* movement types of events and possibly all events.
*
* @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
*
* Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but
* they bubble to document.
*
* @param {string} registrationName Name of listener (e.g. `onClick`).
* @param {object} mountAt Container where to mount the listener
*/
export function listenToEvent(
registrationName: string,
mountAt: Document | Element | Node,
): void {
const listeningSet = getListenerMapForElement(mountAt);
const dependencies = registrationNameDependencies[registrationName];
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
listenToTopLevelEvent(dependency, mountAt, listeningSet);
}
}
export function listenToTopLevelEvent(
topLevelType: DOMTopLevelEventType,
mountAt: Document | Element | Node,
listenerMap: Map<DOMTopLevelEventType | string, null | (any => void)>,
): void {
if (!listenerMap.has(topLevelType)) {
switch (topLevelType) {
case TOP_SCROLL:
trapCapturedEvent(TOP_SCROLL, mountAt);
break;
case TOP_FOCUS:
case TOP_BLUR:
trapCapturedEvent(TOP_FOCUS, mountAt);
trapCapturedEvent(TOP_BLUR, mountAt);
// We set the flag for a single dependency later in this function,
// but this ensures we mark both as attached rather than just one.
listenerMap.set(TOP_BLUR, null);
listenerMap.set(TOP_FOCUS, null);
break;
case TOP_CANCEL:
case TOP_CLOSE:
if (isEventSupported(getRawEventName(topLevelType))) {
trapCapturedEvent(topLevelType, mountAt);
}
break;
case TOP_INVALID:
case TOP_SUBMIT:
case TOP_RESET:
// We listen to them on the target DOM elements.
// Some of them bubble so we don't want them to fire twice.
break;
default:
// By default, listen on the top level to all non-media events.
// Media events don't bubble so adding the listener wouldn't do anything.
const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1;
if (!isMediaEvent) {
trapBubbledEvent(topLevelType, mountAt);
}
break;
}
listenerMap.set(topLevelType, null);
}
}
export function isListeningToAllDependencies(
registrationName: string,
mountAt: Document | Element,
): boolean {
const listenerMap = getListenerMapForElement(mountAt);
const dependencies = registrationNameDependencies[registrationName];
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
if (!listenerMap.has(dependency)) {
return false;
}
}
return true;
}
@@ -1,203 +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 {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
import {
TOP_BLUR,
TOP_CANCEL,
TOP_CLOSE,
TOP_FOCUS,
TOP_INVALID,
TOP_RESET,
TOP_SCROLL,
TOP_SUBMIT,
getRawEventName,
mediaEventTypes,
} from './DOMTopLevelEventTypes';
import {
setEnabled,
isEnabled,
trapBubbledEvent,
trapCapturedEvent,
} from './ReactDOMEventListener';
import isEventSupported from './isEventSupported';
/**
* Summary of `ReactBrowserEventEmitter` event handling:
*
* - Top-level delegation is used to trap most native browser events. This
* may only occur in the main thread and is the responsibility of
* ReactDOMEventListener, which is injected and can therefore support
* pluggable event sources. This is the only work that occurs in the main
* thread.
*
* - We normalize and de-duplicate events to account for browser quirks. This
* may be done in the worker thread.
*
* - Forward these native events (with the associated top-level type used to
* trap it) to `EventPluginRegistry`, which in turn will ask plugins if they want
* to extract any synthetic events.
*
* - The `EventPluginRegistry` will then process each event by annotating them with
* "dispatches", a sequence of listeners and IDs that care about that event.
*
* - The `EventPluginRegistry` then dispatches the events.
*
* Overview of React and the event system:
*
* +------------+ .
* | DOM | .
* +------------+ .
* | .
* v .
* +------------+ .
* | ReactEvent | .
* | Listener | .
* +------------+ . +-----------+
* | . +--------+|SimpleEvent|
* | . | |Plugin |
* +-----|------+ . v +-----------+
* | | | . +--------------+ +------------+
* | +-----------.--->|PluginRegistry| | Event |
* | | . | | +-----------+ | Propagators|
* | ReactEvent | . | | |TapEvent | |------------|
* | Emitter | . | |<---+|Plugin | |other plugin|
* | | . | | +-----------+ | utilities |
* | +-----------.--->| | +------------+
* | | | . +--------------+
* +-----|------+ . ^ +-----------+
* | . | |Enter/Leave|
* + . +-------+|Plugin |
* +-------------+ . +-----------+
* | application | .
* |-------------| .
* | | .
* | | .
* +-------------+ .
* .
* React Core . General Purpose Event Plugin System
*/
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
// prettier-ignore
const elementListenerMap:
// $FlowFixMe Work around Flow bug
| WeakMap
| Map<
Document | Element | Node,
Map<DOMTopLevelEventType | string, null | (any => void)>,
> = new PossiblyWeakMap();
export function getListenerMapForElement(
element: Document | Element | Node,
): Map<DOMTopLevelEventType | string, null | (any => void)> {
let listenerMap = elementListenerMap.get(element);
if (listenerMap === undefined) {
listenerMap = new Map();
elementListenerMap.set(element, listenerMap);
}
return listenerMap;
}
/**
* We listen for bubbled touch events on the document object.
*
* Firefox v8.01 (and possibly others) exhibited strange behavior when
* mounting `onmousemove` events at some node that was not the document
* element. The symptoms were that if your mouse is not moving over something
* contained within that mount point (for example on the background) the
* top-level listeners for `onmousemove` won't be called. However, if you
* register the `mousemove` on the document object, then it will of course
* catch all `mousemove`s. This along with iOS quirks, justifies restricting
* top-level listeners to the document object only, at least for these
* movement types of events and possibly all events.
*
* @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
*
* Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but
* they bubble to document.
*
* @param {string} registrationName Name of listener (e.g. `onClick`).
* @param {object} mountAt Container where to mount the listener
*/
export function listenTo(
registrationName: string,
mountAt: Document | Element | Node,
): void {
const listeningSet = getListenerMapForElement(mountAt);
const dependencies = registrationNameDependencies[registrationName];
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
listenToTopLevel(dependency, mountAt, listeningSet);
}
}
export function listenToTopLevel(
topLevelType: DOMTopLevelEventType,
mountAt: Document | Element | Node,
listenerMap: Map<DOMTopLevelEventType | string, null | (any => void)>,
): void {
if (!listenerMap.has(topLevelType)) {
switch (topLevelType) {
case TOP_SCROLL:
trapCapturedEvent(TOP_SCROLL, mountAt);
break;
case TOP_FOCUS:
case TOP_BLUR:
trapCapturedEvent(TOP_FOCUS, mountAt);
trapCapturedEvent(TOP_BLUR, mountAt);
// We set the flag for a single dependency later in this function,
// but this ensures we mark both as attached rather than just one.
listenerMap.set(TOP_BLUR, null);
listenerMap.set(TOP_FOCUS, null);
break;
case TOP_CANCEL:
case TOP_CLOSE:
if (isEventSupported(getRawEventName(topLevelType))) {
trapCapturedEvent(topLevelType, mountAt);
}
break;
case TOP_INVALID:
case TOP_SUBMIT:
case TOP_RESET:
// We listen to them on the target DOM elements.
// Some of them bubble so we don't want them to fire twice.
break;
default:
// By default, listen on the top level to all non-media events.
// Media events don't bubble so adding the listener wouldn't do anything.
const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1;
if (!isMediaEvent) {
trapBubbledEvent(topLevelType, mountAt);
}
break;
}
listenerMap.set(topLevelType, null);
}
}
export function isListeningToAllDependencies(
registrationName: string,
mountAt: Document | Element,
): boolean {
const listenerMap = getListenerMapForElement(mountAt);
const dependencies = registrationNameDependencies[registrationName];
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
if (!listenerMap.has(dependency)) {
return false;
}
}
return true;
}
export {setEnabled, isEnabled, trapBubbledEvent, trapCapturedEvent};
+3 -5
View File
@@ -32,10 +32,7 @@ import {
attemptToDispatchEvent,
addResponderEventSystemEvent,
} from './ReactDOMEventListener';
import {
getListenerMapForElement,
listenToTopLevel,
} from './ReactBrowserEventEmitter';
import {getListenerMapForElement} from './DOMEventListenerMap';
import {
getInstanceFromNode,
getClosestInstanceFromNode,
@@ -120,6 +117,7 @@ import {
TOP_BLUR,
} from './DOMTopLevelEventTypes';
import {IS_REPLAYED} from 'legacy-events/EventSystemFlags';
import {listenToTopLevelEvent} from './DOMEventPluginSystem';
type QueuedReplayableEvent = {|
blockedOn: null | Container | SuspenseInstance,
@@ -217,7 +215,7 @@ function trapReplayableEvent(
document: Document,
listenerMap: Map<DOMTopLevelEventType | string, null | (any => void)>,
) {
listenToTopLevel(topLevelType, document, listenerMap);
listenToTopLevelEvent(topLevelType, document, listenerMap);
if (enableDeprecatedFlareAPI) {
// Trap events for the responder system.
const topLevelTypeString = unsafeCastDOMTopLevelTypeToString(topLevelType);
+1 -1
View File
@@ -22,11 +22,11 @@ import {
TOP_MOUSE_UP,
TOP_SELECTION_CHANGE,
} from './DOMTopLevelEventTypes';
import {isListeningToAllDependencies} from './ReactBrowserEventEmitter';
import getActiveElement from '../client/getActiveElement';
import {getNodeFromInstance} from '../client/ReactDOMComponentTree';
import {hasSelectionCapabilities} from '../client/ReactInputSelection';
import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
import {isListeningToAllDependencies} from './DOMEventPluginSystem';
const skipSelectionChangeEvent =
canUseDOM && 'documentMode' in document && document.documentMode <= 11;