Skip to content

Commit

Permalink
Enable eager listeners statically (facebook#19983)
Browse files Browse the repository at this point in the history
  • Loading branch information
gaearon authored and koto committed Jun 15, 2021
1 parent b02c2c1 commit f7a99b5
Show file tree
Hide file tree
Showing 21 changed files with 55 additions and 402 deletions.
1 change: 0 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMFiber-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1040,7 +1040,6 @@ describe('ReactDOMFiber', () => {
expect(ops).toEqual([]);
});

// @gate enableEagerRootListeners
it('listens to events that do not exist in the Portal subtree', () => {
const onClick = jest.fn();

Expand Down
88 changes: 5 additions & 83 deletions packages/react-dom/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,25 +61,16 @@ import {
shouldRemoveAttribute,
} from '../shared/DOMProperty';
import assertValidProps from '../shared/assertValidProps';
import {
DOCUMENT_NODE,
ELEMENT_NODE,
COMMENT_NODE,
DOCUMENT_FRAGMENT_NODE,
} from '../shared/HTMLNodeType';
import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
import isCustomComponent from '../shared/isCustomComponent';
import possibleStandardNames from '../shared/possibleStandardNames';
import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';
import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';
import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols';

import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';
import {
enableTrustedTypesIntegration,
enableEagerRootListeners,
} from 'shared/ReactFeatureFlags';
import {
listenToReactEvent,
mediaEventTypes,
listenToNonDelegatedEvent,
} from '../events/DOMPluginEventSystem';
Expand Down Expand Up @@ -253,39 +244,6 @@ if (__DEV__) {
};
}

export function ensureListeningTo(
rootContainerInstance: Element | Node,
reactPropEvent: string,
targetElement: Element | null,
): void {
if (!enableEagerRootListeners) {
// If we have a comment node, then use the parent node,
// which should be an element.
const rootContainerElement =
rootContainerInstance.nodeType === COMMENT_NODE
? rootContainerInstance.parentNode
: rootContainerInstance;
if (__DEV__) {
if (
rootContainerElement == null ||
(rootContainerElement.nodeType !== ELEMENT_NODE &&
// This is to support rendering into a ShadowRoot:
rootContainerElement.nodeType !== DOCUMENT_FRAGMENT_NODE)
) {
console.error(
'ensureListeningTo(): received a container that was not an element node. ' +
'This is likely a bug in React. Please file an issue.',
);
}
}
listenToReactEvent(
reactPropEvent,
((rootContainerElement: any): Element),
targetElement,
);
}
}

function getOwnerDocumentFromRootContainer(
rootContainerElement: Element | Document,
): Document {
Expand Down Expand Up @@ -364,9 +322,7 @@ function setInitialDOMProperties(
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
if (!enableEagerRootListeners) {
ensureListeningTo(rootContainerElement, propKey, domElement);
} else if (propKey === 'onScroll') {
if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
}
}
Expand Down Expand Up @@ -577,11 +533,6 @@ export function setInitialProperties(
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
listenToNonDelegatedEvent('invalid', domElement);
if (!enableEagerRootListeners) {
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange', domElement);
}
break;
case 'option':
ReactDOMOptionValidateProps(domElement, rawProps);
Expand All @@ -593,23 +544,13 @@ export function setInitialProperties(
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
listenToNonDelegatedEvent('invalid', domElement);
if (!enableEagerRootListeners) {
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange', domElement);
}
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
props = ReactDOMTextareaGetHostProps(domElement, rawProps);
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
listenToNonDelegatedEvent('invalid', domElement);
if (!enableEagerRootListeners) {
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange', domElement);
}
break;
default:
props = rawProps;
Expand Down Expand Up @@ -827,9 +768,7 @@ export function diffProperties(
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
if (!enableEagerRootListeners) {
ensureListeningTo(rootContainerElement, propKey, domElement);
} else if (propKey === 'onScroll') {
if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
}
}
Expand Down Expand Up @@ -983,11 +922,6 @@ export function diffHydratedProperties(
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
listenToNonDelegatedEvent('invalid', domElement);
if (!enableEagerRootListeners) {
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange', domElement);
}
break;
case 'option':
ReactDOMOptionValidateProps(domElement, rawProps);
Expand All @@ -997,22 +931,12 @@ export function diffHydratedProperties(
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
listenToNonDelegatedEvent('invalid', domElement);
if (!enableEagerRootListeners) {
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange', domElement);
}
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
listenToNonDelegatedEvent('invalid', domElement);
if (!enableEagerRootListeners) {
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange', domElement);
}
break;
}

Expand Down Expand Up @@ -1079,9 +1003,7 @@ export function diffHydratedProperties(
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
if (!enableEagerRootListeners) {
ensureListeningTo(rootContainerElement, propKey, domElement);
} else if (propKey === 'onScroll') {
if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
}
}
Expand Down
87 changes: 3 additions & 84 deletions packages/react-dom/src/client/ReactDOMEventHandle.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,43 +16,26 @@ import type {

import {allNativeEvents} from '../events/EventRegistry';
import {
getClosestInstanceFromNode,
getEventHandlerListeners,
setEventHandlerListeners,
getFiberFromScopeInstance,
doesTargetHaveEventHandle,
addEventHandleToTarget,
} from './ReactDOMComponentTree';
import {ELEMENT_NODE, COMMENT_NODE} from '../shared/HTMLNodeType';
import {ELEMENT_NODE} from '../shared/HTMLNodeType';
import {listenToNativeEvent} from '../events/DOMPluginEventSystem';

import {HostRoot, HostPortal} from 'react-reconciler/src/ReactWorkTags';
import {IS_EVENT_HANDLE_NON_MANAGED_NODE} from '../events/EventSystemFlags';

import {
enableScopeAPI,
enableCreateEventHandleAPI,
enableEagerRootListeners,
} from 'shared/ReactFeatureFlags';
import invariant from 'shared/invariant';

type EventHandleOptions = {|
capture?: boolean,
|};

function getNearestRootOrPortalContainer(node: Fiber): null | Element {
while (node !== null) {
const tag = node.tag;
// Once we encounter a host container or root container
// we can return their DOM instance.
if (tag === HostRoot || tag === HostPortal) {
return node.stateNode.containerInfo;
}
node = node.return;
}
return null;
}

function isValidEventTarget(target: EventTarget | ReactScopeInstance): boolean {
return typeof (target: Object).addEventListener === 'function';
}
Expand All @@ -73,79 +56,15 @@ function createEventHandleListener(
};
}

function registerEventOnNearestTargetContainer(
targetFiber: Fiber,
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
targetElement: Element | null,
): void {
if (!enableEagerRootListeners) {
// If it is, find the nearest root or portal and make it
// our event handle target container.
let targetContainer = getNearestRootOrPortalContainer(targetFiber);
if (targetContainer === null) {
if (__DEV__) {
console.error(
'ReactDOM.createEventHandle: setListener called on an target ' +
'that did not have a corresponding root. This is likely a bug in React.',
);
}
return;
}
if (targetContainer.nodeType === COMMENT_NODE) {
targetContainer = ((targetContainer.parentNode: any): Element);
}
listenToNativeEvent(
domEventName,
isCapturePhaseListener,
targetContainer,
targetElement,
);
}
}

function registerReactDOMEvent(
target: EventTarget | ReactScopeInstance,
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
): void {
// Check if the target is a DOM element.
if ((target: any).nodeType === ELEMENT_NODE) {
if (!enableEagerRootListeners) {
const targetElement = ((target: any): Element);
// Check if the DOM element is managed by React.
const targetFiber = getClosestInstanceFromNode(targetElement);
if (targetFiber === null) {
if (__DEV__) {
console.error(
'ReactDOM.createEventHandle: setListener called on an element ' +
'target that is not managed by React. Ensure React rendered the DOM element.',
);
}
return;
}
registerEventOnNearestTargetContainer(
targetFiber,
domEventName,
isCapturePhaseListener,
targetElement,
);
}
// Do nothing. We already attached all root listeners.
} else if (enableScopeAPI && isReactScope(target)) {
if (!enableEagerRootListeners) {
const scopeTarget = ((target: any): ReactScopeInstance);
const targetFiber = getFiberFromScopeInstance(scopeTarget);
if (targetFiber === null) {
// Scope is unmounted, do not proceed.
return;
}
registerEventOnNearestTargetContainer(
targetFiber,
domEventName,
isCapturePhaseListener,
null,
);
}
// Do nothing. We already attached all root listeners.
} else if (isValidEventTarget(target)) {
const eventTarget = ((target: any): EventTarget);
// These are valid event targets, but they are also
Expand Down
12 changes: 2 additions & 10 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,9 @@ import {
enableFundamentalAPI,
enableCreateEventHandleAPI,
enableScopeAPI,
enableEagerRootListeners,
} from 'shared/ReactFeatureFlags';
import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
import {
listenToReactEvent,
listenToAllSupportedEvents,
} from '../events/DOMPluginEventSystem';
import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';

export type Type = string;
export type Props = {
Expand Down Expand Up @@ -1073,11 +1069,7 @@ export function makeOpaqueHydratingObject(
}

export function preparePortalMount(portalInstance: Instance): void {
if (enableEagerRootListeners) {
listenToAllSupportedEvents(portalInstance);
} else {
listenToReactEvent('onMouseEnter', portalInstance, null);
}
listenToAllSupportedEvents(portalInstance);
}

export function prepareScopeUpdate(
Expand Down
31 changes: 4 additions & 27 deletions packages/react-dom/src/client/ReactDOMRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,12 @@ import {
unmarkContainerAsRoot,
} from './ReactDOMComponentTree';
import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
import {eagerlyTrapReplayableEvents} from '../events/ReactDOMEventReplaying';
import {
ELEMENT_NODE,
COMMENT_NODE,
DOCUMENT_NODE,
DOCUMENT_FRAGMENT_NODE,
} from '../shared/HTMLNodeType';
import {ensureListeningTo} from './ReactDOMComponent';

import {
createContainer,
Expand All @@ -52,7 +50,6 @@ import {
registerMutableSourceForHydration,
} from 'react-reconciler/src/ReactFiberReconciler';
import invariant from 'shared/invariant';
import {enableEagerRootListeners} from 'shared/ReactFeatureFlags';
import {
BlockingRoot,
ConcurrentRoot,
Expand Down Expand Up @@ -133,30 +130,10 @@ function createRootImpl(
null;
const root = createContainer(container, tag, hydrate, hydrationCallbacks);
markContainerAsRoot(root.current, container);
const containerNodeType = container.nodeType;

if (enableEagerRootListeners) {
const rootContainerElement =
container.nodeType === COMMENT_NODE ? container.parentNode : container;
listenToAllSupportedEvents(rootContainerElement);
} else {
if (hydrate && tag !== LegacyRoot) {
const doc =
containerNodeType === DOCUMENT_NODE
? container
: container.ownerDocument;
// We need to cast this because Flow doesn't work
// with the hoisted containerNodeType. If we inline
// it, then Flow doesn't complain. We intentionally
// hoist it to reduce code-size.
eagerlyTrapReplayableEvents(container, ((doc: any): Document));
} else if (
containerNodeType !== DOCUMENT_FRAGMENT_NODE &&
containerNodeType !== DOCUMENT_NODE
) {
ensureListeningTo(container, 'onMouseEnter', null);
}
}

const rootContainerElement =
container.nodeType === COMMENT_NODE ? container.parentNode : container;
listenToAllSupportedEvents(rootContainerElement);

if (mutableSources) {
for (let i = 0; i < mutableSources.length; i++) {
Expand Down
Loading

0 comments on commit f7a99b5

Please sign in to comment.