From 17905198380c28b3338ecf5808a23279d4db5141 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Tue, 7 Jun 2022 21:38:11 -0400 Subject: [PATCH] V3 - set root.hasUnknownUpdates --- .../src/__tests__/ReactDOMFiberAsync-test.js | 119 ++++++++++++++++++ .../src/client/ReactDOMHostConfig.js | 37 +++--- .../src/createReactNoop.js | 6 +- packages/react-reconciler/README.md | 13 +- .../src/ReactEventPriorities.js | 5 + .../src/ReactEventPriorities.new.js | 1 + .../src/ReactEventPriorities.old.js | 1 + .../src/ReactFiberClassComponent.new.js | 29 ++++- .../src/ReactFiberClassComponent.old.js | 29 ++++- .../src/ReactFiberHooks.new.js | 33 ++++- .../src/ReactFiberHooks.old.js | 33 ++++- .../ReactFiberHostConfigWithNoMicrotasks.js | 6 +- .../src/ReactFiberLane.new.js | 11 +- .../src/ReactFiberLane.old.js | 11 +- .../src/ReactFiberReconciler.new.js | 12 +- .../src/ReactFiberReconciler.old.js | 16 ++- .../src/ReactFiberWorkLoop.new.js | 65 +++++----- .../src/ReactFiberWorkLoop.old.js | 65 +++++----- .../src/ReactInternalTypes.js | 1 + .../ReactFiberHostContext-test.internal.js | 2 +- .../src/forks/ReactFiberHostConfig.custom.js | 8 +- 21 files changed, 385 insertions(+), 118 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js index ff37654c055da..0b44868fd825a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js @@ -780,6 +780,125 @@ describe('ReactDOMFiberAsync', () => { 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 ( +

{ + setCount(c => c + 1); + }}> + Count: {count} +

+ ); + } + + const root = ReactDOMClient.createRoot(container, { + unstable_concurrentUpdatesByDefault: true, + }); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Count: 0']); + + window.event = undefined; + setState(1); + + // Dispatch a click event on the button. + const firstEvent = document.createEvent('Event'); + firstEvent.initEvent('click', true, true); + counterRef.current.dispatchEvent(firstEvent); + + await null; + + 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 + it('unknown updates should be rescheduled in rAF after a higher priority update', async () => { + let setState = null; + let setThrowing = null; + let counterRef = null; + + let promise = null; + let unsuspend = null; + let isResolved = false; + + 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 = () => { + isResolved = true; + resolve(); + }; + }); + } + Scheduler.unstable_yieldValue('suspending'); + throw promise; + } + return ( +

{ + setCount(c => c + 1); + }}> + Count: {count} +

+ ); + } + + const root = ReactDOMClient.createRoot(container, { + unstable_concurrentUpdatesByDefault: true, + }); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Count: 0']); + + window.event = undefined; + setState(1); + + act(() => { + setThrowing(true); + }); + + expect(Scheduler).toHaveYielded(['Count: 1', 'suspending']); + expect(counterRef.current.textContent).toBe('Count: 0'); + + act(() => { + unsuspend(); + setThrowing(false); + + // Should not be scheduled in a rAF. + window.event = 'test'; + setState(2); + }); + + expect(Scheduler).toHaveYielded(['Count: 2']); + expect(counterRef.current.textContent).toBe('Count: 2'); + }); }); it('regression test: does not drop passive effects across roots (#17066)', () => { diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 940cae28cff80..3dbff0f3574d2 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -68,10 +68,11 @@ import { import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags'; import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem'; -import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities'; +import {UnknownEventPriority} 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'; export type Type = string; export type Props = { @@ -368,7 +369,7 @@ export function createTextInstance( export function getCurrentEventPriority(): * { const currentEvent = window.event; if (currentEvent === undefined) { - return DefaultEventPriority; + return UnknownEventPriority; } return getEventPriority(currentEvent.type); } @@ -411,18 +412,26 @@ export const scheduleMicrotask: any = // ------------------- // requestAnimationFrame // ------------------- -export const scheduleAnimationFrame: any = localRequestAnimationFrame; -export const cancelAnimationFrame: any = localCancelAnimationFrame; -export const shouldScheduleAnimationFrame = - scheduleAnimationFrame != null && cancelAnimationFrame != null - ? function() { - return ( - typeof window !== 'undefined' && typeof window.event === 'undefined' - ); - } - : function() { - return false; - }; +export const supportsFrameEndTask = true; +export function scheduleFrameEndTask(task) { + // Schedule both tasks, we'll race them and use the first to fire. + return { + frameNode: localRequestAnimationFrame(task), + callbackNode: Scheduler.unstable_scheduleCallback( + Scheduler.unstable_NormalPriority, + task, + ), + }; +} +export function cancelFrameEndTask(task) { + if (task.frameNode != null) { + localCancelAnimationFrame(task.frameNode); + } + + if (task.callbackNode != null) { + Scheduler.unstable_cancelCallback(task.callbackNode); + } +} function handleErrorInNextTick(error) { setTimeout(() => { diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 881becf759a02..177777d7a2fba 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -434,9 +434,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { }) : setTimeout, - shouldScheduleAnimationFrame: () => false, - scheduleAnimationFrame: undefined, - cancelAnimationFrame: undefined, + supportsFrameEndTask: false, + scheduleFrameEndTask: undefined, + cancelFrameEndTask: undefined, prepareForCommit(): null | Object { return null; diff --git a/packages/react-reconciler/README.md b/packages/react-reconciler/README.md index 4ac1d04739b15..be4a098f67846 100644 --- a/packages/react-reconciler/README.md +++ b/packages/react-reconciler/README.md @@ -210,17 +210,16 @@ 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. -#### `shouldScheduleAnimationFrame` +#### `supportsFrameEndTask` +TODO -Function that returns whether the current event should additionally schedule an animation frame to flush default updated. We use this in React DOM when updates are schedule inside of events that need to flush before the next paint such as ResizeObserver. +### `scheduleFrameEndTask(fn)` -#### `scheduleAnimationFrame(fn)` +TODO -Optional. Function to schedule an animation frame. You can proxy this to `requestAnimationFrame` or its equivalent in your environment. +#### `cancelFrameEndTask(fn)` -#### `cancelAnimationFrame(fn)` - -Optional. Function to cancel a scheduled animation frame. You can proxy this to `cancelAnimationFrame` or its equivalent in your environment. +TODO #### `isPrimaryRenderer` diff --git a/packages/react-reconciler/src/ReactEventPriorities.js b/packages/react-reconciler/src/ReactEventPriorities.js index 46223aa70df55..a3f289815c5dc 100644 --- a/packages/react-reconciler/src/ReactEventPriorities.js +++ b/packages/react-reconciler/src/ReactEventPriorities.js @@ -10,6 +10,7 @@ import {enableNewReconciler} from 'shared/ReactFeatureFlags'; import { + UnknownEventPriority as UnknownEventPriority_old, DiscreteEventPriority as DiscreteEventPriority_old, ContinuousEventPriority as ContinuousEventPriority_old, DefaultEventPriority as DefaultEventPriority_old, @@ -21,6 +22,7 @@ import { } from './ReactEventPriorities.old'; import { + UnknownEventPriority as UnknownEventPriority_new, DiscreteEventPriority as DiscreteEventPriority_new, ContinuousEventPriority as ContinuousEventPriority_new, DefaultEventPriority as DefaultEventPriority_new, @@ -33,6 +35,9 @@ import { export opaque type EventPriority = number; +export const UnknownEventPriority: EventPriority = enableNewReconciler + ? (UnknownEventPriority_new: any) + : (UnknownEventPriority_old: any); export const DiscreteEventPriority: EventPriority = enableNewReconciler ? (DiscreteEventPriority_new: any) : (DiscreteEventPriority_old: any); diff --git a/packages/react-reconciler/src/ReactEventPriorities.new.js b/packages/react-reconciler/src/ReactEventPriorities.new.js index 96225c19b3f11..da6f1029f4089 100644 --- a/packages/react-reconciler/src/ReactEventPriorities.new.js +++ b/packages/react-reconciler/src/ReactEventPriorities.new.js @@ -21,6 +21,7 @@ import { export opaque type EventPriority = Lane; +export const UnknownEventPriority: EventPriority = NoLane; export const DiscreteEventPriority: EventPriority = SyncLane; export const ContinuousEventPriority: EventPriority = InputContinuousLane; export const DefaultEventPriority: EventPriority = DefaultLane; diff --git a/packages/react-reconciler/src/ReactEventPriorities.old.js b/packages/react-reconciler/src/ReactEventPriorities.old.js index 8db74b9397a83..ced8d8453dd92 100644 --- a/packages/react-reconciler/src/ReactEventPriorities.old.js +++ b/packages/react-reconciler/src/ReactEventPriorities.old.js @@ -21,6 +21,7 @@ import { export opaque type EventPriority = Lane; +export const UnknownEventPriority: EventPriority = NoLane; export const DiscreteEventPriority: EventPriority = SyncLane; export const ContinuousEventPriority: EventPriority = InputContinuousLane; export const DefaultEventPriority: EventPriority = DefaultLane; diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.new.js b/packages/react-reconciler/src/ReactFiberClassComponent.new.js index 45977bfa6fee8..25689906aed30 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.new.js @@ -71,6 +71,7 @@ import {readContext, checkIfContextChanged} from './ReactFiberNewContext.new'; import { requestEventTime, requestUpdateLane, + requestUpdateLane_isUnknownEventPriority, scheduleUpdateOnFiber, } from './ReactFiberWorkLoop.new'; import {logForceUpdateScheduled, logStateUpdateScheduled} from './DebugTracing'; @@ -205,7 +206,7 @@ const classComponentUpdater = { const fiber = getInstance(inst); const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); - + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update = createUpdate(eventTime, lane); update.payload = payload; if (callback !== undefined && callback !== null) { @@ -217,7 +218,13 @@ const classComponentUpdater = { const root = enqueueUpdate(fiber, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitions(root, fiber, lane); } @@ -238,6 +245,7 @@ const classComponentUpdater = { const fiber = getInstance(inst); const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update = createUpdate(eventTime, lane); update.tag = ReplaceState; @@ -252,7 +260,13 @@ const classComponentUpdater = { const root = enqueueUpdate(fiber, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitions(root, fiber, lane); } @@ -273,6 +287,7 @@ const classComponentUpdater = { const fiber = getInstance(inst); const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update = createUpdate(eventTime, lane); update.tag = ForceUpdate; @@ -286,7 +301,13 @@ const classComponentUpdater = { const root = enqueueUpdate(fiber, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitions(root, fiber, lane); } diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.old.js b/packages/react-reconciler/src/ReactFiberClassComponent.old.js index a921cf2f47ba9..422f2ecf1dd15 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.old.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.old.js @@ -71,6 +71,7 @@ import {readContext, checkIfContextChanged} from './ReactFiberNewContext.old'; import { requestEventTime, requestUpdateLane, + requestUpdateLane_isUnknownEventPriority, scheduleUpdateOnFiber, } from './ReactFiberWorkLoop.old'; import {logForceUpdateScheduled, logStateUpdateScheduled} from './DebugTracing'; @@ -205,7 +206,7 @@ const classComponentUpdater = { const fiber = getInstance(inst); const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); - + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update = createUpdate(eventTime, lane); update.payload = payload; if (callback !== undefined && callback !== null) { @@ -217,7 +218,13 @@ const classComponentUpdater = { const root = enqueueUpdate(fiber, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitions(root, fiber, lane); } @@ -238,6 +245,7 @@ const classComponentUpdater = { const fiber = getInstance(inst); const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update = createUpdate(eventTime, lane); update.tag = ReplaceState; @@ -252,7 +260,13 @@ const classComponentUpdater = { const root = enqueueUpdate(fiber, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitions(root, fiber, lane); } @@ -273,6 +287,7 @@ const classComponentUpdater = { const fiber = getInstance(inst); const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update = createUpdate(eventTime, lane); update.tag = ForceUpdate; @@ -286,7 +301,13 @@ const classComponentUpdater = { const root = enqueueUpdate(fiber, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitions(root, fiber, lane); } diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 697e87dd01379..ef9b2e26f7643 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -85,6 +85,7 @@ import { getWorkInProgressRoot, scheduleUpdateOnFiber, requestUpdateLane, + requestUpdateLane_isUnknownEventPriority, requestEventTime, markSkippedUpdateLanes, } from './ReactFiberWorkLoop.new'; @@ -1125,6 +1126,7 @@ function useMutableSource( setSnapshot(maybeNewSnapshot); const lane = requestUpdateLane(fiber); + // TODO: What to do about isUnknownEventPriority markRootMutableRead(root, lane); } // If the source mutated between render and now, @@ -1145,7 +1147,7 @@ function useMutableSource( // Record a pending mutable source update with the same expiration time. const lane = requestUpdateLane(fiber); - + // TODO: What to do about isUnknownEventPriority markRootMutableRead(root, lane); } catch (error) { // A selector might throw after a source mutation. @@ -2142,11 +2144,18 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { case HostRoot: { // Schedule an update on the cache boundary to trigger a refresh. const lane = requestUpdateLane(provider); + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const eventTime = requestEventTime(); const refreshUpdate = createLegacyQueueUpdate(eventTime, lane); const root = enqueueLegacyQueueUpdate(provider, refreshUpdate, lane); if (root !== null) { - scheduleUpdateOnFiber(root, provider, lane, eventTime); + scheduleUpdateOnFiber( + root, + provider, + lane, + eventTime, + isUnknownEventPriority, + ); entangleLegacyQueueTransitions(root, provider, lane); } @@ -2188,7 +2197,7 @@ function dispatchReducerAction( } const lane = requestUpdateLane(fiber); - + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update: Update = { lane, action, @@ -2203,7 +2212,13 @@ function dispatchReducerAction( const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane); if (root !== null) { const eventTime = requestEventTime(); - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitionUpdate(root, queue, lane); } } @@ -2227,7 +2242,7 @@ function dispatchSetState( } const lane = requestUpdateLane(fiber); - + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update: Update = { lane, action, @@ -2285,7 +2300,13 @@ function dispatchSetState( const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane); if (root !== null) { const eventTime = requestEventTime(); - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitionUpdate(root, queue, lane); } } diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index fc25083fe4253..28c8dbe2743aa 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -85,6 +85,7 @@ import { getWorkInProgressRoot, scheduleUpdateOnFiber, requestUpdateLane, + requestUpdateLane_isUnknownEventPriority, requestEventTime, markSkippedUpdateLanes, } from './ReactFiberWorkLoop.old'; @@ -1142,6 +1143,7 @@ function useMutableSource( setSnapshot(maybeNewSnapshot); const lane = requestUpdateLane(fiber); + // TODO: What to do about isUnknownEventPriority markRootMutableRead(root, lane); } // If the source mutated between render and now, @@ -1162,7 +1164,7 @@ function useMutableSource( // Record a pending mutable source update with the same expiration time. const lane = requestUpdateLane(fiber); - + // TODO: What to do about isUnknownEventPriority markRootMutableRead(root, lane); } catch (error) { // A selector might throw after a source mutation. @@ -2161,11 +2163,18 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { case HostRoot: { // Schedule an update on the cache boundary to trigger a refresh. const lane = requestUpdateLane(provider); + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const eventTime = requestEventTime(); const refreshUpdate = createLegacyQueueUpdate(eventTime, lane); const root = enqueueLegacyQueueUpdate(provider, refreshUpdate, lane); if (root !== null) { - scheduleUpdateOnFiber(root, provider, lane, eventTime); + scheduleUpdateOnFiber( + root, + provider, + lane, + eventTime, + isUnknownEventPriority, + ); entangleLegacyQueueTransitions(root, provider, lane); } @@ -2207,7 +2216,7 @@ function dispatchReducerAction( } const lane = requestUpdateLane(fiber); - + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update: Update = { lane, action, @@ -2222,7 +2231,13 @@ function dispatchReducerAction( const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane); if (root !== null) { const eventTime = requestEventTime(); - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitionUpdate(root, queue, lane); } } @@ -2246,7 +2261,7 @@ function dispatchSetState( } const lane = requestUpdateLane(fiber); - + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); const update: Update = { lane, action, @@ -2309,7 +2324,13 @@ function dispatchSetState( const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane); if (root !== null) { const eventTime = requestEventTime(); - scheduleUpdateOnFiber(root, fiber, lane, eventTime); + scheduleUpdateOnFiber( + root, + fiber, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitionUpdate(root, queue, lane); } } diff --git a/packages/react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks.js b/packages/react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks.js index 0236d342bb3ea..c4f0a04cfd3b2 100644 --- a/packages/react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks.js +++ b/packages/react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks.js @@ -23,6 +23,6 @@ export const supportsMicrotasks = false; export const scheduleMicrotask = shim; // Test selectors (when unsupported) -export const shouldScheduleAnimationFrame = () => false; -export const scheduleAnimationFrame = shim; -export const cancelAnimationFrame = shim; +export const supportsFrameEndTask = false; +export const scheduleFrameEndTask = shim; +export const cancelFrameEndTask = shim; diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index ccade312940d4..127a9ca08dd23 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -469,7 +469,11 @@ export function includesBlockingLane(root: FiberRoot, lanes: Lanes) { allowConcurrentByDefault && (root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode ) { - if (enableFrameEndScheduling && root.frameAlignedNode != null) { + if ( + enableFrameEndScheduling && + (lanes & DefaultLane) !== NoLanes && + root.hasUnknownUpdates != null + ) { // Unknown updates should flush synchronously, even in concurrent by default. return true; } @@ -581,9 +585,13 @@ export function markRootUpdated( root: FiberRoot, updateLane: Lane, eventTime: number, + isUnknownEventPriority: boolean, ) { root.pendingLanes |= updateLane; + if (isUnknownEventPriority) { + root.hasUnknownUpdates = true; + } // If there are any suspended transitions, it's possible this new update // could unblock them. Clear the suspended lanes so that we can try rendering // them again. @@ -611,6 +619,7 @@ export function markRootUpdated( export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) { root.suspendedLanes |= suspendedLanes; root.pingedLanes &= ~suspendedLanes; + root.hasUnknownUpdates = false; // The suspended lanes are no longer CPU-bound. Clear their expiration times. const expirationTimes = root.expirationTimes; diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js index f64c7e270ad74..8dee3eb616e6d 100644 --- a/packages/react-reconciler/src/ReactFiberLane.old.js +++ b/packages/react-reconciler/src/ReactFiberLane.old.js @@ -469,7 +469,11 @@ export function includesBlockingLane(root: FiberRoot, lanes: Lanes) { allowConcurrentByDefault && (root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode ) { - if (enableFrameEndScheduling && root.frameAlignedNode != null) { + if ( + enableFrameEndScheduling && + (lanes & DefaultLane) !== NoLanes && + root.hasUnknownUpdates != null + ) { // Unknown updates should flush synchronously, even in concurrent by default. return true; } @@ -581,9 +585,13 @@ export function markRootUpdated( root: FiberRoot, updateLane: Lane, eventTime: number, + isUnknownEventPriority: boolean, ) { root.pendingLanes |= updateLane; + if (isUnknownEventPriority) { + root.hasUnknownUpdates = true; + } // If there are any suspended transitions, it's possible this new update // could unblock them. Clear the suspended lanes so that we can try rendering // them again. @@ -611,6 +619,7 @@ export function markRootUpdated( export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) { root.suspendedLanes |= suspendedLanes; root.pingedLanes &= ~suspendedLanes; + root.hasUnknownUpdates = false; // The suspended lanes are no longer CPU-bound. Clear their expiration times. const expirationTimes = root.expirationTimes; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index f9899c93b9211..2d1a32a6be465 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -99,6 +99,7 @@ import { findHostInstancesForRefresh, } from './ReactFiberHotReloading.new'; import ReactVersion from 'shared/ReactVersion'; +import {requestUpdateLane_isUnknownEventPriority} from './ReactFiberWorkLoop.new'; export {registerMutableSourceForHydration} from './ReactMutableSource.new'; export {createPortal} from './ReactPortal'; export { @@ -309,6 +310,7 @@ export function createHydrationContainer( const current = root.current; const eventTime = requestEventTime(); const lane = requestUpdateLane(current); + // TODO what to do about isUnknownEventPriority here const update = createUpdate(eventTime, lane); update.callback = callback !== undefined && callback !== null ? callback : null; @@ -330,7 +332,7 @@ export function updateContainer( const current = container.current; const eventTime = requestEventTime(); const lane = requestUpdateLane(current); - + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); if (enableSchedulingProfiler) { markRenderScheduled(lane); } @@ -380,7 +382,13 @@ export function updateContainer( const root = enqueueUpdate(current, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, current, lane, eventTime); + scheduleUpdateOnFiber( + root, + current, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitions(root, current, lane); } diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index 58ee3656d501b..7bf8f0c79adcd 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -99,7 +99,12 @@ import { findHostInstancesForRefresh, } from './ReactFiberHotReloading.old'; import ReactVersion from 'shared/ReactVersion'; +<<<<<<< packages/react-reconciler/src/ReactFiberReconciler.old.js export {registerMutableSourceForHydration} from './ReactMutableSource.old'; +======= +import {requestUpdateLane_isUnknownEventPriority} from './ReactFiberWorkLoop.new'; +export {registerMutableSourceForHydration} from './ReactMutableSource.new'; +>>>>>>> packages/react-reconciler/src/ReactFiberReconciler.new.js export {createPortal} from './ReactPortal'; export { createComponentSelector, @@ -309,6 +314,7 @@ export function createHydrationContainer( const current = root.current; const eventTime = requestEventTime(); const lane = requestUpdateLane(current); + // TODO what to do about isUnknownEventPriority here const update = createUpdate(eventTime, lane); update.callback = callback !== undefined && callback !== null ? callback : null; @@ -330,7 +336,7 @@ export function updateContainer( const current = container.current; const eventTime = requestEventTime(); const lane = requestUpdateLane(current); - + const isUnknownEventPriority = requestUpdateLane_isUnknownEventPriority(); if (enableSchedulingProfiler) { markRenderScheduled(lane); } @@ -380,7 +386,13 @@ export function updateContainer( const root = enqueueUpdate(current, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, current, lane, eventTime); + scheduleUpdateOnFiber( + root, + current, + lane, + eventTime, + isUnknownEventPriority, + ); entangleTransitions(root, current, lane); } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 6b3b4dfb99b52..18189ff2d7b1d 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -80,11 +80,11 @@ import { afterActiveInstanceBlur, getCurrentEventPriority, supportsMicrotasks, - shouldScheduleAnimationFrame, - scheduleAnimationFrame, - cancelAnimationFrame, errorHydratingContainer, scheduleMicrotask, + cancelFrameEndTask, + scheduleFrameEndTask, + supportsFrameEndTask, } from './ReactFiberHostConfig'; import { createWorkInProgress, @@ -441,7 +441,14 @@ export function getCurrentTime() { return now(); } +let isUnknownEventPriority = false; + +export function requestUpdateLane_isUnknownEventPriority(): boolean { + return isUnknownEventPriority; +} + export function requestUpdateLane(fiber: Fiber): Lane { + isUnknownEventPriority = false; // Special cases const mode = fiber.mode; if ((mode & ConcurrentMode) === NoMode) { @@ -504,7 +511,11 @@ export function requestUpdateLane(fiber: Fiber): Lane { // The opaque type returned by the host config is internally a lane, so we can // use that directly. // TODO: Move this type conversion to the event priority module. - const eventLane: Lane = (getCurrentEventPriority(): any); + let eventLane: Lane = (getCurrentEventPriority(): any); + if (eventLane === NoLane) { + isUnknownEventPriority = true; + eventLane = DefaultLane; + } return eventLane; } @@ -527,6 +538,7 @@ export function scheduleUpdateOnFiber( fiber: Fiber, lane: Lane, eventTime: number, + isUnknownEventPriority: boolean, ) { checkForNestedUpdates(); @@ -543,7 +555,7 @@ export function scheduleUpdateOnFiber( } // Mark that the root has a pending update. - markRootUpdated(root, lane, eventTime); + markRootUpdated(root, lane, eventTime, isUnknownEventPriority); if ( (executionContext & RenderContext) !== NoLanes && @@ -687,7 +699,6 @@ export function isUnsafeClassRenderPhaseUpdate(fiber: Fiber) { // exiting a task. function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { const existingCallbackNode = root.callbackNode; - const existingFrameAlignedNode = root.frameAlignedNode; // Check if any lanes are being starved by other work. If so, mark them as // expired so we know to work on those next. @@ -742,9 +753,7 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { if ( enableFrameEndScheduling && newCallbackPriority === DefaultLane && - existingFrameAlignedNode == null && - typeof window !== 'undefined' && - typeof window.event === 'undefined' + root.hasUnknownUpdates ) { // Do nothing, we need to cancel the existing default task and schedule a rAF. } else { @@ -755,21 +764,21 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { if (existingCallbackNode !== null) { // Cancel the existing callback. We'll schedule a new one below. - cancelCallback(existingCallbackNode); - } - - if ( - enableFrameEndScheduling && - cancelAnimationFrame != null && - existingFrameAlignedNode != null - ) { - // Cancel the existing rAF. We'll schedule a new one below. - cancelAnimationFrame(existingFrameAlignedNode); + if ( + enableFrameEndScheduling && + supportsFrameEndTask && + existingCallbackNode != null && + // TODO: is there a better check for callbackNode type? + existingCallbackNode.frameNode != null + ) { + cancelFrameEndTask(existingCallbackNode); + } else { + cancelCallback(existingCallbackNode); + } } // Schedule a new callback. let newCallbackNode; - let newFrameAlignedNode; if (newCallbackPriority === SyncLane) { // Special case: Sync React callbacks are scheduled on a special // internal queue @@ -811,15 +820,11 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { newCallbackNode = null; } else if ( enableFrameEndScheduling && + supportsFrameEndTask && newCallbackPriority === DefaultLane && - shouldScheduleAnimationFrame() + root.hasUnknownUpdates ) { - // Schedule both tasks, we'll race them and use the first to fire. - newFrameAlignedNode = scheduleAnimationFrame( - performConcurrentWorkOnRoot.bind(null, root), - ); - newCallbackNode = scheduleCallback( - NormalSchedulerPriority, + newCallbackNode = scheduleFrameEndTask( performConcurrentWorkOnRoot.bind(null, root), ); } else { @@ -849,9 +854,6 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { root.callbackPriority = newCallbackPriority; root.callbackNode = newCallbackNode; - if (enableFrameEndScheduling) { - root.frameAlignedNode = newFrameAlignedNode; - } } // This is the entry point for every concurrent task, i.e. anything that @@ -2309,6 +2311,9 @@ function commitRootImpl( // Always call this before exiting `commitRoot`, to ensure that any // additional work on this root is scheduled. ensureRootIsScheduled(root, now()); + // Clear the unknown updates after we've scheduled. + // Ideally this would be in markRootCompleted, but that is called too soon. + root.hasUnknownUpdates = false; if (recoverableErrors !== null) { // There were errors during this render, but recovered from them without diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 5a6329190fb64..5082090562914 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -80,11 +80,11 @@ import { afterActiveInstanceBlur, getCurrentEventPriority, supportsMicrotasks, - shouldScheduleAnimationFrame, - scheduleAnimationFrame, - cancelAnimationFrame, errorHydratingContainer, scheduleMicrotask, + cancelFrameEndTask, + scheduleFrameEndTask, + supportsFrameEndTask, } from './ReactFiberHostConfig'; import { createWorkInProgress, @@ -441,7 +441,14 @@ export function getCurrentTime() { return now(); } +let isUnknownEventPriority = false; + +export function requestUpdateLane_isUnknownEventPriority(): boolean { + return isUnknownEventPriority; +} + export function requestUpdateLane(fiber: Fiber): Lane { + isUnknownEventPriority = false; // Special cases const mode = fiber.mode; if ((mode & ConcurrentMode) === NoMode) { @@ -504,7 +511,11 @@ export function requestUpdateLane(fiber: Fiber): Lane { // The opaque type returned by the host config is internally a lane, so we can // use that directly. // TODO: Move this type conversion to the event priority module. - const eventLane: Lane = (getCurrentEventPriority(): any); + let eventLane: Lane = (getCurrentEventPriority(): any); + if (eventLane === NoLane) { + isUnknownEventPriority = true; + eventLane = DefaultLane; + } return eventLane; } @@ -527,6 +538,7 @@ export function scheduleUpdateOnFiber( fiber: Fiber, lane: Lane, eventTime: number, + isUnknownEventPriority: boolean, ) { checkForNestedUpdates(); @@ -543,7 +555,7 @@ export function scheduleUpdateOnFiber( } // Mark that the root has a pending update. - markRootUpdated(root, lane, eventTime); + markRootUpdated(root, lane, eventTime, isUnknownEventPriority); if ( (executionContext & RenderContext) !== NoLanes && @@ -687,7 +699,6 @@ export function isUnsafeClassRenderPhaseUpdate(fiber: Fiber) { // exiting a task. function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { const existingCallbackNode = root.callbackNode; - const existingFrameAlignedNode = root.frameAlignedNode; // Check if any lanes are being starved by other work. If so, mark them as // expired so we know to work on those next. @@ -742,9 +753,7 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { if ( enableFrameEndScheduling && newCallbackPriority === DefaultLane && - existingFrameAlignedNode == null && - typeof window !== 'undefined' && - typeof window.event === 'undefined' + root.hasUnknownUpdates ) { // Do nothing, we need to cancel the existing default task and schedule a rAF. } else { @@ -755,21 +764,21 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { if (existingCallbackNode !== null) { // Cancel the existing callback. We'll schedule a new one below. - cancelCallback(existingCallbackNode); - } - - if ( - enableFrameEndScheduling && - cancelAnimationFrame != null && - existingFrameAlignedNode != null - ) { - // Cancel the existing rAF. We'll schedule a new one below. - cancelAnimationFrame(existingFrameAlignedNode); + if ( + enableFrameEndScheduling && + supportsFrameEndTask && + existingCallbackNode != null && + // TODO: is there a better check for callbackNode type? + existingCallbackNode.frameNode != null + ) { + cancelFrameEndTask(existingCallbackNode); + } else { + cancelCallback(existingCallbackNode); + } } // Schedule a new callback. let newCallbackNode; - let newFrameAlignedNode; if (newCallbackPriority === SyncLane) { // Special case: Sync React callbacks are scheduled on a special // internal queue @@ -811,15 +820,11 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { newCallbackNode = null; } else if ( enableFrameEndScheduling && + supportsFrameEndTask && newCallbackPriority === DefaultLane && - shouldScheduleAnimationFrame() + root.hasUnknownUpdates ) { - // Schedule both tasks, we'll race them and use the first to fire. - newFrameAlignedNode = scheduleAnimationFrame( - performConcurrentWorkOnRoot.bind(null, root), - ); - newCallbackNode = scheduleCallback( - NormalSchedulerPriority, + newCallbackNode = scheduleFrameEndTask( performConcurrentWorkOnRoot.bind(null, root), ); } else { @@ -849,9 +854,6 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { root.callbackPriority = newCallbackPriority; root.callbackNode = newCallbackNode; - if (enableFrameEndScheduling) { - root.frameAlignedNode = newFrameAlignedNode; - } } // This is the entry point for every concurrent task, i.e. anything that @@ -2303,6 +2305,9 @@ function commitRootImpl( // Always call this before exiting `commitRoot`, to ensure that any // additional work on this root is scheduled. ensureRootIsScheduled(root, now()); + // Clear the unknown updates after we've scheduled. + // Ideally this would be in markRootCompleted, but that is called too soon. + root.hasUnknownUpdates = false; if (recoverableErrors !== null) { // There were errors during this render, but recovered from them without diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index c1e1d0ced7d08..1e12bf3fa35d7 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -224,6 +224,7 @@ type BaseFiberRootProperties = {| callbackNode: *, callbackPriority: Lane, frameAlignedNode?: number | null, + hasUnknownUpdates?: boolean, eventTimes: LaneMap, expirationTimes: LaneMap, diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js index ecda3ede2419b..68ba1f814166d 100644 --- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js @@ -67,7 +67,7 @@ describe('ReactFiberHostContext', () => { return DefaultEventPriority; }, supportsMutation: true, - shouldScheduleAnimationFrame: () => false, + supportsFrameEndTask: false, }); const container = Renderer.createContainer( diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js index 354d0cf2eb6ee..303a4715c8145 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -80,10 +80,10 @@ export const scheduleMicrotask = $$$hostConfig.scheduleMicrotask; // Animation Frame // (optional) // ------------------- -export const shouldScheduleAnimationFrame = - $$$hostConfig.shouldScheduleAnimationFrame; -export const scheduleAnimationFrame = $$$hostConfig.scheduleAnimationFrame; -export const cancelAnimationFrame = $$$hostConfig.cancelAnimationFrame; +export const supportsFrameEndTask = + $$$hostConfig.supportsFrameEndTask; +export const scheduleFrameEndTask = $$$hostConfig.scheduleFrameEndTask; +export const cancelFrameEndTask = $$$hostConfig.cancelFrameEndTask; // ------------------- // Test selectors