Compare commits

...

7 Commits

Author SHA1 Message Date
Tianyu Yao b340309c38 Cleanup window.event 2023-04-12 17:18:09 -07:00
Tianyu Yao 7a34b06cbd add test 2023-04-12 15:55:12 -07:00
Tianyu Yao 7fb616330f flow 2023-04-12 15:02:45 -07:00
Tianyu Yao f642605a45 feedback 2023-04-12 12:34:54 -07:00
Tianyu Yao bfaa019b20 Fix the test 2023-04-12 12:03:46 -07:00
Tianyu Yao 6aaa397d86 Use currentEventTransitionLane 2023-04-12 12:03:37 -07:00
Tianyu Yao 79294e914b Synchronously render the transition lane on popstate 2023-04-12 12:00:03 -07:00
11 changed files with 196 additions and 9 deletions

View File

@ -346,6 +346,10 @@ export function getCurrentEventPriority() {
return DefaultEventPriority;
}
export function shouldAttemptEagerTransition() {
return false;
}
// The ART renderer is secondary to the React DOM renderer.
export const isPrimaryRenderer = false;

View File

@ -527,6 +527,10 @@ export function getCurrentEventPriority(): EventPriority {
return getEventPriority(currentEvent.type);
}
export function shouldAttemptEagerTransition(): boolean {
return window.event && window.event.type === 'popstate';
}
export const isPrimaryRenderer = true;
export const warnsIfNotActing = true;
// This initialization code may run even on server environments

View File

@ -27,7 +27,6 @@ describe('ReactDOMFiberAsync', () => {
let container;
beforeEach(() => {
jest.resetModules();
container = document.createElement('div');
React = require('react');
ReactDOM = require('react-dom');
@ -40,6 +39,7 @@ describe('ReactDOMFiberAsync', () => {
assertLog = InternalTestUtils.assertLog;
document.body.appendChild(container);
window.event = undefined;
});
afterEach(() => {
@ -566,4 +566,139 @@ describe('ReactDOMFiberAsync', () => {
expect(container.textContent).toBe('new');
});
it('should synchronously render the transition lane scheduled in a popState', async () => {
function App() {
const [syncState, setSyncState] = React.useState(false);
const [hasNavigated, setHasNavigated] = React.useState(false);
function onPopstate() {
Scheduler.log(`popState`);
React.startTransition(() => {
setHasNavigated(true);
});
setSyncState(true);
}
React.useEffect(() => {
window.addEventListener('popstate', onPopstate);
return () => {
window.removeEventListener('popstate', onPopstate);
};
}, []);
Scheduler.log(`render:${hasNavigated}/${syncState}`);
return null;
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<App />);
});
assertLog(['render:false/false']);
await act(async () => {
const popStateEvent = new Event('popstate');
// Jest is not emulating window.event correctly in the microtask
window.event = popStateEvent;
window.dispatchEvent(popStateEvent);
queueMicrotask(() => {
window.event = undefined;
});
});
assertLog(['popState', 'render:true/true']);
await act(() => {
root.unmount();
});
});
it('Should not flush transition lanes if there is no transition scheduled in popState', async () => {
let setHasNavigated;
function App() {
const [syncState, setSyncState] = React.useState(false);
const [hasNavigated, _setHasNavigated] = React.useState(false);
setHasNavigated = _setHasNavigated;
function onPopstate() {
setSyncState(true);
}
React.useEffect(() => {
window.addEventListener('popstate', onPopstate);
return () => {
window.removeEventListener('popstate', onPopstate);
};
}, []);
Scheduler.log(`render:${hasNavigated}/${syncState}`);
return null;
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<App />);
});
assertLog(['render:false/false']);
React.startTransition(() => {
setHasNavigated(true);
});
await act(async () => {
const popStateEvent = new Event('popstate');
// Jest is not emulating window.event correctly in the microtask
window.event = popStateEvent;
window.dispatchEvent(popStateEvent);
queueMicrotask(() => {
window.event = undefined;
});
});
assertLog(['render:false/true', 'render:true/true']);
await act(() => {
root.unmount();
});
});
it('transition lane in popState should yield if it suspends', async () => {
const never = {then() {}};
let _setText;
function App() {
const [shouldSuspend, setShouldSuspend] = React.useState(false);
const [text, setText] = React.useState('0');
_setText = setText;
if (shouldSuspend) {
Scheduler.log('Suspend!');
throw never;
}
function onPopstate() {
React.startTransition(() => {
setShouldSuspend(val => !val);
});
}
React.useEffect(() => {
window.addEventListener('popstate', onPopstate);
return () => window.removeEventListener('popstate', onPopstate);
}, []);
Scheduler.log(`Child:${shouldSuspend}/${text}`);
return text;
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<App />);
});
assertLog(['Child:false/0']);
await act(() => {
const popStateEvent = new Event('popstate');
window.event = popStateEvent;
window.dispatchEvent(popStateEvent);
queueMicrotask(() => {
window.event = undefined;
});
});
assertLog(['Suspend!']);
await act(async () => {
_setText('1');
});
assertLog(['Child:false/1', 'Suspend!']);
root.unmount();
});
});

View File

@ -334,6 +334,10 @@ export function getCurrentEventPriority(): * {
return DefaultEventPriority;
}
export function shouldAttemptEagerTransition(): boolean {
return false;
}
// The Fabric renderer is secondary to the existing React Native renderer.
export const isPrimaryRenderer = false;

View File

@ -265,6 +265,10 @@ export function getCurrentEventPriority(): * {
return DefaultEventPriority;
}
export function shouldAttemptEagerTransition(): boolean {
return false;
}
// -------------------
// Mutation
// -------------------

View File

@ -526,6 +526,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
return currentEventPriority;
},
shouldAttemptEagerTransition(): boolean {
return false;
},
now: Scheduler.unstable_now,
isPrimaryRenderer: true,

View File

@ -20,6 +20,8 @@ import {
getNextLanes,
includesSyncLane,
markStarvedLanesAsExpired,
markRootEntangled,
mergeLanes,
} from './ReactFiberLane';
import {
CommitContext,
@ -49,7 +51,11 @@ import {
IdleEventPriority,
lanesToEventPriority,
} from './ReactEventPriorities';
import {supportsMicrotasks, scheduleMicrotask} from './ReactFiberConfig';
import {
supportsMicrotasks,
scheduleMicrotask,
shouldAttemptEagerTransition,
} from './ReactFiberConfig';
import ReactSharedInternals from 'shared/ReactSharedInternals';
const {ReactCurrentActQueue} = ReactSharedInternals;
@ -72,6 +78,8 @@ let mightHavePendingSyncWork: boolean = false;
let isFlushingWork: boolean = false;
let currentEventTransitionLane: Lane = NoLanes;
export function ensureRootIsScheduled(root: FiberRoot): void {
// This function is called whenever a root receives an update. It does two
// things 1) it ensures the root is in the root schedule, and 2) it ensures
@ -238,6 +246,14 @@ function processRootScheduleInMicrotask() {
let root = firstScheduledRoot;
while (root !== null) {
const next = root.next;
if (
currentEventTransitionLane !== NoLane &&
shouldAttemptEagerTransition()
) {
markRootEntangled(root, mergeLanes(currentEventTransitionLane, SyncLane));
}
const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
if (nextLanes === NoLane) {
// This root has no more pending work. Remove it from the schedule. To
@ -267,6 +283,8 @@ function processRootScheduleInMicrotask() {
root = next;
}
currentEventTransitionLane = NoLane;
// At the end of the microtask, flush any pending synchronous work. This has
// to come at the end, because it does actual rendering work that might throw.
flushSyncWorkOnAllRoots();
@ -472,3 +490,11 @@ function scheduleImmediateTask(cb: () => mixed) {
Scheduler_scheduleCallback(ImmediateSchedulerPriority, cb);
}
}
export function getCurrentEventTransitionLane(): Lane {
return currentEventTransitionLane;
}
export function setCurrentEventTransitionLane(lane: Lane): void {
currentEventTransitionLane = lane;
}

View File

@ -279,6 +279,8 @@ import {
flushSyncWorkOnAllRoots,
flushSyncWorkOnLegacyRootsOnly,
getContinuationForRoot,
getCurrentEventTransitionLane,
setCurrentEventTransitionLane,
} from './ReactFiberRootScheduler';
import {getMaskedContext, getUnmaskedContext} from './ReactFiberContext';
@ -583,8 +585,6 @@ const NESTED_PASSIVE_UPDATE_LIMIT = 50;
let nestedPassiveUpdateCount: number = 0;
let rootWithPassiveNestedUpdates: FiberRoot | null = null;
let currentEventTransitionLane: Lanes = NoLanes;
let isRunningInsertionEffect = false;
export function getWorkInProgressRoot(): FiberRoot | null {
@ -641,11 +641,11 @@ export function requestUpdateLane(fiber: Fiber): Lane {
// The trick we use is to cache the first of each of these inputs within an
// event. Then reset the cached values once we can be sure the event is
// over. Our heuristic for that is whenever we enter a concurrent work loop.
if (currentEventTransitionLane === NoLane) {
if (getCurrentEventTransitionLane() === NoLane) {
// All transitions within the same event are assigned the same lane.
currentEventTransitionLane = claimNextTransitionLane();
setCurrentEventTransitionLane(claimNextTransitionLane());
}
return currentEventTransitionLane;
return getCurrentEventTransitionLane();
}
// Updates originating inside certain React methods, like flushSync, have
@ -849,8 +849,6 @@ export function performConcurrentWorkOnRoot(
resetNestedUpdateFlag();
}
currentEventTransitionLane = NoLanes;
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}

View File

@ -70,6 +70,9 @@ describe('ReactFiberHostContext', () => {
getCurrentEventPriority: function () {
return DefaultEventPriority;
},
shouldAttemptEagerTransition() {
return false;
},
requestPostPaintCallback: function () {},
maySuspendCommit(type, props) {
return false;

View File

@ -66,6 +66,8 @@ export const preparePortalMount = $$$config.preparePortalMount;
export const prepareScopeUpdate = $$$config.prepareScopeUpdate;
export const getInstanceFromScope = $$$config.getInstanceFromScope;
export const getCurrentEventPriority = $$$config.getCurrentEventPriority;
export const shouldAttemptEagerTransition =
$$$config.shouldAttemptEagerTransition;
export const detachDeletedInstance = $$$config.detachDeletedInstance;
export const requestPostPaintCallback = $$$config.requestPostPaintCallback;
export const maySuspendCommit = $$$config.maySuspendCommit;

View File

@ -213,6 +213,9 @@ export function createTextInstance(
export function getCurrentEventPriority(): * {
return DefaultEventPriority;
}
export function shouldAttemptEagerTransition(): boolean {
return false;
}
export const isPrimaryRenderer = false;
export const warnsIfNotActing = true;