From bc9818f24d55e681aea39ecae59b8f544318169d Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 6 Feb 2019 08:16:41 +0000 Subject: [PATCH] Scheduler.unstable_next (#14756) * Add Scheduler.unstable_next * Use Scheduler to prioritize updates Changes the implementation of syncUpdates, deferredUpdates, and interactiveUpdates to use runWithPriority, so This is the minimum integration between Scheduler and React needed to unblock use of the Scheduler.next API. * Add Scheduler.unstable_next * Use Scheduler to prioritize updates Changes the implementation of syncUpdates, deferredUpdates, and interactiveUpdates to use runWithPriority, so This is the minimum integration between Scheduler and React needed to unblock use of the Scheduler.next API. --- .../src/__tests__/ReactCache-test.internal.js | 22 +++ .../src/ReactFiberScheduler.js | 148 +++++++++--------- packages/react/src/ReactSharedInternals.js | 2 + .../npm/umd/scheduler.development.js | 8 + .../npm/umd/scheduler.production.min.js | 8 + .../npm/umd/scheduler.profiling.min.js | 8 + packages/scheduler/src/Scheduler.js | 32 ++++ packages/shared/forks/Scheduler.umd.js | 16 ++ 8 files changed, 166 insertions(+), 78 deletions(-) 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, };