Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replay capture phase for continuous events #22680

Closed

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion packages/react-dom/src/events/DOMPluginEventSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import * as ChangeEventPlugin from './plugins/ChangeEventPlugin';
import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin';
import * as SelectEventPlugin from './plugins/SelectEventPlugin';
import * as SimpleEventPlugin from './plugins/SimpleEventPlugin';
import {isReplayingEvent} from './replayedEvent';

type DispatchListener = {|
instance: null | Fiber,
Expand Down Expand Up @@ -557,7 +558,8 @@ export function dispatchEventForPluginEventSystem(
// for legacy FB support, where the expected behavior was to
// match React < 16 behavior of delegated clicks to the doc.
domEventName === 'click' &&
(eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0
(eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0 &&
!isReplayingEvent(nativeEvent)
) {
deferClickToDocumentForLegacyFBSupport(domEventName, targetContainer);
return;
Expand Down
5 changes: 2 additions & 3 deletions packages/react-dom/src/events/EventSystemFlags.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ export const IS_EVENT_HANDLE_NON_MANAGED_NODE = 1;
export const IS_NON_DELEGATED = 1 << 1;
export const IS_CAPTURE_PHASE = 1 << 2;
export const IS_PASSIVE = 1 << 3;
export const IS_REPLAYED = 1 << 4;
export const IS_LEGACY_FB_SUPPORT_MODE = 1 << 5;
export const IS_LEGACY_FB_SUPPORT_MODE = 1 << 4;

export const SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE =
IS_LEGACY_FB_SUPPORT_MODE | IS_REPLAYED | IS_CAPTURE_PHASE;
IS_LEGACY_FB_SUPPORT_MODE | IS_CAPTURE_PHASE;

// We do not want to defer if the event system has already been
// set to LEGACY_FB_SUPPORT. LEGACY_FB_SUPPORT only gets set when
Expand Down
193 changes: 140 additions & 53 deletions packages/react-dom/src/events/ReactDOMEventListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,128 @@ export function dispatchEvent(
return;
}

if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
} else {
dispatchEventOriginal(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
}
}

function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
) {
let blockedOn = findInstanceBlockingEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);

// We can dispatch the event now
salazarm marked this conversation as resolved.
Show resolved Hide resolved
// Intentional double equals, either null or undefined
if (blockedOn == null) {
clearIfContinuousEvent(domEventName, nativeEvent);
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
blockedOn === null
? getClosestInstanceFromNode(getEventTarget(nativeEvent))
: null,
targetContainer,
);
return;
}

if (
queueIfContinuousEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
)
) {
nativeEvent.stopPropagation();
return;
}
// We need to clear only if we didn't queue because
// queueing is accumulative.
clearIfContinuousEvent(domEventName, nativeEvent);

if (
eventSystemFlags & IS_CAPTURE_PHASE &&
isDiscreteEventThatRequiresHydration(domEventName)
) {
// Intentionally not strict equal. Could be `null` or `undefined`
while (blockedOn != null) {
const fiber = getInstanceFromNode(blockedOn);
if (fiber !== null) {
attemptSynchronousHydration(fiber);
}
const nextBlockedOn = findInstanceBlockingEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
if (nextBlockedOn === blockedOn) {
break;
}
blockedOn = nextBlockedOn;
}
if (blockedOn) {
nativeEvent.stopPropagation();
return;
}
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
blockedOn === null
? getClosestInstanceFromNode(getEventTarget(nativeEvent))
: null,
targetContainer,
);
return;
}

// This is not replayable so we'll invoke it but without a target,
// in case the event system needs to trace it.
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
}

function dispatchEventOriginal(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
) {
// TODO: replaying capture phase events is currently broken
// because we used to do it during top-level native bubble handlers
// but now we use different bubble and capture handlers.
// In eager mode, we attach capture listeners early, so we need
// to filter them out until we fix the logic to handle them correctly.
const allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;

if (
allowReplay &&
hasQueuedDiscreteEvents() &&
Expand All @@ -180,14 +295,22 @@ export function dispatchEvent(
return;
}

let blockedOn = attemptToDispatchEvent(
const blockedOn = findInstanceBlockingEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);

if (blockedOn === null) {
if (blockedOn == null) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
blockedOn === null
? getClosestInstanceFromNode(getEventTarget(nativeEvent))
: null,
targetContainer,
);
// We successfully dispatched this event.
if (allowReplay) {
clearIfContinuousEvent(domEventName, nativeEvent);
Expand All @@ -196,10 +319,7 @@ export function dispatchEvent(
}

if (allowReplay) {
if (
!enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
isDiscreteEventThatRequiresHydration(domEventName)
) {
if (isDiscreteEventThatRequiresHydration(domEventName)) {
// This this to be replayed later once the target is available.
queueDiscreteEvent(
blockedOn,
Expand All @@ -226,33 +346,6 @@ export function dispatchEvent(
clearIfContinuousEvent(domEventName, nativeEvent);
}

if (
enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
eventSystemFlags & IS_CAPTURE_PHASE &&
isDiscreteEventThatRequiresHydration(domEventName)
) {
while (blockedOn !== null) {
const fiber = getInstanceFromNode(blockedOn);
if (fiber !== null) {
attemptSynchronousHydration(fiber);
}
const nextBlockedOn = attemptToDispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
if (nextBlockedOn === blockedOn) {
break;
}
blockedOn = nextBlockedOn;
}
if (blockedOn) {
nativeEvent.stopPropagation();
return;
}
}

// This is not replayable so we'll invoke it but without a target,
// in case the event system needs to trace it.
dispatchEventForPluginEventSystem(
Expand All @@ -264,24 +357,23 @@ export function dispatchEvent(
);
}

// Attempt dispatching an event. Returns a SuspenseInstance or Container if it's blocked.
export function attemptToDispatchEvent(
// Returns a SuspenseInstance or Container if it's blocked.
// Returns null if not blocked and we should use closestInstance
// Returns undefined if not blocked but we should dispatch without a targetInst
export function findInstanceBlockingEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
): typeof undefined | null | Container | SuspenseInstance {
salazarm marked this conversation as resolved.
Show resolved Hide resolved
// TODO: Warn if _enabled is false.

const nativeEventTarget = getEventTarget(nativeEvent);
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
const targetInst = getClosestInstanceFromNode(nativeEventTarget);

if (targetInst !== null) {
const nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
// This tree has been unmounted already. Dispatch without a target.
salazarm marked this conversation as resolved.
Show resolved Hide resolved
targetInst = null;
} else {
if (nearestMounted !== null) {
const tag = nearestMounted.tag;
if (tag === SuspenseComponent) {
const instance = getSuspenseInstanceFromFiber(nearestMounted);
Expand All @@ -295,31 +387,26 @@ export function attemptToDispatchEvent(
// This shouldn't happen, something went wrong but to avoid blocking
// the whole system, dispatch the event without a target.
// TODO: Warn.
salazarm marked this conversation as resolved.
Show resolved Hide resolved
targetInst = null;
return undefined;
} else if (tag === HostRoot) {
const root: FiberRoot = nearestMounted.stateNode;
if (root.isDehydrated) {
// If this happens during a replay something went wrong and it might block
// the whole system.
return getContainerFromFiber(nearestMounted);
salazarm marked this conversation as resolved.
Show resolved Hide resolved
}
targetInst = null;
salazarm marked this conversation as resolved.
Show resolved Hide resolved
} else if (nearestMounted !== targetInst) {
// If we get an event (ex: img onload) before committing that
// component's mount, ignore it for now (that is, treat it as if it was an
// event on a non-React tree). We might also consider queueing events and
// dispatching them after the mount.
targetInst = null;
return undefined;
}
} else {
// This tree has been unmounted already. Dispatch without a target.
return undefined;
}
}
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
// We're not blocked on anything.
return null;
}
Expand Down
Loading