Skip to content

Commit

Permalink
Remove capturePhaseEvents and separate events by bubbling
Browse files Browse the repository at this point in the history
WIP

Refine all logic

Revise types

Fix

Fix conflicts

Fix flags

Fix

Fix

Fix test

Revise

Cleanup
  • Loading branch information
trueadm committed Jul 10, 2020
1 parent 61dd00d commit 9553a7d
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 125 deletions.
11 changes: 4 additions & 7 deletions packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,13 +349,10 @@ describe('ReactDOMEventListener', () => {
}),
);
// As of the modern event system refactor, we now support
// this on <img>. The reason for this, is because we now
// attach all media events to the "root" or "portal" in the
// capture phase, rather than the bubble phase. This allows
// us to assign less event listeners to individual elements,
// which also nicely allows us to support more without needing
// to add more individual code paths to support various
// events that do not bubble.
// this on <img>. The reason for this, is because we allow
// events to be attached to nodes regardless of if they
// necessary support them. This is a strange test, as this
// would never occur from normal browser behavior.
expect(handleImgLoadStart).toHaveBeenCalledTimes(1);

videoRef.current.dispatchEvent(
Expand Down
19 changes: 10 additions & 9 deletions packages/react-dom/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ if (__DEV__) {
export function ensureListeningTo(
rootContainerInstance: Element | Node,
reactPropEvent: string,
targetElement?: Element,
): void {
// If we have a comment node, then use the parent node,
// which should be an element.
Expand Down Expand Up @@ -364,7 +365,7 @@ function setInitialDOMProperties(
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
ensureListeningTo(rootContainerElement, propKey);
ensureListeningTo(rootContainerElement, propKey, domElement);
}
} else if (nextProp != null) {
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
Expand Down Expand Up @@ -552,7 +553,7 @@ export function setInitialProperties(
props = ReactDOMInputGetHostProps(domElement, rawProps);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
case 'option':
ReactDOMOptionValidateProps(domElement, rawProps);
Expand All @@ -563,14 +564,14 @@ export function setInitialProperties(
props = ReactDOMSelectGetHostProps(domElement, rawProps);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
props = ReactDOMTextareaGetHostProps(domElement, rawProps);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
default:
props = rawProps;
Expand Down Expand Up @@ -790,7 +791,7 @@ export function diffProperties(
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
ensureListeningTo(rootContainerElement, propKey);
ensureListeningTo(rootContainerElement, propKey, domElement);
}
if (!updatePayload && lastProp !== nextProp) {
// This is a special case. If any listener updates we need to ensure
Expand Down Expand Up @@ -904,7 +905,7 @@ export function diffHydratedProperties(
ReactDOMInputInitWrapperState(domElement, rawProps);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
case 'option':
ReactDOMOptionValidateProps(domElement, rawProps);
Expand All @@ -913,13 +914,13 @@ export function diffHydratedProperties(
ReactDOMSelectInitWrapperState(domElement, rawProps);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
ensureListeningTo(rootContainerElement, 'onChange', domElement);
break;
}

Expand Down Expand Up @@ -986,7 +987,7 @@ export function diffHydratedProperties(
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
ensureListeningTo(rootContainerElement, propKey);
ensureListeningTo(rootContainerElement, propKey, domElement);
}
} else if (
__DEV__ &&
Expand Down
18 changes: 9 additions & 9 deletions packages/react-dom/src/client/ReactDOMEventHandle.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
getClosestInstanceFromNode,
getEventHandlerListeners,
setEventHandlerListeners,
getEventListenerMap,
getFiberFromScopeInstance,
} from './ReactDOMComponentTree';
import {ELEMENT_NODE} from '../shared/HTMLNodeType';
Expand Down Expand Up @@ -87,6 +86,7 @@ function registerEventOnNearestTargetContainer(
isPassiveListener: boolean | void,
listenerPriority: EventPriority | void,
isCapturePhaseListener: boolean,
targetElement?: Element,
): void {
// If it is, find the nearest root or portal and make it
// our event handle target container.
Expand All @@ -98,13 +98,11 @@ function registerEventOnNearestTargetContainer(
'that did not have a corresponding root. This is likely a bug in React.',
);
}
const listenerMap = getEventListenerMap(targetContainer);
listenToNativeEvent(
topLevelType,
targetContainer,
listenerMap,
PLUGIN_EVENT_SYSTEM,
isCapturePhaseListener,
targetContainer,
targetElement,
isPassiveListener,
listenerPriority,
);
Expand Down Expand Up @@ -135,6 +133,7 @@ function registerReactDOMEvent(
isPassiveListener,
listenerPriority,
isCapturePhaseListener,
targetElement,
);
} else if (enableScopeAPI && isReactScope(target)) {
const scopeTarget = ((target: any): ReactScopeInstance);
Expand All @@ -152,15 +151,16 @@ function registerReactDOMEvent(
);
} else if (isValidEventTarget(target)) {
const eventTarget = ((target: any): EventTarget);
const listenerMap = getEventListenerMap(eventTarget);
// These are valid event targets, but they are also
// non-managed React nodes.
listenToNativeEvent(
topLevelType,
eventTarget,
listenerMap,
PLUGIN_EVENT_SYSTEM | IS_EVENT_HANDLE_NON_MANAGED_NODE,
isCapturePhaseListener,
eventTarget,
undefined,
isPassiveListener,
listenerPriority,
PLUGIN_EVENT_SYSTEM | IS_EVENT_HANDLE_NON_MANAGED_NODE,
);
} else {
invariant(
Expand Down
133 changes: 71 additions & 62 deletions packages/react-dom/src/events/DOMModernPluginEventSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ import type {TopLevelType, DOMTopLevelEventType} from './TopLevelEventTypes';
import type {EventSystemFlags} from './EventSystemFlags';
import type {AnyNativeEvent} from './PluginModuleType';
import type {ReactSyntheticEvent} from './ReactSyntheticEventType';
import type {
ElementListenerMap,
ElementListenerMapEntry,
} from '../client/ReactDOMComponentTree';
import type {ElementListenerMapEntry} from '../client/ReactDOMComponentTree';
import type {EventPriority} from 'shared/ReactTypes';
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';

Expand All @@ -25,6 +22,7 @@ import {
IS_REPLAYED,
IS_CAPTURE_PHASE,
IS_EVENT_HANDLE_NON_MANAGED_NODE,
IS_NON_DELEGATED,
} from './EventSystemFlags';

import {
Expand Down Expand Up @@ -71,7 +69,6 @@ import {
TOP_PLAYING,
TOP_CLICK,
TOP_SELECTION_CHANGE,
TOP_AFTER_BLUR,
getRawEventName,
} from './DOMTopLevelEventTypes';
import {
Expand Down Expand Up @@ -154,8 +151,11 @@ function extractEvents(
targetContainer,
);
const shouldProcessPolyfillPlugins =
(eventSystemFlags & IS_CAPTURE_PHASE) === 0 ||
capturePhaseEvents.has(topLevelType);
(eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
(eventSystemFlags & IS_NON_DELEGATED) === 0 &&
((eventSystemFlags & IS_CAPTURE_PHASE) === 0 ||
topLevelType === TOP_FOCUS ||
topLevelType === TOP_BLUR);
// We don't process these events unless we are in the
// event's native "bubble" phase, which means that we're
// not in the capture phase. That's because we emulate
Expand Down Expand Up @@ -213,18 +213,14 @@ function extractEvents(
}
}

export const capturePhaseEvents: Set<DOMTopLevelEventType> = new Set([
TOP_FOCUS,
TOP_BLUR,
// We should not delegate these events to the container, but rather
// set them on the actual target element itself.
export const nonDelegatedEvents: Set<DOMTopLevelEventType> = new Set([
TOP_SCROLL,
TOP_LOAD,
TOP_ABORT,
TOP_CANCEL,
TOP_CLOSE,
TOP_INVALID,
TOP_RESET,
TOP_SUBMIT,
TOP_ABORT,
TOP_CAN_PLAY,
TOP_CAN_PLAY_THROUGH,
TOP_DURATION_CHANGE,
Expand All @@ -247,12 +243,14 @@ export const capturePhaseEvents: Set<DOMTopLevelEventType> = new Set([
TOP_TIME_UPDATE,
TOP_VOLUME_CHANGE,
TOP_WAITING,
// INVALID, RESET and SUBMIT bubble, but this causes problems internally,
// so we should keep them as non-delegating:
// https://github.com/facebook/react/pull/13462
TOP_INVALID,
TOP_RESET,
TOP_SUBMIT,
]);

if (enableCreateEventHandleAPI) {
capturePhaseEvents.add(TOP_AFTER_BLUR);
}

function executeDispatch(
event: ReactSyntheticEvent,
listener: Function,
Expand Down Expand Up @@ -337,20 +335,31 @@ function shouldUpgradeListener(

export function listenToNativeEvent(
topLevelType: DOMTopLevelEventType,
target: EventTarget,
listenerMap: ElementListenerMap,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
rootContainerElement: EventTarget,
targetElement?: Element,
isPassiveListener?: boolean,
priority?: EventPriority,
listenerPriority?: EventPriority,
eventSystemFlags?: EventSystemFlags = PLUGIN_EVENT_SYSTEM,
): void {
let target = rootContainerElement;
// TOP_SELECTION_CHANGE needs to be attached to the document
// otherwise it won't capture incoming events that are only
// triggered on the document directly.
if (topLevelType === TOP_SELECTION_CHANGE) {
target = (target: any).ownerDocument || target;
listenerMap = getEventListenerMap(target);
target = (rootContainerElement: any).ownerDocument;
}
// If the event can be delegated, we can register it to the root container.
// Otherwise, we should register the event to the target element.
if (targetElement !== undefined && nonDelegatedEvents.has(topLevelType)) {
eventSystemFlags |= IS_NON_DELEGATED;
target = targetElement;
}
// This is temporary, we will remove this when introducing focusin/focusout
if (topLevelType === TOP_FOCUS || topLevelType === TOP_BLUR) {
isCapturePhaseListener = true;
}
const listenerMap = getEventListenerMap(target);
const listenerMapKey = getListenerMapKey(
topLevelType,
isCapturePhaseListener,
Expand Down Expand Up @@ -383,50 +392,51 @@ export function listenToNativeEvent(
isCapturePhaseListener,
false,
isPassiveListener,
priority,
listenerPriority,
);
listenerMap.set(listenerMapKey, {passive: isPassiveListener, listener});
}
}

function isCaptureRegistrationName(registrationName: string): boolean {
const len = registrationName.length;
return registrationName.substr(len - 7) === 'Capture';
}

export function listenToReactEvent(
reactPropEvent: string,
reactEvent: string,
rootContainerElement: Element,
targetElement?: Element,
): void {
const listenerMap = getEventListenerMap(rootContainerElement);
// For optimization, let's check if we have the registration name
// on the rootContainerElement.
if (listenerMap.has(reactPropEvent)) {
return;
}
// Add the registration name to the map, so we can avoid processing
// this React prop event again.
listenerMap.set(reactPropEvent, null);
const dependencies = registrationNameDependencies[reactPropEvent];
const dependencies = registrationNameDependencies[reactEvent];
const dependenciesLength = dependencies.length;
// If the dependencies length is 1, that means we're not using a polyfill
// plugin like ChangeEventPlugin, BeforeInputPlugin, EnterLeavePlugin and
// SelectEventPlugin. SimpleEventPlugin always only has a single dependency.
// Given this, we know that we never need to apply capture phase event
// listeners to anything other than the SimpleEventPlugin.
const registrationCapturePhase =
isCaptureRegistrationName(reactPropEvent) && dependenciesLength === 1;
// plugin like ChangeEventPlugin, BeforeInputPlugin, EnterLeavePlugin
// and SelectEventPlugin. We always use the native bubble event phase for
// these plugins and emulate two phase event dispatching. SimpleEventPlugin
// always only has a single dependency and SimpleEventPlugin events also
// use either the native capture event phase or bubble event phase, there
// is no emulation (except for focus/blur, but that will be removed soon).
const isPolyfillEventPlugin = dependenciesLength !== 1;

for (let i = 0; i < dependenciesLength; i++) {
const dependency = dependencies[i];
const capture =
capturePhaseEvents.has(dependency) || registrationCapturePhase;
if (isPolyfillEventPlugin) {
const listenerMap = getEventListenerMap(rootContainerElement);
// For optimization, we register plugins on the listener map, so we
// don't need to check each of their dependencies each time.
if (!listenerMap.has(reactEvent)) {
listenerMap.set(reactEvent, null);
for (let i = 0; i < dependenciesLength; i++) {
listenToNativeEvent(
dependencies[i],
false,
rootContainerElement,
targetElement,
);
}
}
} else {
// Check if the react event ends in "Capture"
const isCapturePhaseListener = reactEvent.substr(-7) === 'Capture';
listenToNativeEvent(
dependency,
dependencies[0],
isCapturePhaseListener,
rootContainerElement,
listenerMap,
PLUGIN_EVENT_SYSTEM,
capture,
targetElement,
);
}
}
Expand Down Expand Up @@ -560,10 +570,10 @@ export function dispatchEventForPluginEventSystem(
targetContainer: EventTarget,
): void {
let ancestorInst = targetInst;
if (eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) {
// For TargetEvent nodes (i.e. document, window)
ancestorInst = null;
} else {
if (
(eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
(eventSystemFlags & IS_NON_DELEGATED) === 0
) {
const targetContainerNode = ((targetContainer: any): Node);

// If we are using the legacy FB support flag, we
Expand Down Expand Up @@ -704,9 +714,8 @@ export function accumulateSinglePhaseListeners(
const targetType = event.type;
// shouldEmulateTwoPhase is temporary till we can polyfill focus/blur to
// focusin/focusout.
const shouldEmulateTwoPhase = capturePhaseEvents.has(
((targetType: any): DOMTopLevelEventType),
);
const shouldEmulateTwoPhase =
targetType === TOP_FOCUS || targetType === TOP_BLUR;

// Accumulate all instances and listeners via the target -> root path.
while (instance !== null) {
Expand Down
Loading

0 comments on commit 9553a7d

Please sign in to comment.