Compare commits

...

1 Commits

Author SHA1 Message Date
Rick Hanlon
6e02f5b4bd Add frame-end scheduling (V3)
Use frameAligned for DefaultUpdate

Modifications after rebasing onto batching lanes
2023-11-14 20:02:30 -08:00
22 changed files with 747 additions and 213 deletions

View File

@@ -33,6 +33,7 @@ import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostCo
import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities';
// TODO: Remove this deep import when we delete the legacy root API
import {ConcurrentMode, NoMode} from 'react-reconciler/src/ReactTypeOfMode';
import * as Scheduler from 'scheduler';
import hasOwnProperty from 'shared/hasOwnProperty';
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
@@ -623,6 +624,11 @@ const localRequestAnimationFrame =
typeof requestAnimationFrame === 'function'
? requestAnimationFrame
: scheduleTimeout;
const localCancelAnimationFrame =
typeof window !== 'undefined' &&
typeof window.cancelAnimationFrame === 'function'
? window.cancelAnimationFrame
: cancelTimeout;
export function getInstanceFromNode(node: HTMLElement): null | Object {
return getClosestInstanceFromNode(node) || null;
@@ -668,6 +674,68 @@ function handleErrorInNextTick(error: any) {
});
}
// -------------------
// Animation Frame
// -------------------
export const supportsFrameAlignedTask = true;
type FrameAlignedTask = {|
rafNode: AnimationFrameID,
schedulerNode: number | null,
task: Function,
|};
let currentTask: FrameAlignedTask | null = null;
function performFrameAlignedWork() {
if (currentTask != null) {
const currentTaskForFlow = currentTask;
const task = currentTask.task;
localCancelAnimationFrame(currentTaskForFlow.rafNode);
if (currentTaskForFlow.schedulerNode !== null) {
Scheduler.unstable_cancelCallback(currentTaskForFlow.schedulerNode);
}
currentTask = null;
if (task != null) {
task();
}
}
}
export function scheduleFrameAlignedTask(task: any): any {
if (currentTask === null) {
const rafNode = localRequestAnimationFrame(performFrameAlignedWork);
const schedulerNode = Scheduler.unstable_scheduleCallback(
Scheduler.unstable_NormalPriority,
performFrameAlignedWork,
);
currentTask = {
rafNode,
schedulerNode,
task,
};
} else {
currentTask.task = task;
currentTask.schedulerNode = Scheduler.unstable_scheduleCallback(
Scheduler.unstable_NormalPriority,
performFrameAlignedWork,
);
}
return currentTask;
}
export function cancelFrameAlignedTask(task: any) {
if (task.schedulerNode) {
Scheduler.unstable_cancelCallback(task.schedulerNode);
task.schedulerNode = null;
}
// We don't cancel the rAF in case it gets re-used later.
// But clear the task so if it fires and shouldn't run, it won't.
task.task = null;
}
// -------------------
// Mutation
// -------------------

View File

@@ -472,6 +472,392 @@ describe('ReactDOMFiberAsync', () => {
// Therefore the form should have been submitted.
expect(formSubmitted).toBe(true);
});
// @gate enableFrameEndScheduling
it('Unknown update followed by default update is batched, scheduled in a rAF', async () => {
let setState = null;
let counterRef = null;
function Counter() {
const [count, setCount] = React.useState(0);
const ref = React.useRef();
setState = setCount;
counterRef = ref;
Scheduler.unstable_yieldValue('Count: ' + count);
return <p ref={ref}>Count: {count}</p>;
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Counter />);
});
expect(Scheduler).toHaveYielded(['Count: 0']);
window.event = undefined;
setState(1);
// Unknown updates should schedule a rAF.
expect(global.requestAnimationFrameQueue.length).toBe(1);
window.event = 'test';
setState(2);
// Default updates after unknown should re-use the scheduled rAF.
expect(global.requestAnimationFrameQueue.length).toBe(1);
expect(Scheduler).toHaveYielded([]);
expect(counterRef.current.textContent).toBe('Count: 0');
global.flushRequestAnimationFrameQueue();
expect(Scheduler).toHaveYielded(['Count: 2']);
expect(counterRef.current.textContent).toBe('Count: 2');
});
// @gate enableFrameEndScheduling
it('Unknown update followed by default update is batched, scheduled in a task', () => {
let setState = null;
let counterRef = null;
function Counter() {
const [count, setCount] = React.useState(0);
const ref = React.useRef();
setState = setCount;
counterRef = ref;
Scheduler.unstable_yieldValue('Count: ' + count);
return <p ref={ref}>Count: {count}</p>;
}
const root = ReactDOMClient.createRoot(container);
act(() => {
root.render(<Counter />);
});
expect(Scheduler).toHaveYielded(['Count: 0']);
window.event = undefined;
setState(1);
// Unknown updates should schedule a rAF.
expect(global.requestAnimationFrameQueue.length).toBe(1);
window.event = 'test';
setState(2);
// Default updates after unknown should re-use the scheduled rAF.
expect(global.requestAnimationFrameQueue.length).toBe(1);
expect(Scheduler).toHaveYielded([]);
expect(counterRef.current.textContent).toBe('Count: 0');
expect(Scheduler).toFlushAndYield(['Count: 2']);
expect(counterRef.current.textContent).toBe('Count: 2');
});
// @gate enableFrameEndScheduling
it('Should re-use scheduled rAF, not cancel and schedule anew', async () => {
let setState = null;
let counterRef = null;
function Counter() {
const [count, setCount] = React.useState(0);
const ref = React.useRef();
setState = setCount;
counterRef = ref;
Scheduler.unstable_yieldValue('Count: ' + count);
return <p ref={ref}>Count: {count}</p>;
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Counter />);
});
expect(Scheduler).toHaveYielded(['Count: 0']);
window.event = undefined;
setState(1);
// Unknown updates should schedule a rAF.
expect(global.requestAnimationFrameQueue.length).toBe(1);
const firstRaf = global.requestAnimationFrameQueue[0];
setState(2);
// Default updates after unknown should re-use the scheduled rAF.
expect(global.requestAnimationFrameQueue.length).toBe(1);
const secondRaf = global.requestAnimationFrameQueue[0];
expect(firstRaf).toBe(secondRaf);
expect(Scheduler).toHaveYielded([]);
expect(counterRef.current.textContent).toBe('Count: 0');
global.flushRequestAnimationFrameQueue();
expect(Scheduler).toHaveYielded(['Count: 2']);
expect(counterRef.current.textContent).toBe('Count: 2');
});
// @gate enableFrameEndScheduling
it('Default update followed by an unknown update is batched, scheduled in a rAF', async () => {
let setState = null;
let counterRef = null;
function Counter() {
const [count, setCount] = React.useState(0);
const ref = React.useRef();
setState = setCount;
counterRef = ref;
Scheduler.unstable_yieldValue('Count: ' + count);
return <p ref={ref}>Count: {count}</p>;
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Counter />);
});
expect(Scheduler).toHaveYielded(['Count: 0']);
window.event = 'test';
setState(1);
// We should schedule a rAF for default updates.
expect(global.requestAnimationFrameQueue.length).toBe(1);
window.event = undefined;
setState(2);
// Unknown updates should schedule a rAF.
expect(global.requestAnimationFrameQueue.length).toBe(1);
expect(Scheduler).toHaveYielded([]);
expect(counterRef.current.textContent).toBe('Count: 0');
global.flushRequestAnimationFrameQueue();
expect(Scheduler).toHaveYielded(['Count: 2']);
expect(counterRef.current.textContent).toBe('Count: 2');
});
// @gate enableFrameEndScheduling
it('Default update followed by unknown update is batched, scheduled in a task', async () => {
let setState = null;
let counterRef = null;
function Counter() {
const [count, setCount] = React.useState(0);
const ref = React.useRef();
setState = setCount;
counterRef = ref;
Scheduler.unstable_yieldValue('Count: ' + count);
return <p ref={ref}>Count: {count}</p>;
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Counter />);
});
expect(Scheduler).toHaveYielded(['Count: 0']);
window.event = 'test';
setState(1);
// We should schedule a rAF for default updates.
expect(global.requestAnimationFrameQueue.length).toBe(1);
window.event = undefined;
setState(2);
expect(global.requestAnimationFrameQueue.length).toBe(1);
expect(Scheduler).toHaveYielded([]);
expect(counterRef.current.textContent).toBe('Count: 0');
expect(Scheduler).toFlushAndYield(['Count: 2']);
expect(counterRef.current.textContent).toBe('Count: 2');
});
// @gate enableFrameEndScheduling || !allowConcurrentByDefault
it('When allowConcurrentByDefault is enabled, unknown updates should not be time sliced', async () => {
let setState = null;
let counterRef = null;
function Counter() {
const [count, setCount] = React.useState(0);
const ref = React.useRef();
setState = setCount;
counterRef = ref;
Scheduler.unstable_yieldValue('Count: ' + count);
return <p ref={ref}>Count: {count}</p>;
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Counter />);
});
expect(Scheduler).toHaveYielded(['Count: 0']);
window.event = undefined;
setState(1);
expect(Scheduler).toFlushAndYieldThrough(['Count: 1']);
expect(counterRef.current.textContent).toBe('Count: 1');
});
// @gate enableFrameEndScheduling || !allowConcurrentByDefault
it('When allowConcurrentByDefault is enabled, unknown updates should not be time sliced event with default first', async () => {
let setState = null;
let counterRef = null;
function Counter() {
const [count, setCount] = React.useState(0);
const ref = React.useRef();
setState = setCount;
counterRef = ref;
Scheduler.unstable_yieldValue('Count: ' + count);
return <p ref={ref}>Count: {count}</p>;
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Counter />);
});
expect(Scheduler).toHaveYielded(['Count: 0']);
window.event = 'test';
setState(1);
window.event = undefined;
setState(2);
expect(Scheduler).toFlushAndYieldThrough(['Count: 2']);
expect(counterRef.current.textContent).toBe('Count: 2');
});
// @gate enableFrameEndScheduling || !allowConcurrentByDefault
it('When allowConcurrentByDefault is enabled, unknown updates should not be time sliced event with default after', async () => {
let setState = null;
let counterRef = null;
function Counter() {
const [count, setCount] = React.useState(0);
const ref = React.useRef();
setState = setCount;
counterRef = ref;
Scheduler.unstable_yieldValue('Count: ' + count);
return <p ref={ref}>Count: {count}</p>;
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Counter />);
});
expect(Scheduler).toHaveYielded(['Count: 0']);
window.event = undefined;
setState(1);
window.event = 'test';
setState(2);
expect(Scheduler).toFlushAndYieldThrough(['Count: 2']);
expect(counterRef.current.textContent).toBe('Count: 2');
});
// @gate enableFrameEndScheduling
it('unknown updates should be rescheduled in rAF after a higher priority update', async () => {
let setState = null;
let counterRef = null;
function Counter() {
const [count, setCount] = React.useState(0);
const ref = React.useRef();
setState = setCount;
counterRef = ref;
Scheduler.unstable_yieldValue('Count: ' + count);
return (
<p
ref={ref}
onClick={() => {
setCount(c => c + 1);
}}>
Count: {count}
</p>
);
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Counter />);
});
expect(Scheduler).toHaveYielded(['Count: 0']);
window.event = undefined;
setState(1);
// Dispatch a click event on the button.
await act(async () => {
const firstEvent = document.createEvent('Event');
firstEvent.initEvent('click', true, true);
counterRef.current.dispatchEvent(firstEvent);
});
if (gate(flags => flags.enableUnifiedSyncLane)) {
expect(Scheduler).toHaveYielded(['Count: 2']);
expect(counterRef.current.textContent).toBe('Count: 2');
} else {
expect(Scheduler).toHaveYielded(['Count: 1']);
expect(counterRef.current.textContent).toBe('Count: 1');
global.flushRequestAnimationFrameQueue();
expect(Scheduler).toHaveYielded(['Count: 2']);
expect(counterRef.current.textContent).toBe('Count: 2');
}
});
// @gate enableFrameEndScheduling && enableUnifiedSyncLane
it('unknown updates should be rescheduled in rAF after suspending without a boundary', async () => {
let setState = null;
let setThrowing = null;
let counterRef = null;
let promise = null;
let unsuspend = null;
function Counter() {
const [count, setCount] = React.useState(0);
const [isThrowing, setThrowingState] = React.useState(false);
setThrowing = setThrowingState;
const ref = React.useRef();
setState = setCount;
counterRef = ref;
Scheduler.unstable_yieldValue('Count: ' + count);
if (isThrowing) {
if (promise === null) {
promise = new Promise(resolve => {
unsuspend = () => {
resolve();
};
});
}
Scheduler.unstable_yieldValue('suspending');
throw promise;
}
return (
<p
ref={ref}
onClick={() => {
setCount(c => c + 1);
}}>
Count: {count}
</p>
);
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Counter />);
});
expect(Scheduler).toHaveYielded(['Count: 0']);
window.event = undefined;
setState(1);
expect(global.requestAnimationFrameQueue.length).toBe(1);
global.flushRequestAnimationFrameQueue();
expect(Scheduler).toHaveYielded(['Count: 1']);
setState(2);
setThrowing(true);
expect(global.requestAnimationFrameQueue.length).toBe(1);
global.flushRequestAnimationFrameQueue();
expect(Scheduler).toHaveYielded(['Count: 2', 'suspending']);
expect(counterRef.current.textContent).toBe('Count: 1');
unsuspend();
// Default update should be scheduled in a rAF.
window.event = 'test';
setThrowing(false);
setState(2);
global.flushRequestAnimationFrameQueue();
expect(Scheduler).toHaveYielded(['Count: 2']);
expect(counterRef.current.textContent).toBe('Count: 2');
});
});
it('regression test: does not drop passive effects across roots (#17066)', async () => {

View File

@@ -408,6 +408,9 @@ describe('ReactDOMRoot', () => {
expect(container.textContent).toEqual('a');
// Set an event so this isn't flushed synchronously as an unknown update.
window.event = 'test';
await act(async () => {
root.render(<Foo value="b" />);
@@ -415,7 +418,12 @@ describe('ReactDOMRoot', () => {
expect(container.textContent).toEqual('a');
await waitFor(['b']);
if (gate(flags => flags.allowConcurrentByDefault)) {
if (
gate(
flags =>
flags.allowConcurrentByDefault && !flags.enableFrameEndScheduling,
)
) {
expect(container.textContent).toEqual('a');
} else {
expect(container.textContent).toEqual('b');

View File

@@ -1404,7 +1404,11 @@ describe('ReactDOMServerPartialHydration', () => {
// While we're part way through the hydration, we update the state.
// This will schedule an update on the children of the suspense boundary.
expect(() => updateText('Hi')).toErrorDev(
expect(() => {
act(() => {
updateText('Hi');
});
}).toErrorDev(
"Can't perform a React state update on a component that hasn't mounted yet.",
);

View File

@@ -142,24 +142,24 @@ describe('useId', () => {
ReactDOMClient.hydrateRoot(container, <App />);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div>
<div>
<div
id="101"
/>
<div
id="1001"
/>
</div>
<div
id="10"
/>
</div>
</div>
`);
<div
id="container"
>
<div>
<div>
<div
id="101"
/>
<div
id="1001"
/>
</div>
<div
id="10"
/>
</div>
</div>
`);
});
test('indirections', async () => {
@@ -187,24 +187,24 @@ describe('useId', () => {
ReactDOMClient.hydrateRoot(container, <App />);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div
id="0"
>
<div>
<div>
<div>
<div
id="1"
/>
</div>
</div>
</div>
</div>
</div>
`);
<div
id="container"
>
<div
id="0"
>
<div>
<div>
<div>
<div
id="1"
/>
</div>
</div>
</div>
</div>
</div>
`);
});
test('StrictMode double rendering', async () => {
@@ -226,14 +226,14 @@ describe('useId', () => {
ReactDOMClient.hydrateRoot(container, <App />);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div
id="0"
/>
</div>
`);
<div
id="container"
>
<div
id="0"
/>
</div>
`);
});
test('empty (null) children', async () => {
@@ -262,17 +262,17 @@ describe('useId', () => {
ReactDOMClient.hydrateRoot(container, <App />);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div
id="10"
/>
<div
id="100"
/>
</div>
`);
<div
id="container"
>
<div
id="10"
/>
<div
id="100"
/>
</div>
`);
});
test('large ids', async () => {
@@ -342,12 +342,12 @@ describe('useId', () => {
});
// We append a suffix to the end of the id to distinguish them
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
:R0:, :R0H1:, :R0H2:
</div>
`);
<div
id="container"
>
:R0:, :R0H1:, :R0H2:
</div>
`);
});
test('local render phase updates', async () => {
@@ -367,12 +367,12 @@ describe('useId', () => {
ReactDOMClient.hydrateRoot(container, <App />);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
:R0:
</div>
`);
<div
id="container"
>
:R0:
</div>
`);
});
test('basic incremental hydration', async () => {
@@ -396,24 +396,24 @@ describe('useId', () => {
ReactDOMClient.hydrateRoot(container, <App />);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div>
<!--$-->
<div
id="101"
/>
<div
id="1001"
/>
<!--/$-->
<div
id="10"
/>
</div>
</div>
`);
<div
id="container"
>
<div>
<!--$-->
<div
id="101"
/>
<div
id="1001"
/>
<!--/$-->
<div
id="10"
/>
</div>
</div>
`);
});
test('inserting/deleting siblings outside a dehydrated Suspense boundary', async () => {
@@ -447,26 +447,26 @@ describe('useId', () => {
const root = ReactDOMClient.hydrateRoot(container, <App />);
await waitForPaint([]);
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div
id="101"
/>
<div
id="1001"
/>
<div
id="1101"
/>
<!--$-->
<div
id="110"
/>
<span />
<!--/$-->
</div>
`);
<div
id="container"
>
<div
id="101"
/>
<div
id="1001"
/>
<div
id="1101"
/>
<!--$-->
<div
id="110"
/>
<span />
<!--/$-->
</div>
`);
// The inner boundary hasn't hydrated yet
expect(span.current).toBe(null);
@@ -476,26 +476,26 @@ describe('useId', () => {
});
// The swap should not have caused a mismatch.
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div
id="101"
/>
<div
id="CLIENT_GENERATED_ID"
/>
<div
id="1101"
/>
<!--$-->
<div
id="110"
/>
<span />
<!--/$-->
</div>
`);
<div
id="container"
>
<div
id="101"
/>
<div
id="CLIENT_GENERATED_ID"
/>
<div
id="1101"
/>
<!--$-->
<div
id="110"
/>
<span />
<!--/$-->
</div>
`);
// Should have hydrated successfully
expect(span.current).toBe(dehydratedSpan);
});
@@ -528,23 +528,23 @@ describe('useId', () => {
const root = ReactDOMClient.hydrateRoot(container, <App />);
await waitForPaint([]);
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<!--$-->
<div
id="101"
/>
<div
id="1001"
/>
<div
id="1101"
/>
<span />
<!--/$-->
</div>
`);
<div
id="container"
>
<!--$-->
<div
id="101"
/>
<div
id="1001"
/>
<div
id="1101"
/>
<span />
<!--/$-->
</div>
`);
// The inner boundary hasn't hydrated yet
expect(span.current).toBe(null);
@@ -554,23 +554,23 @@ describe('useId', () => {
});
// The swap should not have caused a mismatch.
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<!--$-->
<div
id="101"
/>
<div
id="CLIENT_GENERATED_ID"
/>
<div
id="1101"
/>
<span />
<!--/$-->
</div>
`);
<div
id="container"
>
<!--$-->
<div
id="101"
/>
<div
id="CLIENT_GENERATED_ID"
/>
<div
id="1101"
/>
<span />
<!--/$-->
</div>
`);
// Should have hydrated successfully
expect(span.current).toBe(dehydratedSpan);
});
@@ -604,37 +604,37 @@ describe('useId', () => {
});
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div>
:custom-prefix-R1:
</div>
<div>
:custom-prefix-R2:
</div>
</div>
`);
<div
id="container"
>
<div>
:custom-prefix-R1:
</div>
<div>
:custom-prefix-R2:
</div>
</div>
`);
// Mount a new, client-only id
await clientAct(async () => {
root.render(<App showMore={true} />);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div>
:custom-prefix-R1:
</div>
<div>
:custom-prefix-R2:
</div>
<div>
:custom-prefix-r0:
</div>
</div>
`);
<div
id="container"
>
<div>
:custom-prefix-R1:
</div>
<div>
:custom-prefix-R2:
</div>
<div>
:custom-prefix-r0:
</div>
</div>
`);
});
// https://github.com/vercel/next.js/issues/43033
@@ -668,36 +668,36 @@ describe('useId', () => {
pipe(writable);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div>
:R0:
<!-- -->
<div>
:R7:
</div>
</div>
</div>
`);
<div
id="container"
>
<div>
:R0:
<!-- -->
<div>
:R7:
</div>
</div>
</div>
`);
await clientAct(async () => {
ReactDOMClient.hydrateRoot(container, <App />);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div>
:R0:
<!-- -->
<div>
:R7:
</div>
</div>
</div>
`);
<div
id="container"
>
<div>
:R0:
<!-- -->
<div>
:R7:
</div>
</div>
</div>
`);
});
});

View File

@@ -499,6 +499,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
})
: setTimeout,
supportsFrameAlignedTask: false,
scheduleFrameAlignedTask: undefined,
cancelFrameAlignedTask: undefined,
prepareForCommit(): null | Object {
return null;
},

View File

@@ -198,6 +198,17 @@ Set this to true to indicate that your renderer supports `scheduleMicrotask`. We
Optional. You can proxy this to `queueMicrotask` or its equivalent in your environment.
#### `supportsFrameAlignedTask`
TODO
### `scheduleFrameAlignedTask(fn)`
TODO
#### `cancelFrameAlignedTask(fn)`
TODO
#### `isPrimaryRenderer`
This is a property (not a function) that should be set to `true` if your renderer is the main one on the page. For example, if you're writing a renderer for the Terminal, it makes sense to set it to `true`, but if your renderer is used *on top of* React DOM or some other existing renderer, set it to `false`.

View File

@@ -21,3 +21,6 @@ function shim(...args: any): empty {
// Test selectors (when unsupported)
export const supportsMicrotasks = false;
export const scheduleMicrotask = shim;
export const supportsFrameAlignedTask = false;
export const scheduleFrameAlignedTask = shim;
export const cancelFrameAlignedTask = shim;

View File

@@ -24,6 +24,7 @@ import {
allowConcurrentByDefault,
enableTransitionTracing,
enableUnifiedSyncLane,
enableFrameEndScheduling,
} from 'shared/ReactFeatureFlags';
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
@@ -498,7 +499,12 @@ export function includesBlockingLane(root: FiberRoot, lanes: Lanes): boolean {
allowConcurrentByDefault &&
(root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode
) {
// Concurrent updates by default always use time slicing.
if (enableFrameEndScheduling && (lanes & DefaultLane) !== NoLanes) {
// Unknown updates should flush synchronously, even in concurrent by default.
return true;
}
// Otherwise, concurrent updates by default always use time slicing.
return false;
}
const SyncDefaultLanes =

View File

@@ -37,6 +37,7 @@ import {
enableUpdaterTracking,
enableCache,
enableTransitionTracing,
enableFrameEndScheduling,
useModernStrictMode,
disableLegacyContext,
alwaysThrottleRetries,

View File

@@ -86,6 +86,7 @@ describe('ReactFiberHostContext', () => {
return null;
},
supportsMutation: true,
shouldScheduleAnimationFrame: () => false,
});
const container = Renderer.createContainer(

View File

@@ -84,6 +84,14 @@ export const NotPendingTransition = $$$config.NotPendingTransition;
export const supportsMicrotasks = $$$config.supportsMicrotasks;
export const scheduleMicrotask = $$$config.scheduleMicrotask;
// -------------------
// Animation Frame
// (optional)
// -------------------
export const supportsFrameAlignedTask = $$$config.supportsFrameAlignedTask;
export const scheduleFrameAlignedTask = $$$config.scheduleFrameAlignedTask;
export const cancelFrameAlignedTask = $$$config.cancelFrameAlignedTask;
// -------------------
// Test selectors
// (optional)

View File

@@ -89,6 +89,7 @@ export const enableBinaryFlight = __EXPERIMENTAL__;
export const enableTaint = __EXPERIMENTAL__;
export const enablePostpone = __EXPERIMENTAL__;
export const enableFrameEndScheduling = __EXPERIMENTAL__;
export const enableTransitionTracing = false;

View File

@@ -42,6 +42,7 @@ export const enableFormActions = true; // Doesn't affect Native
export const enableBinaryFlight = true;
export const enableTaint = true;
export const enablePostpone = false;
export const enableFrameEndScheduling = false;
export const enableSchedulerDebugging = false;
export const debugRenderPhaseSideEffectsForStrictMode = true;
export const disableJavaScriptURLs = false;

View File

@@ -27,6 +27,7 @@ export const enableFormActions = true; // Doesn't affect Native
export const enableBinaryFlight = true;
export const enableTaint = true;
export const enablePostpone = false;
export const enableFrameEndScheduling = false;
export const disableJavaScriptURLs = false;
export const disableCommentsAsDOMContainers = true;
export const disableInputAttributeSyncing = false;

View File

@@ -27,6 +27,7 @@ export const enableFormActions = true; // Doesn't affect Test Renderer
export const enableBinaryFlight = true;
export const enableTaint = true;
export const enablePostpone = false;
export const enableFrameEndScheduling = false;
export const disableJavaScriptURLs = false;
export const disableCommentsAsDOMContainers = true;
export const disableInputAttributeSyncing = false;

View File

@@ -27,6 +27,7 @@ export const enableFormActions = true; // Doesn't affect Test Renderer
export const enableBinaryFlight = true;
export const enableTaint = true;
export const enablePostpone = false;
export const enableFrameEndScheduling = false;
export const disableJavaScriptURLs = false;
export const disableCommentsAsDOMContainers = true;
export const disableInputAttributeSyncing = false;

View File

@@ -27,6 +27,7 @@ export const enableFormActions = true; // Doesn't affect Test Renderer
export const enableBinaryFlight = true;
export const enableTaint = true;
export const enablePostpone = false;
export const enableFrameEndScheduling = false;
export const enableSchedulerDebugging = false;
export const disableJavaScriptURLs = false;
export const disableCommentsAsDOMContainers = true;

View File

@@ -29,6 +29,7 @@ export const enableAsyncActions = __VARIANT__;
export const alwaysThrottleRetries = __VARIANT__;
export const enableDO_NOT_USE_disableStrictPassiveEffect = __VARIANT__;
export const enableUseDeferredValueInitialArg = __VARIANT__;
export const enableFrameEndScheduling = __VARIANT__;
// Enable this flag to help with concurrent mode debugging.
// It logs information to the console about React scheduling, rendering, and commit phases.

View File

@@ -24,6 +24,7 @@ export const {
enableUseRefAccessWarning,
enableLazyContextPropagation,
enableUnifiedSyncLane,
enableFrameEndScheduling,
enableTransitionTracing,
enableCustomElementPropertySupport,
enableDeferRootSchedulingToMicrotask,

View File

@@ -38,4 +38,28 @@ if (typeof window !== 'undefined') {
} else {
global.AbortController =
require('abortcontroller-polyfill/dist/cjs-ponyfill').AbortController;
// We need to mock rAF because Jest 26 does not flush rAF.
// Once we upgrade to Jest 27+, rAF is flushed every 16ms.
global.requestAnimationFrameQueue = null;
global.requestAnimationFrame = function (callback) {
if (global.requestAnimationFrameQueue == null) {
global.requestAnimationFrameQueue = [];
}
global.requestAnimationFrameQueue.push(callback);
return global.requestAnimationFrameQueue.length - 1;
};
global.cancelAnimationFrame = function (id) {
if (global.requestAnimationFrameQueue != null) {
global.requestAnimationFrameQueue.splice(id, 1);
}
};
global.flushRequestAnimationFrameQueue = function () {
if (global.requestAnimationFrameQueue != null) {
global.requestAnimationFrameQueue.forEach(callback => callback());
global.requestAnimationFrameQueue = null;
}
};
}

View File

@@ -52,6 +52,8 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
// loop must always fail the test!
beforeEach(() => {
global.infiniteLoopError = null;
// TODO: warn if this has not flushed.
global.requestAnimationFrameQueue = null;
});
afterEach(() => {
const error = global.infiniteLoopError;