-
Notifications
You must be signed in to change notification settings - Fork 48.8k
Replay capture phase for continuous events #22680
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
Changes from all commits
e7cc04d
bb6943f
0233d1b
236af6c
5c76d9c
f46749f
5994948
56d1b06
353e011
6bf50cb
a88e173
f866d88
bfe8062
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,8 +11,11 @@ import type {AnyNativeEvent} from '../events/PluginModuleType'; | |
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; | ||
import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig'; | ||
import type {DOMEventName} from '../events/DOMEventNames'; | ||
import type {NullTarget} from './ReactDOMEventReplaying'; | ||
import {enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay} from 'shared/ReactFeatureFlags'; | ||
import { | ||
nullTarget, | ||
isBlocked, | ||
isDiscreteEventThatRequiresHydration, | ||
queueDiscreteEvent, | ||
hasQueuedDiscreteEvents, | ||
|
@@ -155,13 +158,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 | ||
let blockedOnInst = isBlocked(blockedOn); | ||
if (!blockedOnInst) { | ||
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) | ||
) { | ||
while (blockedOnInst) { | ||
const fiber = getInstanceFromNode(blockedOnInst); | ||
if (fiber !== null) { | ||
attemptSynchronousHydration(fiber); | ||
} | ||
const nextBlockedOn = findInstanceBlockingEvent( | ||
domEventName, | ||
eventSystemFlags, | ||
targetContainer, | ||
nativeEvent, | ||
); | ||
if (nextBlockedOn === blockedOn) { | ||
break; | ||
} | ||
blockedOn = nextBlockedOn; | ||
blockedOnInst = isBlocked(blockedOn); | ||
} | ||
if (blockedOnInst) { | ||
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() && | ||
|
@@ -180,14 +298,23 @@ export function dispatchEvent( | |
return; | ||
} | ||
|
||
let blockedOn = attemptToDispatchEvent( | ||
const blockedOn = findInstanceBlockingEvent( | ||
domEventName, | ||
eventSystemFlags, | ||
targetContainer, | ||
nativeEvent, | ||
); | ||
|
||
if (blockedOn === null) { | ||
const blockedOnInst = isBlocked(blockedOn); | ||
if (!blockedOnInst) { | ||
dispatchEventForPluginEventSystem( | ||
domEventName, | ||
eventSystemFlags, | ||
nativeEvent, | ||
blockedOn === null | ||
? getClosestInstanceFromNode(getEventTarget(nativeEvent)) | ||
: null, | ||
targetContainer, | ||
); | ||
// We successfully dispatched this event. | ||
if (allowReplay) { | ||
clearIfContinuousEvent(domEventName, nativeEvent); | ||
|
@@ -196,10 +323,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, | ||
|
@@ -226,33 +350,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( | ||
|
@@ -264,62 +361,55 @@ 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 nullTarget if not blocked but we should dispatch without a targetInst | ||
export function findInstanceBlockingEvent( | ||
domEventName: DOMEventName, | ||
eventSystemFlags: EventSystemFlags, | ||
targetContainer: EventTarget, | ||
nativeEvent: AnyNativeEvent, | ||
): null | Container | SuspenseInstance { | ||
): NullTarget | null | Container | SuspenseInstance { | ||
// 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); | ||
if (instance !== null) { | ||
// Queue the event to be replayed later. Abort dispatching since we | ||
// don't want this event dispatched twice through the event system. | ||
// TODO: If this is the first discrete event in the queue. Schedule an increased | ||
// priority for this boundary. | ||
return instance; | ||
} | ||
// 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 nullTarget; | ||
} 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
|
||
return nullTarget; | ||
} 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 nullTarget; | ||
} | ||
} else { | ||
// This tree has been unmounted already. Dispatch without a target. | ||
return nullTarget; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I couldn't come up with a test case where we
Based off how hydration works it shouldn't be possible for the |
||
} | ||
} | ||
dispatchEventForPluginEventSystem( | ||
domEventName, | ||
eventSystemFlags, | ||
nativeEvent, | ||
targetInst, | ||
targetContainer, | ||
); | ||
// We're not blocked on anything. | ||
return null; | ||
} | ||
|
Uh oh!
There was an error while loading. Please reload this page.