Compare commits

...

3 Commits

Author SHA1 Message Date
Tianyu Yao
c2cebbf9ee Attach new listeners to ReactDOMRoot 2023-01-20 16:26:05 -08:00
Tianyu Yao
31ca11900f fix tests 2023-01-20 11:47:08 -08:00
Tianyu Yao
7c8acf9d26 Flush continuous updates in the capture phase of a discrete event 2023-01-20 11:08:42 -08:00
7 changed files with 96 additions and 0 deletions

View File

@@ -27,7 +27,9 @@ import {
getSuspenseInstanceFromFiber,
} from 'react-reconciler/src/ReactFiberTreeReflection';
import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
import {flushPendingContinuousUpdates} from './ReactDOMUpdateBatching';
import {type EventSystemFlags, IS_CAPTURE_PHASE} from './EventSystemFlags';
import {allNativeEvents} from './EventRegistry';
import getEventTarget from './getEventTarget';
import {
@@ -111,6 +113,38 @@ export function createEventListenerWrapperWithPriority(
);
}
const listeningMarker =
'_reactListening' +
Math.random()
.toString(36)
.slice(2);
// For flushing continuous events before a discrete event in capture phase
// this function needs to be called before `listenToAllSupportedEvents` so
// that it runs before event system's capture phase callback.
export function listenToCapturePhaseDiscreteEvents(
rootContainerElement: EventTarget,
root: FiberRoot,
) {
if ((rootContainerElement: any)[listeningMarker]) {
return;
}
(rootContainerElement: any)[listeningMarker] = true;
function onEvent() {
flushPendingContinuousUpdates(root);
}
allNativeEvents.forEach(domEventName => {
const eventPriority = getEventPriority(domEventName);
if (eventPriority !== DiscreteEventPriority) {
return;
}
rootContainerElement.addEventListener(domEventName, onEvent, {
capture: true,
});
});
}
function dispatchDiscreteEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,

View File

@@ -24,6 +24,7 @@ let discreteUpdatesImpl = function(fn, a, b, c, d) {
return fn(a, b, c, d);
};
let flushSyncImpl = function() {};
let flushPendingContinuousUpdatesImpl = function() {};
let isInsideEventHandler = false;
@@ -64,12 +65,18 @@ export function discreteUpdates(fn, a, b, c, d) {
return discreteUpdatesImpl(fn, a, b, c, d);
}
export function flushPendingContinuousUpdates(root) {
return flushPendingContinuousUpdatesImpl(root);
}
export function setBatchingImplementation(
_batchedUpdatesImpl,
_discreteUpdatesImpl,
_flushSyncImpl,
_flushPendingContinuousUpdatesImpl,
) {
batchedUpdatesImpl = _batchedUpdatesImpl;
discreteUpdatesImpl = _discreteUpdatesImpl;
flushSyncImpl = _flushSyncImpl;
flushPendingContinuousUpdatesImpl = _flushPendingContinuousUpdatesImpl;
}

View File

@@ -35,6 +35,7 @@ import {createEventHandle} from 'react-dom-bindings/src/client/ReactDOMEventHand
import {
batchedUpdates,
discreteUpdates,
flushPendingContinuousUpdates,
flushSync as flushSyncWithoutWarningIfAlreadyRendering,
isAlreadyRendering,
flushControlled,
@@ -108,6 +109,7 @@ setBatchingImplementation(
batchedUpdates,
discreteUpdates,
flushSyncWithoutWarningIfAlreadyRendering,
flushPendingContinuousUpdates,
);
function createPortal(

View File

@@ -61,6 +61,7 @@ import {
unmarkContainerAsRoot,
} from 'react-dom-bindings/src/client/ReactDOMComponentTree';
import {listenToAllSupportedEvents} from 'react-dom-bindings/src/events/DOMPluginEventSystem';
import {listenToCapturePhaseDiscreteEvents} from 'react-dom-bindings/src/events/ReactDOMEventLIstener';
import {
ELEMENT_NODE,
COMMENT_NODE,
@@ -256,6 +257,7 @@ export function createRoot(
container.nodeType === COMMENT_NODE
? (container.parentNode: any)
: container;
listenToCapturePhaseDiscreteEvents(rootContainerElement, root);
listenToAllSupportedEvents(rootContainerElement);
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
@@ -344,6 +346,7 @@ export function hydrateRoot(
Dispatcher.current = ReactDOMClientDispatcher;
}
// This can't be a comment node since hydration doesn't work on comment nodes anyway.
listenToCapturePhaseDiscreteEvents(container, root);
listenToAllSupportedEvents(container);
if (mutableSources) {

View File

@@ -1260,6 +1260,46 @@ describe('DOMPluginEventSystem', () => {
}
});
it('flushes continuous events in the capture phase of a discrete event', async () => {
const buttonRef = React.createRef();
function Test() {
const [clientX, setClientX] = React.useState(0);
Scheduler.unstable_yieldValue(`Render:${clientX}`);
return (
<div>
<button
ref={buttonRef}
onClickCapture={() => {
Scheduler.unstable_yieldValue(`Click:${clientX}`);
}}
onMouseOver={e => {
setClientX(e.clientX);
}}
/>
</div>
);
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Test />);
});
expect(Scheduler).toHaveYielded(['Render:0']);
const buttonElement = buttonRef.current;
// Expect the click event to be able to get the latest state value set by mouse over events
buttonElement.dispatchEvent(
new MouseEvent('mouseover', {
bubbles: true,
capture: true,
cancelable: true,
relatedTarget: null,
clientX: 5,
}),
);
dispatchClickEvent(buttonElement);
expect(Scheduler).toHaveYielded(['Render:5', 'Click:5']);
});
describe('ReactDOM.createEventHandle', () => {
beforeEach(() => {
jest.resetModules();

View File

@@ -61,6 +61,7 @@ import {
scheduleUpdateOnFiber,
scheduleInitialHydrationOnRoot,
flushRoot,
flushPendingContinuousUpdates,
batchedUpdates,
flushSync,
isAlreadyRendering,
@@ -393,6 +394,7 @@ export {
deferredUpdates,
discreteUpdates,
flushControlled,
flushPendingContinuousUpdates,
flushSync,
isAlreadyRendering,
flushPassiveEffects,

View File

@@ -134,10 +134,12 @@ import {
NoLanes,
NoLane,
SyncLane,
InputContinuousLane,
NoTimestamp,
claimNextTransitionLane,
claimNextRetryLane,
includesSyncLane,
includesSomeLane,
isSubsetOfLanes,
mergeLanes,
removeLanes,
@@ -1542,6 +1544,12 @@ export function flushRoot(root: FiberRoot, lanes: Lanes) {
}
}
export function flushPendingContinuousUpdates(root: FiberRoot) {
if (includesSomeLane(root.pendingLanes, InputContinuousLane)) {
flushRoot(root, InputContinuousLane);
}
}
export function getExecutionContext(): ExecutionContext {
return executionContext;
}