diff --git a/packages/react-cache/src/__tests__/ReactCache-test.internal.js b/packages/react-cache/src/__tests__/ReactCache-test.internal.js index e87d53e063c40..36a6f0f84744b 100644 --- a/packages/react-cache/src/__tests__/ReactCache-test.internal.js +++ b/packages/react-cache/src/__tests__/ReactCache-test.internal.js @@ -24,6 +24,8 @@ describe('ReactCache', () => { beforeEach(() => { jest.resetModules(); + let currentPriorityLevel = 3; + jest.mock('scheduler', () => { let callbacks = []; return { @@ -38,6 +40,26 @@ describe('ReactCache', () => { callback(); } }, + + unstable_ImmediatePriority: 1, + unstable_UserBlockingPriority: 2, + unstable_NormalPriority: 3, + unstable_LowPriority: 4, + unstable_IdlePriority: 5, + + unstable_runWithPriority(priorityLevel, fn) { + const prevPriorityLevel = currentPriorityLevel; + currentPriorityLevel = priorityLevel; + try { + return fn(); + } finally { + currentPriorityLevel = prevPriorityLevel; + } + }, + + unstable_getCurrentPriorityLevel() { + return currentPriorityLevel; + }, }; }); diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 5712dbc9cd6d1..baa100f0d266a 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -15,8 +15,18 @@ import type {Interaction} from 'scheduler/src/Tracing'; import { __interactionsRef, __subscriberRef, - unstable_wrap as Schedule_tracing_wrap, + unstable_wrap as Scheduler_tracing_wrap, } from 'scheduler/tracing'; +import { + unstable_next as Scheduler_next, + unstable_getCurrentPriorityLevel as getCurrentPriorityLevel, + unstable_runWithPriority as runWithPriority, + unstable_ImmediatePriority as ImmediatePriority, + unstable_UserBlockingPriority as UserBlockingPriority, + unstable_NormalPriority as NormalPriority, + unstable_LowPriority as LowPriority, + unstable_IdlePriority as IdlePriority, +} from 'scheduler'; import { invokeGuardedCallback, hasCaughtError, @@ -122,7 +132,7 @@ import { computeAsyncExpiration, computeInteractiveExpiration, } from './ReactFiberExpirationTime'; -import {ConcurrentMode, ProfileMode} from './ReactTypeOfMode'; +import {ConcurrentMode, ProfileMode, NoContext} from './ReactTypeOfMode'; import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; import { @@ -242,11 +252,6 @@ if (__DEV__) { // Used to ensure computeUniqueAsyncExpiration is monotonically decreasing. let lastUniqueAsyncExpiration: number = Sync - 1; -// Represents the expiration time that incoming updates should use. (If this -// is NoWork, use the default strategy: async updates in async mode, sync -// updates in sync mode.) -let expirationContext: ExpirationTime = NoWork; - let isWorking: boolean = false; // The next work in progress fiber that we're currently working on. @@ -793,9 +798,11 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void { // TODO: Avoid this extra callback by mutating the tracing ref directly, // like we do at the beginning of commitRoot. I've opted not to do that // here because that code is still in flux. - callback = Schedule_tracing_wrap(callback); + callback = Scheduler_tracing_wrap(callback); } - passiveEffectCallbackHandle = schedulePassiveEffects(callback); + passiveEffectCallbackHandle = runWithPriority(NormalPriority, () => { + return schedulePassiveEffects(callback); + }); passiveEffectCallback = callback; } @@ -1579,52 +1586,58 @@ function computeUniqueAsyncExpiration(): ExpirationTime { } function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) { + const priorityLevel = getCurrentPriorityLevel(); + let expirationTime; - if (expirationContext !== NoWork) { - // An explicit expiration context was set; - expirationTime = expirationContext; - } else if (isWorking) { - if (isCommitting) { - // Updates that occur during the commit phase should have sync priority - // by default. - expirationTime = Sync; - } else { - // Updates during the render phase should expire at the same time as - // the work that is being rendered. - expirationTime = nextRenderExpirationTime; - } + if ((fiber.mode & ConcurrentMode) === NoContext) { + // Outside of concurrent mode, updates are always synchronous. + expirationTime = Sync; + } else if (isWorking && !isCommitting) { + // During render phase, updates expire during as the current render. + expirationTime = nextRenderExpirationTime; } else { - // No explicit expiration context was set, and we're not currently - // performing work. Calculate a new expiration time. - if (fiber.mode & ConcurrentMode) { - if (isBatchingInteractiveUpdates) { - // This is an interactive update + switch (priorityLevel) { + case ImmediatePriority: + expirationTime = Sync; + break; + case UserBlockingPriority: expirationTime = computeInteractiveExpiration(currentTime); - } else { - // This is an async update + break; + case NormalPriority: + // This is a normal, concurrent update expirationTime = computeAsyncExpiration(currentTime); - } - // If we're in the middle of rendering a tree, do not update at the same - // expiration time that is already rendering. - if (nextRoot !== null && expirationTime === nextRenderExpirationTime) { - expirationTime -= 1; - } - } else { - // This is a sync update - expirationTime = Sync; + break; + case LowPriority: + case IdlePriority: + expirationTime = Never; + break; + default: + invariant( + false, + 'Unknown priority level. This error is likely caused by a bug in ' + + 'React. Please file an issue.', + ); } - } - if (isBatchingInteractiveUpdates) { - // This is an interactive update. Keep track of the lowest pending - // interactive expiration time. This allows us to synchronously flush - // all interactive updates when needed. - if ( - lowestPriorityPendingInteractiveExpirationTime === NoWork || - expirationTime < lowestPriorityPendingInteractiveExpirationTime - ) { - lowestPriorityPendingInteractiveExpirationTime = expirationTime; + + // If we're in the middle of rendering a tree, do not update at the same + // expiration time that is already rendering. + if (nextRoot !== null && expirationTime === nextRenderExpirationTime) { + expirationTime -= 1; } } + + // Keep track of the lowest pending interactive expiration time. This + // allows us to synchronously flush all interactive updates + // when needed. + // TODO: Move this to renderer? + if ( + priorityLevel === UserBlockingPriority && + (lowestPriorityPendingInteractiveExpirationTime === NoWork || + expirationTime < lowestPriorityPendingInteractiveExpirationTime) + ) { + lowestPriorityPendingInteractiveExpirationTime = expirationTime; + } + return expirationTime; } @@ -1862,20 +1875,6 @@ function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) { } } -function deferredUpdates(fn: () => A): A { - const currentTime = requestCurrentTime(); - const previousExpirationContext = expirationContext; - const previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates; - expirationContext = computeAsyncExpiration(currentTime); - isBatchingInteractiveUpdates = false; - try { - return fn(); - } finally { - expirationContext = previousExpirationContext; - isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates; - } -} - function syncUpdates( fn: (A, B, C0, D) => R, a: A, @@ -1883,13 +1882,9 @@ function syncUpdates( c: C0, d: D, ): R { - const previousExpirationContext = expirationContext; - expirationContext = Sync; - try { + return runWithPriority(ImmediatePriority, () => { return fn(a, b, c, d); - } finally { - expirationContext = previousExpirationContext; - } + }); } // TODO: Everything below this is written as if it has been lifted to the @@ -1910,7 +1905,6 @@ let unhandledError: mixed | null = null; let isBatchingUpdates: boolean = false; let isUnbatchingUpdates: boolean = false; -let isBatchingInteractiveUpdates: boolean = false; let completedBatches: Array | null = null; @@ -2441,7 +2435,9 @@ function completeRoot( lastCommittedRootDuringThisBatch = root; nestedUpdateCount = 0; } - commitRoot(root, finishedWork); + runWithPriority(ImmediatePriority, () => { + commitRoot(root, finishedWork); + }); } function onUncaughtError(error: mixed) { @@ -2507,9 +2503,6 @@ function flushSync(fn: (a: A) => R, a: A): R { } function interactiveUpdates(fn: (A, B) => R, a: A, b: B): R { - if (isBatchingInteractiveUpdates) { - return fn(a, b); - } // If there are any pending interactive updates, synchronously flush them. // This needs to happen before we read any handlers, because the effect of // the previous event may influence which handlers are called during @@ -2523,14 +2516,13 @@ function interactiveUpdates(fn: (A, B) => R, a: A, b: B): R { performWork(lowestPriorityPendingInteractiveExpirationTime, false); lowestPriorityPendingInteractiveExpirationTime = NoWork; } - const previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates; const previousIsBatchingUpdates = isBatchingUpdates; - isBatchingInteractiveUpdates = true; isBatchingUpdates = true; try { - return fn(a, b); + return runWithPriority(UserBlockingPriority, () => { + return fn(a, b); + }); } finally { - isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates; isBatchingUpdates = previousIsBatchingUpdates; if (!isBatchingUpdates && !isRendering) { performSyncWork(); @@ -2580,7 +2572,7 @@ export { unbatchedUpdates, flushSync, flushControlled, - deferredUpdates, + Scheduler_next as deferredUpdates, syncUpdates, interactiveUpdates, flushInteractiveUpdates, diff --git a/packages/react/src/ReactSharedInternals.js b/packages/react/src/ReactSharedInternals.js index 9cdce1891bc5c..1fe0c2391bd13 100644 --- a/packages/react/src/ReactSharedInternals.js +++ b/packages/react/src/ReactSharedInternals.js @@ -12,6 +12,7 @@ import { unstable_now, unstable_scheduleCallback, unstable_runWithPriority, + unstable_next, unstable_getFirstCallbackNode, unstable_pauseExecution, unstable_continueExecution, @@ -53,6 +54,7 @@ if (__UMD__) { unstable_now, unstable_scheduleCallback, unstable_runWithPriority, + unstable_next, unstable_wrapCallback, unstable_getFirstCallbackNode, unstable_pauseExecution, diff --git a/packages/scheduler/npm/umd/scheduler.development.js b/packages/scheduler/npm/umd/scheduler.development.js index 41ac8e437bbd6..ac632eb288bff 100644 --- a/packages/scheduler/npm/umd/scheduler.development.js +++ b/packages/scheduler/npm/umd/scheduler.development.js @@ -54,6 +54,13 @@ ); } + function unstable_next() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_next.apply( + this, + arguments + ); + } + function unstable_wrapCallback() { return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_wrapCallback.apply( this, @@ -95,6 +102,7 @@ unstable_cancelCallback: unstable_cancelCallback, unstable_shouldYield: unstable_shouldYield, unstable_runWithPriority: unstable_runWithPriority, + unstable_next: unstable_next, unstable_wrapCallback: unstable_wrapCallback, unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel, unstable_continueExecution: unstable_continueExecution, diff --git a/packages/scheduler/npm/umd/scheduler.production.min.js b/packages/scheduler/npm/umd/scheduler.production.min.js index cea54f4da3cba..da2aefa9e4bf1 100644 --- a/packages/scheduler/npm/umd/scheduler.production.min.js +++ b/packages/scheduler/npm/umd/scheduler.production.min.js @@ -54,6 +54,13 @@ ); } + function unstable_next() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_next.apply( + this, + arguments + ); + } + function unstable_wrapCallback() { return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_wrapCallback.apply( this, @@ -89,6 +96,7 @@ unstable_cancelCallback: unstable_cancelCallback, unstable_shouldYield: unstable_shouldYield, unstable_runWithPriority: unstable_runWithPriority, + unstable_next: unstable_next, unstable_wrapCallback: unstable_wrapCallback, unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel, unstable_continueExecution: unstable_continueExecution, diff --git a/packages/scheduler/npm/umd/scheduler.profiling.min.js b/packages/scheduler/npm/umd/scheduler.profiling.min.js index cea54f4da3cba..da2aefa9e4bf1 100644 --- a/packages/scheduler/npm/umd/scheduler.profiling.min.js +++ b/packages/scheduler/npm/umd/scheduler.profiling.min.js @@ -54,6 +54,13 @@ ); } + function unstable_next() { + return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_next.apply( + this, + arguments + ); + } + function unstable_wrapCallback() { return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler.unstable_wrapCallback.apply( this, @@ -89,6 +96,7 @@ unstable_cancelCallback: unstable_cancelCallback, unstable_shouldYield: unstable_shouldYield, unstable_runWithPriority: unstable_runWithPriority, + unstable_next: unstable_next, unstable_wrapCallback: unstable_wrapCallback, unstable_getCurrentPriorityLevel: unstable_getCurrentPriorityLevel, unstable_continueExecution: unstable_continueExecution, diff --git a/packages/scheduler/src/Scheduler.js b/packages/scheduler/src/Scheduler.js index a6e27850dab71..df1e9b3bdada0 100644 --- a/packages/scheduler/src/Scheduler.js +++ b/packages/scheduler/src/Scheduler.js @@ -264,6 +264,37 @@ function unstable_runWithPriority(priorityLevel, eventHandler) { } } +function unstable_next(eventHandler) { + let priorityLevel; + switch (currentPriorityLevel) { + case ImmediatePriority: + case UserBlockingPriority: + case NormalPriority: + // Shift down to normal priority + priorityLevel = NormalPriority; + break; + default: + // Anything lower than normal priority should remain at the current level. + priorityLevel = currentPriorityLevel; + break; + } + + var previousPriorityLevel = currentPriorityLevel; + var previousEventStartTime = currentEventStartTime; + currentPriorityLevel = priorityLevel; + currentEventStartTime = getCurrentTime(); + + try { + return eventHandler(); + } finally { + currentPriorityLevel = previousPriorityLevel; + currentEventStartTime = previousEventStartTime; + + // Before exiting, flush all the immediate work that was scheduled. + flushImmediateWork(); + } +} + function unstable_wrapCallback(callback) { var parentPriorityLevel = currentPriorityLevel; return function() { @@ -688,6 +719,7 @@ export { IdlePriority as unstable_IdlePriority, LowPriority as unstable_LowPriority, unstable_runWithPriority, + unstable_next, unstable_scheduleCallback, unstable_cancelCallback, unstable_wrapCallback, diff --git a/packages/shared/forks/Scheduler.umd.js b/packages/shared/forks/Scheduler.umd.js index c33b2d0667416..6c9c918dc8779 100644 --- a/packages/shared/forks/Scheduler.umd.js +++ b/packages/shared/forks/Scheduler.umd.js @@ -17,8 +17,16 @@ const { unstable_scheduleCallback, unstable_shouldYield, unstable_getFirstCallbackNode, + unstable_runWithPriority, + unstable_next, unstable_continueExecution, unstable_pauseExecution, + unstable_getCurrentPriorityLevel, + unstable_ImmediatePriority, + unstable_UserBlockingPriority, + unstable_NormalPriority, + unstable_LowPriority, + unstable_IdlePriority, } = ReactInternals.Scheduler; export { @@ -27,6 +35,14 @@ export { unstable_scheduleCallback, unstable_shouldYield, unstable_getFirstCallbackNode, + unstable_runWithPriority, + unstable_next, unstable_continueExecution, unstable_pauseExecution, + unstable_getCurrentPriorityLevel, + unstable_ImmediatePriority, + unstable_UserBlockingPriority, + unstable_NormalPriority, + unstable_LowPriority, + unstable_IdlePriority, };