From ae24a13d671a9f5931216c0ac119a402189cad8a Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 9 Aug 2017 16:04:08 -0700 Subject: [PATCH 01/24] [Work-in-progress] Assign expiration times to updates An expiration time represents a time in the future by which an update should flush. The priority of the update is related to the difference between the current clock time and the expiration time. This has the effect of increasing the priority of updates as time progresses, to prevent starvation. This lays the initial groundwork for expiration times without changing any behavior. Future commits will replace work priority with expiration times. --- src/renderers/art/ReactARTFiberEntry.js | 5 + src/renderers/dom/fiber/ReactDOMFiberEntry.js | 18 +++ .../native-rt/ReactNativeRTFiberRenderer.js | 5 + .../native/ReactNativeFiberRenderer.js | 5 + src/renderers/noop/ReactNoopEntry.js | 5 + .../shared/fiber/ReactFiberBeginWork.js | 3 + .../shared/fiber/ReactFiberClassComponent.js | 11 +- .../shared/fiber/ReactFiberExpirationTime.js | 104 ++++++++++++++++++ .../shared/fiber/ReactFiberReconciler.js | 6 +- .../shared/fiber/ReactFiberScheduler.js | 14 +++ .../shared/fiber/ReactFiberUpdateQueue.js | 75 +++++++++++-- .../__tests__/ReactFiberHostContext-test.js | 3 + .../testing/ReactTestRendererFiberEntry.js | 5 + 13 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 src/renderers/shared/fiber/ReactFiberExpirationTime.js diff --git a/src/renderers/art/ReactARTFiberEntry.js b/src/renderers/art/ReactARTFiberEntry.js index a4e340c2f75e1..ea0c4d68649e6 100644 --- a/src/renderers/art/ReactARTFiberEntry.js +++ b/src/renderers/art/ReactARTFiberEntry.js @@ -532,6 +532,11 @@ const ARTRenderer = ReactFiberReconciler({ ); }, + now(): number { + // TODO: Enable expiration by implementing this method. + return 0; + }, + useSyncScheduling: true, }); diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index d0dd466bfb4fb..6076c61fcc19c 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -162,6 +162,22 @@ function shouldAutoFocusHostComponent(type: string, props: Props): boolean { return false; } +// TODO: Better polyfill +let now; +if ( + typeof window !== 'undefined' && + window.performance && + typeof window.performance.now === 'function' +) { + now = function() { + return performance.now(); + }; +} else { + now = function() { + return Date.now(); + }; +} + var DOMRenderer = ReactFiberReconciler({ getRootHostContext(rootContainerInstance: Container): HostContext { let type; @@ -437,6 +453,8 @@ var DOMRenderer = ReactFiberReconciler({ } }, + now: now, + canHydrateInstance( instance: Instance | TextInstance, type: string, diff --git a/src/renderers/native-rt/ReactNativeRTFiberRenderer.js b/src/renderers/native-rt/ReactNativeRTFiberRenderer.js index 799ee85c1f55e..443c84b1fe831 100644 --- a/src/renderers/native-rt/ReactNativeRTFiberRenderer.js +++ b/src/renderers/native-rt/ReactNativeRTFiberRenderer.js @@ -227,6 +227,11 @@ const NativeRTRenderer = ReactFiberReconciler({ }, useSyncScheduling: true, + + now(): number { + // TODO: Enable expiration by implementing this method. + return 0; + }, }); module.exports = NativeRTRenderer; diff --git a/src/renderers/native/ReactNativeFiberRenderer.js b/src/renderers/native/ReactNativeFiberRenderer.js index 8f36d4886a51a..859f422f4c2ac 100644 --- a/src/renderers/native/ReactNativeFiberRenderer.js +++ b/src/renderers/native/ReactNativeFiberRenderer.js @@ -377,6 +377,11 @@ const NativeRenderer = ReactFiberReconciler({ }, useSyncScheduling: true, + + now(): number { + // TODO: Enable expiration by implementing this method. + return 0; + }, }); module.exports = NativeRenderer; diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index cc98ecb920d94..9a2dcadaa3150 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -201,6 +201,11 @@ var NoopRenderer = ReactFiberReconciler({ prepareForCommit(): void {}, resetAfterCommit(): void {}, + + now(): number { + // TODO: Add an API to advance time. + return 0; + }, }); var rootContainers = new Map(); diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index ed81a1d778944..d308973277e46 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -17,6 +17,7 @@ import type {HydrationContext} from 'ReactFiberHydrationContext'; import type {FiberRoot} from 'ReactFiberRoot'; import type {HostConfig} from 'ReactFiberReconciler'; import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; var { mountChildFibersInPlace, @@ -73,6 +74,7 @@ module.exports = function( hydrationContext: HydrationContext, scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void, getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel, + recalculateCurrentTime: () => ExpirationTime, ) { const { shouldSetTextContent, @@ -99,6 +101,7 @@ module.exports = function( getPriorityContext, memoizeProps, memoizeState, + recalculateCurrentTime, ); function reconcileChildren(current, workInProgress, nextChildren) { diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 2e080e23bff42..4c72e6236b7c3 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -12,6 +12,7 @@ import type {Fiber} from 'ReactFiber'; import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; var {Update} = require('ReactTypeOfSideEffect'); @@ -81,6 +82,7 @@ module.exports = function( getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel, memoizeProps: (workInProgress: Fiber, props: any) => void, memoizeState: (workInProgress: Fiber, state: any) => void, + recalculateCurrentTime: () => ExpirationTime, ) { // Class component state updater const updater = { @@ -88,31 +90,34 @@ module.exports = function( enqueueSetState(instance, partialState, callback) { const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); + const currentTime = recalculateCurrentTime(); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'setState'); } - addUpdate(fiber, partialState, callback, priorityLevel); + addUpdate(fiber, partialState, callback, priorityLevel, currentTime); scheduleUpdate(fiber, priorityLevel); }, enqueueReplaceState(instance, state, callback) { const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); + const currentTime = recalculateCurrentTime(); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'replaceState'); } - addReplaceUpdate(fiber, state, callback, priorityLevel); + addReplaceUpdate(fiber, state, callback, priorityLevel, currentTime); scheduleUpdate(fiber, priorityLevel); }, enqueueForceUpdate(instance, callback) { const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); + const currentTime = recalculateCurrentTime(); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'forceUpdate'); } - addForceUpdate(fiber, callback, priorityLevel); + addForceUpdate(fiber, callback, priorityLevel, currentTime); scheduleUpdate(fiber, priorityLevel); }, }; diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js new file mode 100644 index 0000000000000..1f5d9c42c4e0a --- /dev/null +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule ReactFiberExpirationTime + * @flow + */ + +'use strict'; + +import type {PriorityLevel} from 'ReactPriorityLevel'; +const { + NoWork, + SynchronousPriority, + TaskPriority, + HighPriority, + LowPriority, + OffscreenPriority, +} = require('ReactPriorityLevel'); + +const invariant = require('fbjs/lib/invariant'); + +// TODO: Use an opaque type once ESLint et al support the syntax +export type ExpirationTime = number; + +const Done = 0; +exports.Done = Done; + +const Never = Number.MAX_SAFE_INTEGER; +exports.Never = Never; + +// 1 unit of expiration time represents 10ms. +function msToExpirationTime(ms: number): ExpirationTime { + // Always add 1 so that we don't clash with the magic number for Done. + return Math.round(ms / 10) + 1; +} +exports.msToExpirationTime = msToExpirationTime; + +function ceiling(time: ExpirationTime, precision: number): ExpirationTime { + return Math.ceil(Math.ceil(time * precision) / precision); +} + +// Given the current clock time and a priority level, returns an expiration time +// that represents a point in the future by which some work should complete. +// The lower the priority, the further out the expiration time. We use rounding +// to batch like updates together. The further out the expiration time, the +// more we want to batch, so we use a larger precision when rounding. +function priorityToExpirationTime( + currentTime: ExpirationTime, + priorityLevel: PriorityLevel, +): ExpirationTime { + switch (priorityLevel) { + case NoWork: + return Done; + case SynchronousPriority: + // Return a number lower than the current time, but higher than Done. + return 1; + case TaskPriority: + // Return the current time, so that this work completes in this batch. + return currentTime; + case HighPriority: + // Should complete within ~100ms. 120ms max. + return msToExpirationTime(ceiling(100, 20)); + case LowPriority: + // Should complete within ~1000ms. 1200ms max. + return msToExpirationTime(ceiling(1000, 200)); + case OffscreenPriority: + return Never; + default: + invariant( + false, + 'Switch statement should be exhuastive. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + } +} +exports.priorityToExpirationTime = priorityToExpirationTime; + +// Given the current clock time and an expiration time, returns the +// corresponding priority level. The more time has advanced, the higher the +// priority level. +function expirationTimeToPriorityLevel( + currentTime: ExpirationTime, + expirationTime: ExpirationTime, +): PriorityLevel { + // First check for magic values + if (expirationTime === Done) { + return NoWork; + } + if (expirationTime === Never) { + return OffscreenPriority; + } + if (expirationTime < currentTime) { + return SynchronousPriority; + } + if (expirationTime === currentTime) { + return TaskPriority; + } + // TODO: We don't currently distinguish between high and low priority. + return LowPriority; +} +exports.expirationTimeToPriorityLevel = expirationTimeToPriorityLevel; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 4876d88f4607b..e20fff429d698 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -122,6 +122,8 @@ export type HostConfig = { prepareForCommit(): void, resetAfterCommit(): void, + now(): number, + // Optional hydration canHydrateInstance?: (instance: I | TI, type: T, props: P) => boolean, canHydrateTextInstance?: (instance: I | TI, text: string) => boolean, @@ -235,6 +237,7 @@ module.exports = function( var { scheduleUpdate, getPriorityContext, + recalculateCurrentTime, batchedUpdates, unbatchedUpdates, flushSync, @@ -274,6 +277,7 @@ module.exports = function( element.type.prototype != null && (element.type.prototype: any).unstable_isAsyncReactComponent === true; const priorityLevel = getPriorityContext(current, forceAsync); + const currentTime = recalculateCurrentTime(); const nextState = {element}; callback = callback === undefined ? null : callback; if (__DEV__) { @@ -284,7 +288,7 @@ module.exports = function( callback, ); } - addTopLevelUpdate(current, nextState, callback, priorityLevel); + addTopLevelUpdate(current, nextState, callback, priorityLevel, currentTime); scheduleUpdate(current, priorityLevel); } diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index cc3800d129ea7..4509708d0fbf4 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -15,6 +15,7 @@ import type {FiberRoot} from 'ReactFiberRoot'; import type {HostConfig, Deadline} from 'ReactFiberReconciler'; import type {PriorityLevel} from 'ReactPriorityLevel'; import type {HydrationContext} from 'ReactFiberHydrationContext'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; export type CapturedError = { componentName: ?string, @@ -62,6 +63,8 @@ var { OffscreenPriority, } = require('ReactPriorityLevel'); +var {msToExpirationTime} = require('ReactFiberExpirationTime'); + var {AsyncUpdates} = require('ReactTypeOfInternalContext'); var { @@ -166,6 +169,7 @@ module.exports = function( hydrationContext, scheduleUpdate, getPriorityContext, + recalculateCurrentTime, ); const {completeWork} = ReactFiberCompleteWork( config, @@ -181,12 +185,16 @@ module.exports = function( commitDetachRef, } = ReactFiberCommitWork(config, captureError); const { + now, scheduleDeferredCallback, useSyncScheduling, prepareForCommit, resetAfterCommit, } = config; + // Represents the current time in ms. + let currentTime: ExpirationTime = msToExpirationTime(now()); + // The priority level to use when scheduling an update. We use NoWork to // represent the default priority. // TODO: Should we change this to an array instead of using the call stack? @@ -1513,6 +1521,11 @@ module.exports = function( scheduleUpdateImpl(fiber, TaskPriority, true); } + function recalculateCurrentTime(): ExpirationTime { + currentTime = msToExpirationTime(now()); + return currentTime; + } + function batchedUpdates(fn: (a: A) => R, a: A): R { const previousIsBatchingUpdates = isBatchingUpdates; isBatchingUpdates = true; @@ -1575,6 +1588,7 @@ module.exports = function( return { scheduleUpdate: scheduleUpdate, getPriorityContext: getPriorityContext, + recalculateCurrentTime: recalculateCurrentTime, batchedUpdates: batchedUpdates, unbatchedUpdates: unbatchedUpdates, flushSync: flushSync, diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index 39f483b7d78df..4b40e0e565d78 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -12,6 +12,7 @@ import type {Fiber} from 'ReactFiber'; import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; const {Callback: CallbackEffect} = require('ReactTypeOfSideEffect'); @@ -21,6 +22,8 @@ const { TaskPriority, } = require('ReactPriorityLevel'); +const {Done, priorityToExpirationTime} = require('ReactFiberExpirationTime'); + const {ClassComponent, HostRoot} = require('ReactTypeOfWork'); const invariant = require('fbjs/lib/invariant'); @@ -35,8 +38,9 @@ type PartialState = // Callbacks are not validated until invocation type Callback = mixed; -type Update = { +export type Update = { priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, partialState: PartialState, callback: Callback | null, isReplace: boolean, @@ -104,6 +108,7 @@ function createUpdateQueue(): UpdateQueue { function cloneUpdate(update: Update): Update { return { priorityLevel: update.priorityLevel, + expirationTime: update.expirationTime, partialState: update.partialState, callback: update.callback, isReplace: update.isReplace, @@ -113,19 +118,41 @@ function cloneUpdate(update: Update): Update { }; } +const COALESCENCE_THRESHOLD: ExpirationTime = 10; + function insertUpdateIntoQueue( queue: UpdateQueue, update: Update, insertAfter: Update | null, insertBefore: Update | null, + currentTime: ExpirationTime, ) { + const priorityLevel = update.priorityLevel; + + let coalescedTime: ExpirationTime | null = null; if (insertAfter !== null) { insertAfter.next = update; + // If we receive multiple updates to the same fiber at the same priority + // level, we coalesce them by assigning the same expiration time, so that + // they all flush at the same time. Because this causes an interruption, it + // could lead to starvation, so we stop coalescing once the time until the + // expiration time reaches a certain threshold. + if (insertAfter !== null && insertAfter.priorityLevel === priorityLevel) { + const expirationTime = insertAfter.expirationTime; + if (expirationTime - currentTime > COALESCENCE_THRESHOLD) { + coalescedTime = expirationTime; + } + } } else { // This is the first item in the queue. update.next = queue.first; queue.first = update; } + update.expirationTime = coalescedTime !== null + ? coalescedTime + : // If we don't coalesce, calculate the expiration time using the + // current time. + priorityToExpirationTime(currentTime, priorityLevel); if (insertBefore !== null) { update.next = insertBefore; @@ -213,7 +240,11 @@ function ensureUpdateQueues(fiber: Fiber) { // we shouldn't make a copy. // // If the update is cloned, it returns the cloned update. -function insertUpdate(fiber: Fiber, update: Update): Update | null { +function insertUpdate( + fiber: Fiber, + update: Update, + currentTime: ExpirationTime, +): Update | null { // We'll have at least one and at most two distinct update queues. ensureUpdateQueues(fiber); const queue1 = _queue1; @@ -240,7 +271,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null { if (queue2 === null) { // If there's no alternate queue, there's nothing else to do but insert. - insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1); + insertUpdateIntoQueue( + queue1, + update, + insertAfter1, + insertBefore1, + currentTime, + ); return null; } @@ -252,7 +289,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null { // Now we can insert into the first queue. This must come after finding both // insertion positions because it mutates the list. - insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1); + insertUpdateIntoQueue( + queue1, + update, + insertAfter1, + insertBefore1, + currentTime, + ); // See if the insertion positions are equal. Be careful to only compare // non-null values. @@ -275,7 +318,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null { // The insertion positions are different, so we need to clone the update and // insert the clone into the alternate queue. const update2 = cloneUpdate(update); - insertUpdateIntoQueue(queue2, update2, insertAfter2, insertBefore2); + insertUpdateIntoQueue( + queue2, + update2, + insertAfter2, + insertBefore2, + currentTime, + ); return update2; } } @@ -285,9 +334,11 @@ function addUpdate( partialState: PartialState | null, callback: mixed, priorityLevel: PriorityLevel, + currentTime: ExpirationTime, ): void { const update = { priorityLevel, + expirationTime: Done, partialState, callback, isReplace: false, @@ -295,7 +346,7 @@ function addUpdate( isTopLevelUnmount: false, next: null, }; - insertUpdate(fiber, update); + insertUpdate(fiber, update, currentTime); } exports.addUpdate = addUpdate; @@ -304,9 +355,11 @@ function addReplaceUpdate( state: any | null, callback: Callback | null, priorityLevel: PriorityLevel, + currentTime: ExpirationTime, ): void { const update = { priorityLevel, + expirationTime: Done, partialState: state, callback, isReplace: true, @@ -314,7 +367,7 @@ function addReplaceUpdate( isTopLevelUnmount: false, next: null, }; - insertUpdate(fiber, update); + insertUpdate(fiber, update, currentTime); } exports.addReplaceUpdate = addReplaceUpdate; @@ -322,9 +375,11 @@ function addForceUpdate( fiber: Fiber, callback: Callback | null, priorityLevel: PriorityLevel, + currentTime: ExpirationTime, ): void { const update = { priorityLevel, + expirationTime: Done, partialState: null, callback, isReplace: false, @@ -332,7 +387,7 @@ function addForceUpdate( isTopLevelUnmount: false, next: null, }; - insertUpdate(fiber, update); + insertUpdate(fiber, update, currentTime); } exports.addForceUpdate = addForceUpdate; @@ -353,11 +408,13 @@ function addTopLevelUpdate( partialState: PartialState, callback: Callback | null, priorityLevel: PriorityLevel, + currentTime: ExpirationTime, ): void { const isTopLevelUnmount = partialState.element === null; const update = { priorityLevel, + expirationTime: Done, partialState, callback, isReplace: false, @@ -365,7 +422,7 @@ function addTopLevelUpdate( isTopLevelUnmount, next: null, }; - const update2 = insertUpdate(fiber, update); + const update2 = insertUpdate(fiber, update, currentTime); if (isTopLevelUnmount) { // TODO: Redesign the top-level mount/update/unmount API to avoid this diff --git a/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js b/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js index 7c00d20f50f76..60416632715f5 100644 --- a/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js @@ -46,6 +46,9 @@ describe('ReactFiberHostContext', () => { appendChildToContainer: function() { return null; }, + now: function() { + return 0; + }, useSyncScheduling: true, }); diff --git a/src/renderers/testing/ReactTestRendererFiberEntry.js b/src/renderers/testing/ReactTestRendererFiberEntry.js index 6779c0dc3662d..2a6d9e1ea5763 100644 --- a/src/renderers/testing/ReactTestRendererFiberEntry.js +++ b/src/renderers/testing/ReactTestRendererFiberEntry.js @@ -247,6 +247,11 @@ var TestRenderer = ReactFiberReconciler({ useSyncScheduling: true, getPublicInstance, + + now(): number { + // Test renderer does not use expiration + return 0; + }, }); var defaultTestOptions = { From 9a58ba2b3614bdaf5eaf4937cb188e1c79eba416 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 10 Aug 2017 14:48:23 -0700 Subject: [PATCH 02/24] Replace pendingWorkPriority with expiration times Instead of a priority, a fiber has an expiration time that represents a point in the future by which it should render. Pending updates still have priorities so that they can be coalesced. We use a host config method to read the current time. This commit implements everything except that method, which currently returns a constant value. So this just proves that expiration times work the same as priorities when time is frozen. Subsequent commits will show the effect of advancing time. --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 33 +- src/renderers/noop/ReactNoopEntry.js | 6 +- src/renderers/shared/fiber/ReactChildFiber.js | 214 +++++++----- src/renderers/shared/fiber/ReactFiber.js | 37 +- .../shared/fiber/ReactFiberBeginWork.js | 131 ++++--- .../shared/fiber/ReactFiberClassComponent.js | 63 +++- .../shared/fiber/ReactFiberCompleteWork.js | 23 +- .../shared/fiber/ReactFiberExpirationTime.js | 17 +- .../shared/fiber/ReactFiberReconciler.js | 16 +- .../shared/fiber/ReactFiberScheduler.js | 326 ++++++++++-------- .../shared/fiber/ReactFiberUpdateQueue.js | 94 ++--- 11 files changed, 566 insertions(+), 394 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 6076c61fcc19c..5dbda35eb0b3c 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -163,20 +163,20 @@ function shouldAutoFocusHostComponent(type: string, props: Props): boolean { } // TODO: Better polyfill -let now; -if ( - typeof window !== 'undefined' && - window.performance && - typeof window.performance.now === 'function' -) { - now = function() { - return performance.now(); - }; -} else { - now = function() { - return Date.now(); - }; -} +// let now; +// if ( +// typeof window !== 'undefined' && +// window.performance && +// typeof window.performance.now === 'function' +// ) { +// now = function() { +// return performance.now(); +// }; +// } else { +// now = function() { +// return Date.now(); +// }; +// } var DOMRenderer = ReactFiberReconciler({ getRootHostContext(rootContainerInstance: Container): HostContext { @@ -453,7 +453,10 @@ var DOMRenderer = ReactFiberReconciler({ } }, - now: now, + now() { + // TODO: Use performance.now to enable expiration + return 0; + }, canHydrateInstance( instance: Instance | TextInstance, diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index 9a2dcadaa3150..afd4eac99d19e 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -405,7 +405,7 @@ var ReactNoop = { ' '.repeat(depth + 1) + '~', firstUpdate && firstUpdate.partialState, firstUpdate.callback ? 'with callback' : '', - '[' + firstUpdate.priorityLevel + ']', + '[' + firstUpdate.expirationTime + ']', ); var next; while ((next = firstUpdate.next)) { @@ -413,7 +413,7 @@ var ReactNoop = { ' '.repeat(depth + 1) + '~', next.partialState, next.callback ? 'with callback' : '', - '[' + firstUpdate.priorityLevel + ']', + '[' + firstUpdate.expirationTime + ']', ); } } @@ -423,7 +423,7 @@ var ReactNoop = { ' '.repeat(depth) + '- ' + (fiber.type ? fiber.type.name || fiber.type : '[root]'), - '[' + fiber.pendingWorkPriority + (fiber.pendingProps ? '*' : '') + ']', + '[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']', ); if (fiber.updateQueue) { logUpdateQueue(fiber.updateQueue, depth); diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 763ee6d9f3c97..935b897b3ca1b 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -13,7 +13,7 @@ import type {ReactElement} from 'ReactElementType'; import type {ReactCoroutine, ReactPortal, ReactYield} from 'ReactTypes'; import type {Fiber} from 'ReactFiber'; -import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; var {REACT_COROUTINE_TYPE, REACT_YIELD_TYPE} = require('ReactCoroutine'); var {REACT_PORTAL_TYPE} = require('ReactPortal'); @@ -288,19 +288,19 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { return existingChildren; } - function useFiber(fiber: Fiber, priority: PriorityLevel): Fiber { + function useFiber(fiber: Fiber, expirationTime: ExpirationTime): Fiber { // We currently set sibling to null and index to 0 here because it is easy // to forget to do before returning it. E.g. for the single child case. if (shouldClone) { - const clone = createWorkInProgress(fiber, priority); + const clone = createWorkInProgress(fiber, expirationTime); clone.index = 0; clone.sibling = null; return clone; } else { - // We override the pending priority even if it is higher, because if - // we're reconciling at a lower priority that means that this was + // We override the expiration time even if it is earlier, because if + // we're reconciling at a later time that means that this was // down-prioritized. - fiber.pendingWorkPriority = priority; + fiber.expirationTime = expirationTime; fiber.effectTag = NoEffect; fiber.index = 0; fiber.sibling = null; @@ -349,20 +349,20 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, textContent: string, - priority: PriorityLevel, + expirationTime: ExpirationTime, ) { if (current === null || current.tag !== HostText) { // Insert const created = createFiberFromText( textContent, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; } else { // Update - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.pendingProps = textContent; existing.return = returnFiber; return existing; @@ -373,21 +373,21 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, element: ReactElement, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { if (current === null || current.type !== element.type) { // Insert const created = createFiberFromElement( element, returnFiber.internalContextTag, - priority, + expirationTime, ); created.ref = coerceRef(current, element); created.return = returnFiber; return created; } else { // Move based on index - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.ref = coerceRef(current, element); existing.pendingProps = element.props; existing.return = returnFiber; @@ -403,7 +403,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, coroutine: ReactCoroutine, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { // TODO: Should this also compare handler to determine whether to reuse? if (current === null || current.tag !== CoroutineComponent) { @@ -411,13 +411,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromCoroutine( coroutine, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; } else { // Move based on index - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.pendingProps = coroutine; existing.return = returnFiber; return existing; @@ -428,21 +428,21 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, yieldNode: ReactYield, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { if (current === null || current.tag !== YieldComponent) { // Insert const created = createFiberFromYield( yieldNode, returnFiber.internalContextTag, - priority, + expirationTime, ); created.type = yieldNode.value; created.return = returnFiber; return created; } else { // Move based on index - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.type = yieldNode.value; existing.return = returnFiber; return existing; @@ -453,7 +453,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, portal: ReactPortal, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { if ( current === null || @@ -465,13 +465,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromPortal( portal, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; } else { // Update - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.pendingProps = portal.children || []; existing.return = returnFiber; return existing; @@ -482,20 +482,20 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, fragment: Iterable<*>, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { if (current === null || current.tag !== Fragment) { // Insert const created = createFiberFromFragment( fragment, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; } else { // Update - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.pendingProps = fragment; existing.return = returnFiber; return existing; @@ -505,7 +505,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { function createChild( returnFiber: Fiber, newChild: any, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber | null { if (typeof newChild === 'string' || typeof newChild === 'number') { // Text nodes doesn't have keys. If the previous node is implicitly keyed @@ -514,7 +514,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromText( '' + newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -526,7 +526,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromElement( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.ref = coerceRef(null, newChild); created.return = returnFiber; @@ -537,7 +537,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromCoroutine( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -547,7 +547,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromYield( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.type = newChild.value; created.return = returnFiber; @@ -558,7 +558,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromPortal( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -569,7 +569,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromFragment( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -591,7 +591,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, oldFiber: Fiber | null, newChild: any, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber | null { // Update the fiber if the keys match, otherwise return null. @@ -604,14 +604,24 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (key !== null) { return null; } - return updateTextNode(returnFiber, oldFiber, '' + newChild, priority); + return updateTextNode( + returnFiber, + oldFiber, + '' + newChild, + expirationTime, + ); } if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { if (newChild.key === key) { - return updateElement(returnFiber, oldFiber, newChild, priority); + return updateElement( + returnFiber, + oldFiber, + newChild, + expirationTime, + ); } else { return null; } @@ -619,7 +629,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { case REACT_COROUTINE_TYPE: { if (newChild.key === key) { - return updateCoroutine(returnFiber, oldFiber, newChild, priority); + return updateCoroutine( + returnFiber, + oldFiber, + newChild, + expirationTime, + ); } else { return null; } @@ -630,7 +645,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // we can continue to replace it without aborting even if it is not a // yield. if (key === null) { - return updateYield(returnFiber, oldFiber, newChild, priority); + return updateYield(returnFiber, oldFiber, newChild, expirationTime); } else { return null; } @@ -638,7 +653,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { case REACT_PORTAL_TYPE: { if (newChild.key === key) { - return updatePortal(returnFiber, oldFiber, newChild, priority); + return updatePortal( + returnFiber, + oldFiber, + newChild, + expirationTime, + ); } else { return null; } @@ -651,7 +671,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (key !== null) { return null; } - return updateFragment(returnFiber, oldFiber, newChild, priority); + return updateFragment(returnFiber, oldFiber, newChild, expirationTime); } throwOnInvalidObjectType(returnFiber, newChild); @@ -671,13 +691,18 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, newIdx: number, newChild: any, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber | null { if (typeof newChild === 'string' || typeof newChild === 'number') { // Text nodes doesn't have keys, so we neither have to check the old nor // new node for the key. If both are text nodes, they match. const matchedFiber = existingChildren.get(newIdx) || null; - return updateTextNode(returnFiber, matchedFiber, '' + newChild, priority); + return updateTextNode( + returnFiber, + matchedFiber, + '' + newChild, + expirationTime, + ); } if (typeof newChild === 'object' && newChild !== null) { @@ -687,7 +712,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; - return updateElement(returnFiber, matchedFiber, newChild, priority); + return updateElement( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); } case REACT_COROUTINE_TYPE: { @@ -695,14 +725,24 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; - return updateCoroutine(returnFiber, matchedFiber, newChild, priority); + return updateCoroutine( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); } case REACT_YIELD_TYPE: { // Yields doesn't have keys, so we neither have to check the old nor // new node for the key. If both are yields, they match. const matchedFiber = existingChildren.get(newIdx) || null; - return updateYield(returnFiber, matchedFiber, newChild, priority); + return updateYield( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); } case REACT_PORTAL_TYPE: { @@ -710,13 +750,23 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; - return updatePortal(returnFiber, matchedFiber, newChild, priority); + return updatePortal( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); } } if (isArray(newChild) || getIteratorFn(newChild)) { const matchedFiber = existingChildren.get(newIdx) || null; - return updateFragment(returnFiber, matchedFiber, newChild, priority); + return updateFragment( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); } throwOnInvalidObjectType(returnFiber, newChild); @@ -782,7 +832,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, newChildren: Array<*>, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber | null { // This algorithm can't optimize by searching from boths ends since we // don't have backpointers on fibers. I'm trying to see how far we can get @@ -830,7 +880,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, oldFiber, newChildren[newIdx], - priority, + expirationTime, ); if (newFiber === null) { // TODO: This breaks on empty slots like null children. That's @@ -877,7 +927,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const newFiber = createChild( returnFiber, newChildren[newIdx], - priority, + expirationTime, ); if (!newFiber) { continue; @@ -904,7 +954,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, newIdx, newChildren[newIdx], - priority, + expirationTime, ); if (newFiber) { if (shouldTrackSideEffects) { @@ -941,7 +991,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, newChildrenIterable: Iterable<*>, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber | null { // This is the same implementation as reconcileChildrenArray(), // but using the iterator instead. @@ -1005,7 +1055,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { } else { nextOldFiber = oldFiber.sibling; } - const newFiber = updateSlot(returnFiber, oldFiber, step.value, priority); + const newFiber = updateSlot( + returnFiber, + oldFiber, + step.value, + expirationTime, + ); if (newFiber === null) { // TODO: This breaks on empty slots like null children. That's // unfortunate because it triggers the slow path all the time. We need @@ -1048,7 +1103,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // If we don't have any more existing children we can choose a fast path // since the rest will all be insertions. for (; !step.done; newIdx++, (step = newChildren.next())) { - const newFiber = createChild(returnFiber, step.value, priority); + const newFiber = createChild(returnFiber, step.value, expirationTime); if (newFiber === null) { continue; } @@ -1074,7 +1129,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, newIdx, step.value, - priority, + expirationTime, ); if (newFiber !== null) { if (shouldTrackSideEffects) { @@ -1111,7 +1166,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, textContent: string, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { // There's no need to check for keys on text nodes since we don't have a // way to define them. @@ -1119,7 +1174,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // We already have an existing node so let's just update it and delete // the rest. deleteRemainingChildren(returnFiber, currentFirstChild.sibling); - const existing = useFiber(currentFirstChild, priority); + const existing = useFiber(currentFirstChild, expirationTime); existing.pendingProps = textContent; existing.return = returnFiber; return existing; @@ -1130,7 +1185,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromText( textContent, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -1140,7 +1195,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const key = element.key; let child = currentFirstChild; @@ -1150,7 +1205,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (child.key === key) { if (child.type === element.type) { deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, priority); + const existing = useFiber(child, expirationTime); existing.ref = coerceRef(child, element); existing.pendingProps = element.props; existing.return = returnFiber; @@ -1172,7 +1227,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromElement( element, returnFiber.internalContextTag, - priority, + expirationTime, ); created.ref = coerceRef(currentFirstChild, element); created.return = returnFiber; @@ -1183,7 +1238,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, coroutine: ReactCoroutine, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const key = coroutine.key; let child = currentFirstChild; @@ -1193,7 +1248,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (child.key === key) { if (child.tag === CoroutineComponent) { deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, priority); + const existing = useFiber(child, expirationTime); existing.pendingProps = coroutine; existing.return = returnFiber; return existing; @@ -1210,7 +1265,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromCoroutine( coroutine, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -1220,14 +1275,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, yieldNode: ReactYield, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { // There's no need to check for keys on yields since they're stateless. let child = currentFirstChild; if (child !== null) { if (child.tag === YieldComponent) { deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, priority); + const existing = useFiber(child, expirationTime); existing.type = yieldNode.value; existing.return = returnFiber; return existing; @@ -1239,7 +1294,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromYield( yieldNode, returnFiber.internalContextTag, - priority, + expirationTime, ); created.type = yieldNode.value; created.return = returnFiber; @@ -1250,7 +1305,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, portal: ReactPortal, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const key = portal.key; let child = currentFirstChild; @@ -1264,7 +1319,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { child.stateNode.implementation === portal.implementation ) { deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, priority); + const existing = useFiber(child, expirationTime); existing.pendingProps = portal.children || []; existing.return = returnFiber; return existing; @@ -1281,7 +1336,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromPortal( portal, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -1294,7 +1349,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, newChild: any, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber | null { // This function is not recursive. // If the top level item is an array, we treat it as a set of children, @@ -1304,8 +1359,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // Handle object types const isObject = typeof newChild === 'object' && newChild !== null; if (isObject) { - // Support only the subset of return types that Stack supports. Treat - // everything else as empty, but log a warning. switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: return placeSingleChild( @@ -1313,7 +1366,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); @@ -1323,17 +1376,16 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); - case REACT_YIELD_TYPE: return placeSingleChild( reconcileSingleYield( returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); @@ -1343,7 +1395,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); } @@ -1355,7 +1407,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, '' + newChild, - priority, + expirationTime, ), ); } @@ -1365,7 +1417,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ); } @@ -1374,7 +1426,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ); } @@ -1446,7 +1498,7 @@ exports.cloneChildFibers = function( let currentChild = workInProgress.child; let newChild = createWorkInProgress( currentChild, - currentChild.pendingWorkPriority, + currentChild.expirationTime, ); // TODO: Pass this as an argument, since it's easy to forget. newChild.pendingProps = currentChild.pendingProps; @@ -1457,7 +1509,7 @@ exports.cloneChildFibers = function( currentChild = currentChild.sibling; newChild = newChild.sibling = createWorkInProgress( currentChild, - currentChild.pendingWorkPriority, + currentChild.expirationTime, ); newChild.pendingProps = currentChild.pendingProps; newChild.return = workInProgress; diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index dae77d55cc79b..faf40a2d6526c 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -21,6 +21,7 @@ import type {TypeOfWork} from 'ReactTypeOfWork'; import type {TypeOfInternalContext} from 'ReactTypeOfInternalContext'; import type {TypeOfSideEffect} from 'ReactTypeOfSideEffect'; import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; import type {UpdateQueue} from 'ReactFiberUpdateQueue'; var { @@ -37,6 +38,8 @@ var { var {NoWork} = require('ReactPriorityLevel'); +var {Done} = require('ReactFiberExpirationTime'); + var {NoContext} = require('ReactTypeOfInternalContext'); var {NoEffect} = require('ReactTypeOfSideEffect'); @@ -134,8 +137,9 @@ export type Fiber = {| firstEffect: Fiber | null, lastEffect: Fiber | null, - // This will be used to quickly determine if a subtree has no pending changes. - pendingWorkPriority: PriorityLevel, + // Represents a time in the future by which this work should be completed. + // This is also used to quickly determine if a subtree has no pending changes. + expirationTime: ExpirationTime, // This is a pooled version of a Fiber. Every fiber that gets updated will // eventually have a pair. There are cases when we can clean up pairs to save @@ -189,7 +193,7 @@ function FiberNode( this.firstEffect = null; this.lastEffect = null; - this.pendingWorkPriority = NoWork; + this.expirationTime = Done; this.alternate = null; @@ -233,7 +237,7 @@ function shouldConstruct(Component) { // This is used to create an alternate fiber to do work on. exports.createWorkInProgress = function( current: Fiber, - renderPriority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { let workInProgress = current.alternate; if (workInProgress === null) { @@ -270,7 +274,7 @@ exports.createWorkInProgress = function( workInProgress.lastEffect = null; } - workInProgress.pendingWorkPriority = renderPriority; + workInProgress.expirationTime = expirationTime; workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; @@ -296,7 +300,7 @@ exports.createHostRootFiber = function(): Fiber { exports.createFiberFromElement = function( element: ReactElement, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { let owner = null; if (__DEV__) { @@ -310,7 +314,7 @@ exports.createFiberFromElement = function( owner, ); fiber.pendingProps = element.props; - fiber.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; if (__DEV__) { fiber._debugSource = element._source; @@ -323,24 +327,24 @@ exports.createFiberFromElement = function( exports.createFiberFromFragment = function( elements: ReactFragment, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { // TODO: Consider supporting keyed fragments. Technically, we accidentally // support that in the existing React. const fiber = createFiber(Fragment, null, internalContextTag); fiber.pendingProps = elements; - fiber.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; return fiber; }; exports.createFiberFromText = function( content: string, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const fiber = createFiber(HostText, null, internalContextTag); fiber.pendingProps = content; - fiber.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; return fiber; }; @@ -411,7 +415,7 @@ exports.createFiberFromHostInstanceForDeletion = function(): Fiber { exports.createFiberFromCoroutine = function( coroutine: ReactCoroutine, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const fiber = createFiber( CoroutineComponent, @@ -420,27 +424,28 @@ exports.createFiberFromCoroutine = function( ); fiber.type = coroutine.handler; fiber.pendingProps = coroutine; - fiber.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; return fiber; }; exports.createFiberFromYield = function( yieldNode: ReactYield, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const fiber = createFiber(YieldComponent, null, internalContextTag); + fiber.expirationTime = expirationTime; return fiber; }; exports.createFiberFromPortal = function( portal: ReactPortal, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const fiber = createFiber(HostPortal, portal.key, internalContextTag); fiber.pendingProps = portal.children || []; - fiber.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; fiber.stateNode = { containerInfo: portal.containerInfo, implementation: portal.implementation, diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index d308973277e46..5d49bf7e89dd7 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -48,7 +48,7 @@ var { YieldComponent, Fragment, } = ReactTypeOfWork; -var {NoWork, OffscreenPriority} = require('ReactPriorityLevel'); +var {Done, Never} = require('ReactFiberExpirationTime'); var { PerformedWork, Placement, @@ -72,9 +72,16 @@ module.exports = function( config: HostConfig, hostContext: HostContext, hydrationContext: HydrationContext, - scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void, - getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel, + scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void, + getPriorityContext: ( + fiber: Fiber, + forceAsync: boolean, + ) => PriorityLevel | null, recalculateCurrentTime: () => ExpirationTime, + getExpirationTimeForPriority: ( + currentTime: ExpirationTime, + priorityLevel: PriorityLevel | null, + ) => ExpirationTime, ) { const { shouldSetTextContent, @@ -102,23 +109,24 @@ module.exports = function( memoizeProps, memoizeState, recalculateCurrentTime, + getExpirationTimeForPriority, ); + // TODO: Remove this and use reconcileChildrenAtExpirationTime directly. function reconcileChildren(current, workInProgress, nextChildren) { - const priorityLevel = workInProgress.pendingWorkPriority; - reconcileChildrenAtPriority( + reconcileChildrenAtExpirationTime( current, workInProgress, nextChildren, - priorityLevel, + workInProgress.expirationTime, ); } - function reconcileChildrenAtPriority( + function reconcileChildrenAtExpirationTime( current, workInProgress, nextChildren, - priorityLevel, + renderExpirationTime, ) { if (current === null) { // If this is a fresh new component that hasn't been rendered yet, we @@ -129,7 +137,7 @@ module.exports = function( workInProgress, workInProgress.child, nextChildren, - priorityLevel, + renderExpirationTime, ); } else if (current.child === workInProgress.child) { // If the current child is the same as the work in progress, it means that @@ -142,7 +150,7 @@ module.exports = function( workInProgress, workInProgress.child, nextChildren, - priorityLevel, + renderExpirationTime, ); } else { // If, on the other hand, it is already using a clone, that means we've @@ -152,7 +160,7 @@ module.exports = function( workInProgress, workInProgress.child, nextChildren, - priorityLevel, + renderExpirationTime, ); } } @@ -226,7 +234,7 @@ module.exports = function( function updateClassComponent( current: Fiber | null, workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ) { // Push context providers early to prevent context stack mismatches. // During mounting we don't know the child context yet as the instance doesn't exist. @@ -238,18 +246,18 @@ module.exports = function( if (!workInProgress.stateNode) { // In the initial pass we might need to construct the instance. constructClassInstance(workInProgress, workInProgress.pendingProps); - mountClassInstance(workInProgress, priorityLevel); + mountClassInstance(workInProgress, renderExpirationTime); shouldUpdate = true; } else { invariant(false, 'Resuming work not yet implemented.'); // In a resume, we'll already have an instance we can reuse. - // shouldUpdate = resumeMountClassInstance(workInProgress, priorityLevel); + // shouldUpdate = resumeMountClassInstance(workInProgress, renderExpirationTime); } } else { shouldUpdate = updateClassInstance( current, workInProgress, - priorityLevel, + renderExpirationTime, ); } return finishClassComponent( @@ -321,7 +329,8 @@ module.exports = function( pushHostContainer(workInProgress, root.containerInfo); } - function updateHostRoot(current, workInProgress, priorityLevel) { + function updateHostRoot(current, workInProgress, renderExpirationTime) { + const root = (workInProgress.stateNode: FiberRoot); pushHostRootContext(workInProgress); const updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { @@ -333,11 +342,11 @@ module.exports = function( null, prevState, null, - priorityLevel, + renderExpirationTime, ); if (prevState === state) { // If the state is the same as before, that's a bailout because we had - // no work matching this priority. + // no work that expires at this time. resetHydrationState(); return bailoutOnAlreadyFinishedWork(current, workInProgress); } @@ -364,7 +373,7 @@ module.exports = function( workInProgress, workInProgress.child, element, - priorityLevel, + renderExpirationTime, ); } else { // Otherwise reset hydration state in case we aborted and resumed another @@ -380,7 +389,7 @@ module.exports = function( return bailoutOnAlreadyFinishedWork(current, workInProgress); } - function updateHostComponent(current, workInProgress, renderPriority) { + function updateHostComponent(current, workInProgress, renderExpirationTime) { pushHostContext(workInProgress); if (current === null) { @@ -426,13 +435,13 @@ module.exports = function( // Check the host config to see if the children are offscreen/hidden. if ( - renderPriority !== OffscreenPriority && + renderExpirationTime !== Never && !useSyncScheduling && shouldDeprioritizeSubtree(type, nextProps) ) { // Down-prioritize the children. - workInProgress.pendingWorkPriority = OffscreenPriority; - // Bailout and come back to this fiber later at OffscreenPriority. + workInProgress.expirationTime = Never; + // Bailout and come back to this fiber later. return null; } @@ -455,7 +464,11 @@ module.exports = function( return null; } - function mountIndeterminateComponent(current, workInProgress, priorityLevel) { + function mountIndeterminateComponent( + current, + workInProgress, + renderExpirationTime, + ) { invariant( current === null, 'An indeterminate component should never have mounted. This error is ' + @@ -490,7 +503,7 @@ module.exports = function( // We will invalidate the child context in finishClassComponent() right after rendering. const hasContext = pushContextProvider(workInProgress); adoptClassInstance(workInProgress, value); - mountClassInstance(workInProgress, priorityLevel); + mountClassInstance(workInProgress, renderExpirationTime); return finishClassComponent(current, workInProgress, true, hasContext); } else { // Proceed under the assumption that this is a functional component @@ -535,7 +548,11 @@ module.exports = function( } } - function updateCoroutineComponent(current, workInProgress) { + function updateCoroutineComponent( + current, + workInProgress, + renderExpirationTime, + ) { var nextCoroutine = (workInProgress.pendingProps: null | ReactCoroutine); if (hasContextChanged()) { // Normally we can bail out on props equality but if context has changed @@ -559,30 +576,29 @@ module.exports = function( } const nextChildren = nextCoroutine.children; - const priorityLevel = workInProgress.pendingWorkPriority; - // The following is a fork of reconcileChildrenAtPriority but using + // The following is a fork of reconcileChildrenAtExpirationTime but using // stateNode to store the child. if (current === null) { workInProgress.stateNode = mountChildFibersInPlace( workInProgress, workInProgress.stateNode, nextChildren, - priorityLevel, + renderExpirationTime, ); } else if (current.child === workInProgress.child) { workInProgress.stateNode = reconcileChildFibers( workInProgress, workInProgress.stateNode, nextChildren, - priorityLevel, + renderExpirationTime, ); } else { workInProgress.stateNode = reconcileChildFibersInPlace( workInProgress, workInProgress.stateNode, nextChildren, - priorityLevel, + renderExpirationTime, ); } @@ -592,9 +608,12 @@ module.exports = function( return workInProgress.stateNode; } - function updatePortalComponent(current, workInProgress) { + function updatePortalComponent( + current, + workInProgress, + renderExpirationTime, + ) { pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); - const priorityLevel = workInProgress.pendingWorkPriority; let nextChildren = workInProgress.pendingProps; if (hasContextChanged()) { // Normally we can bail out on props equality but if context has changed @@ -624,7 +643,7 @@ module.exports = function( workInProgress, workInProgress.child, nextChildren, - priorityLevel, + renderExpirationTime, ); memoizeProps(workInProgress, nextChildren); } else { @@ -719,11 +738,11 @@ module.exports = function( function beginWork( current: Fiber | null, workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ): Fiber | null { if ( - workInProgress.pendingWorkPriority === NoWork || - workInProgress.pendingWorkPriority > priorityLevel + workInProgress.expirationTime === Done || + workInProgress.expirationTime > renderExpirationTime ) { return bailoutOnLowPriority(current, workInProgress); } @@ -733,16 +752,24 @@ module.exports = function( return mountIndeterminateComponent( current, workInProgress, - priorityLevel, + renderExpirationTime, ); case FunctionalComponent: return updateFunctionalComponent(current, workInProgress); case ClassComponent: - return updateClassComponent(current, workInProgress, priorityLevel); + return updateClassComponent( + current, + workInProgress, + renderExpirationTime, + ); case HostRoot: - return updateHostRoot(current, workInProgress, priorityLevel); + return updateHostRoot(current, workInProgress, renderExpirationTime); case HostComponent: - return updateHostComponent(current, workInProgress, priorityLevel); + return updateHostComponent( + current, + workInProgress, + renderExpirationTime, + ); case HostText: return updateHostText(current, workInProgress); case CoroutineHandlerPhase: @@ -750,13 +777,21 @@ module.exports = function( workInProgress.tag = CoroutineComponent; // Intentionally fall through since this is now the same. case CoroutineComponent: - return updateCoroutineComponent(current, workInProgress); + return updateCoroutineComponent( + current, + workInProgress, + renderExpirationTime, + ); case YieldComponent: // A yield component is just a placeholder, we can just run through the // next one immediately. return null; case HostPortal: - return updatePortalComponent(current, workInProgress); + return updatePortalComponent( + current, + workInProgress, + renderExpirationTime, + ); case Fragment: return updateFragment(current, workInProgress); default: @@ -771,7 +806,7 @@ module.exports = function( function beginFailedWork( current: Fiber | null, workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ) { // Push context providers here to avoid a push/pop context mismatch. switch (workInProgress.tag) { @@ -804,8 +839,8 @@ module.exports = function( } if ( - workInProgress.pendingWorkPriority === NoWork || - workInProgress.pendingWorkPriority > priorityLevel + workInProgress.expirationTime === Done || + workInProgress.expirationTime > renderExpirationTime ) { return bailoutOnLowPriority(current, workInProgress); } @@ -817,11 +852,11 @@ module.exports = function( // Unmount the current children as if the component rendered null const nextChildren = null; - reconcileChildrenAtPriority( + reconcileChildrenAtExpirationTime( current, workInProgress, nextChildren, - priorityLevel, + renderExpirationTime, ); if (workInProgress.tag === ClassComponent) { diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 4c72e6236b7c3..62f35742b0256 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -78,11 +78,18 @@ if (__DEV__) { } module.exports = function( - scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void, - getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel, + scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void, + getPriorityContext: ( + fiber: Fiber, + forceAsync: boolean, + ) => PriorityLevel | null, memoizeProps: (workInProgress: Fiber, props: any) => void, memoizeState: (workInProgress: Fiber, state: any) => void, recalculateCurrentTime: () => ExpirationTime, + getExpirationTimeForPriority: ( + currentTime: ExpirationTime, + priorityLevel: PriorityLevel | null, + ) => ExpirationTime, ) { // Class component state updater const updater = { @@ -91,34 +98,66 @@ module.exports = function( const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); const currentTime = recalculateCurrentTime(); + const expirationTime = getExpirationTimeForPriority( + currentTime, + priorityLevel, + ); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'setState'); } - addUpdate(fiber, partialState, callback, priorityLevel, currentTime); - scheduleUpdate(fiber, priorityLevel); + addUpdate( + fiber, + partialState, + callback, + priorityLevel, + expirationTime, + currentTime, + ); + scheduleUpdate(fiber, expirationTime); }, enqueueReplaceState(instance, state, callback) { const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); const currentTime = recalculateCurrentTime(); + const expirationTime = getExpirationTimeForPriority( + currentTime, + priorityLevel, + ); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'replaceState'); } - addReplaceUpdate(fiber, state, callback, priorityLevel, currentTime); - scheduleUpdate(fiber, priorityLevel); + addReplaceUpdate( + fiber, + state, + callback, + priorityLevel, + expirationTime, + currentTime, + ); + scheduleUpdate(fiber, expirationTime); }, enqueueForceUpdate(instance, callback) { const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); const currentTime = recalculateCurrentTime(); + const expirationTime = getExpirationTimeForPriority( + currentTime, + priorityLevel, + ); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'forceUpdate'); } - addForceUpdate(fiber, callback, priorityLevel, currentTime); - scheduleUpdate(fiber, priorityLevel); + addForceUpdate( + fiber, + callback, + priorityLevel, + expirationTime, + currentTime, + ); + scheduleUpdate(fiber, expirationTime); }, }; @@ -388,7 +427,7 @@ module.exports = function( // Invokes the mount life-cycles on a previously never rendered instance. function mountClassInstance( workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ): void { const current = workInProgress.alternate; @@ -435,7 +474,7 @@ module.exports = function( instance, state, props, - priorityLevel, + renderExpirationTime, ); } } @@ -553,7 +592,7 @@ module.exports = function( function updateClassInstance( current: Fiber, workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ): boolean { const instance = workInProgress.stateNode; resetInputPointers(workInProgress, instance); @@ -602,7 +641,7 @@ module.exports = function( instance, oldState, newProps, - priorityLevel, + renderExpirationTime, ); } else { newState = oldState; diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 451e379965da1..edb6048e91338 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -12,7 +12,7 @@ import type {ReactCoroutine} from 'ReactTypes'; import type {Fiber} from 'ReactFiber'; -import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; import type {HostContext} from 'ReactFiberHostContext'; import type {HydrationContext} from 'ReactFiberHydrationContext'; import type {FiberRoot} from 'ReactFiberRoot'; @@ -25,7 +25,7 @@ var { } = require('ReactFiberContext'); var ReactTypeOfWork = require('ReactTypeOfWork'); var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect'); -var ReactPriorityLevel = require('ReactPriorityLevel'); +var ReactFiberExpirationTime = require('ReactFiberExpirationTime'); var { IndeterminateComponent, FunctionalComponent, @@ -40,7 +40,7 @@ var { Fragment, } = ReactTypeOfWork; var {Placement, Ref, Update} = ReactTypeOfSideEffect; -var {OffscreenPriority} = ReactPriorityLevel; +var {Never} = ReactFiberExpirationTime; var invariant = require('fbjs/lib/invariant'); @@ -113,6 +113,7 @@ module.exports = function( function moveCoroutineToHandlerPhase( current: Fiber | null, workInProgress: Fiber, + renderExpirationTime: ExpirationTime, ) { var coroutine = (workInProgress.memoizedProps: ?ReactCoroutine); invariant( @@ -139,13 +140,11 @@ module.exports = function( var nextChildren = fn(props, yields); var currentFirstChild = current !== null ? current.child : null; - // Inherit the priority of the returnFiber. - const priority = workInProgress.pendingWorkPriority; workInProgress.child = reconcileChildFibers( workInProgress, currentFirstChild, nextChildren, - priority, + renderExpirationTime, ); return workInProgress.child; } @@ -181,15 +180,15 @@ module.exports = function( function completeWork( current: Fiber | null, workInProgress: Fiber, - renderPriority: PriorityLevel, + renderExpirationTime: ExpirationTime, ): Fiber | null { // Get the latest props. let newProps = workInProgress.pendingProps; if (newProps === null) { newProps = workInProgress.memoizedProps; } else if ( - workInProgress.pendingWorkPriority !== OffscreenPriority || - renderPriority === OffscreenPriority + workInProgress.expirationTime !== Never || + renderExpirationTime === Never ) { // Reset the pending props, unless this was a down-prioritization. workInProgress.pendingProps = null; @@ -358,7 +357,11 @@ module.exports = function( return null; } case CoroutineComponent: - return moveCoroutineToHandlerPhase(current, workInProgress); + return moveCoroutineToHandlerPhase( + current, + workInProgress, + renderExpirationTime, + ); case CoroutineHandlerPhase: // Reset the tag to now be a first phase coroutine. workInProgress.tag = CoroutineComponent; diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js index 1f5d9c42c4e0a..e520995a7e9eb 100644 --- a/src/renderers/shared/fiber/ReactFiberExpirationTime.js +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -28,13 +28,15 @@ export type ExpirationTime = number; const Done = 0; exports.Done = Done; +const MAGIC_NUMBER_OFFSET = 2; + const Never = Number.MAX_SAFE_INTEGER; exports.Never = Never; // 1 unit of expiration time represents 10ms. function msToExpirationTime(ms: number): ExpirationTime { - // Always add 1 so that we don't clash with the magic number for Done. - return Math.round(ms / 10) + 1; + // Always add an offset so that we don't clash with the magic number for Done. + return Math.round(ms / 10) + MAGIC_NUMBER_OFFSET; } exports.msToExpirationTime = msToExpirationTime; @@ -56,7 +58,7 @@ function priorityToExpirationTime( return Done; case SynchronousPriority: // Return a number lower than the current time, but higher than Done. - return 1; + return MAGIC_NUMBER_OFFSET - 1; case TaskPriority: // Return the current time, so that this work completes in this batch. return currentTime; @@ -69,6 +71,7 @@ function priorityToExpirationTime( case OffscreenPriority: return Never; default: + console.log(priorityLevel); invariant( false, 'Switch statement should be exhuastive. ' + @@ -102,3 +105,11 @@ function expirationTimeToPriorityLevel( return LowPriority; } exports.expirationTimeToPriorityLevel = expirationTimeToPriorityLevel; + +function earlierExpirationTime( + t1: ExpirationTime, + t2: ExpirationTime, +): ExpirationTime { + return t1 !== Done && (t2 === Done || t2 > t1) ? t1 : t2; +} +exports.earlierExpirationTime = earlierExpirationTime; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index e20fff429d698..c672fe86a5c55 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -237,6 +237,7 @@ module.exports = function( var { scheduleUpdate, getPriorityContext, + getExpirationTimeForPriority, recalculateCurrentTime, batchedUpdates, unbatchedUpdates, @@ -278,6 +279,10 @@ module.exports = function( (element.type.prototype: any).unstable_isAsyncReactComponent === true; const priorityLevel = getPriorityContext(current, forceAsync); const currentTime = recalculateCurrentTime(); + const expirationTime = getExpirationTimeForPriority( + currentTime, + priorityLevel, + ); const nextState = {element}; callback = callback === undefined ? null : callback; if (__DEV__) { @@ -288,8 +293,15 @@ module.exports = function( callback, ); } - addTopLevelUpdate(current, nextState, callback, priorityLevel, currentTime); - scheduleUpdate(current, priorityLevel); + addTopLevelUpdate( + current, + nextState, + callback, + priorityLevel, + expirationTime, + currentTime, + ); + scheduleUpdate(current, expirationTime); } return { diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 4509708d0fbf4..9fcbf2064bbd1 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -51,7 +51,7 @@ var ReactFiberHydrationContext = require('ReactFiberHydrationContext'); var {ReactCurrentOwner} = require('ReactGlobalSharedState'); var getComponentName = require('getComponentName'); -var {createWorkInProgress, largerPriority} = require('ReactFiber'); +var {createWorkInProgress} = require('ReactFiber'); var {onCommitRoot} = require('ReactFiberDevToolsHook'); var { @@ -63,7 +63,14 @@ var { OffscreenPriority, } = require('ReactPriorityLevel'); -var {msToExpirationTime} = require('ReactFiberExpirationTime'); +var { + Done, + Never, + msToExpirationTime, + earlierExpirationTime, + priorityToExpirationTime, + expirationTimeToPriorityLevel, +} = require('ReactFiberExpirationTime'); var {AsyncUpdates} = require('ReactTypeOfInternalContext'); @@ -86,7 +93,7 @@ var { ClassComponent, } = require('ReactTypeOfWork'); -var {getUpdatePriority} = require('ReactFiberUpdateQueue'); +var {getUpdateExpirationTime} = require('ReactFiberUpdateQueue'); var {resetContext} = require('ReactFiberContext'); @@ -170,6 +177,7 @@ module.exports = function( scheduleUpdate, getPriorityContext, recalculateCurrentTime, + getExpirationTimeForPriority, ); const {completeWork} = ReactFiberCompleteWork( config, @@ -193,12 +201,10 @@ module.exports = function( } = config; // Represents the current time in ms. - let currentTime: ExpirationTime = msToExpirationTime(now()); + let mostRecentCurrentTime: ExpirationTime = msToExpirationTime(now()); // The priority level to use when scheduling an update. We use NoWork to // represent the default priority. - // TODO: Should we change this to an array instead of using the call stack? - // Might be less confusing. let priorityContext: PriorityLevel = NoWork; // Keeps track of whether we're currently in a work loop. @@ -216,7 +222,8 @@ module.exports = function( // The next work in progress fiber that we're currently working on. let nextUnitOfWork: Fiber | null = null; - let nextPriorityLevel: PriorityLevel = NoWork; + // The time at which we're currently rendering work. + let nextRenderExpirationTime: ExpirationTime = Done; // The next fiber with an effect that we're currently committing. let nextEffect: Fiber | null = null; @@ -259,14 +266,11 @@ module.exports = function( resetHostContainer(); } - // resetNextUnitOfWork mutates the current priority context. It is reset after - // after the workLoop exits, so never call resetNextUnitOfWork from outside - // the work loop. function resetNextUnitOfWork() { // Clear out roots with no more work on them, or if they have uncaught errors while ( nextScheduledRoot !== null && - nextScheduledRoot.current.pendingWorkPriority === NoWork + nextScheduledRoot.current.expirationTime === Done ) { // Unschedule this root. nextScheduledRoot.isScheduled = false; @@ -278,7 +282,7 @@ module.exports = function( if (nextScheduledRoot === lastScheduledRoot) { nextScheduledRoot = null; lastScheduledRoot = null; - nextPriorityLevel = NoWork; + nextRenderExpirationTime = Done; return null; } // Continue with the next root. @@ -287,22 +291,22 @@ module.exports = function( } let root = nextScheduledRoot; - let highestPriorityRoot = null; - let highestPriorityLevel = NoWork; + let earliestExpirationRoot = null; + let earliestExpirationTime = Done; while (root !== null) { if ( - root.current.pendingWorkPriority !== NoWork && - (highestPriorityLevel === NoWork || - highestPriorityLevel > root.current.pendingWorkPriority) + root.current.expirationTime !== Done && + (earliestExpirationTime === Done || + earliestExpirationTime > root.current.expirationTime) ) { - highestPriorityLevel = root.current.pendingWorkPriority; - highestPriorityRoot = root; + earliestExpirationTime = root.current.expirationTime; + earliestExpirationRoot = root; } // We didn't find anything to do in this root, so let's try the next one. root = root.nextScheduledRoot; } - if (highestPriorityRoot !== null) { - nextPriorityLevel = highestPriorityLevel; + if (earliestExpirationRoot !== null) { + nextRenderExpirationTime = earliestExpirationTime; // Before we start any new work, let's make sure that we have a fresh // stack to work from. // TODO: This call is buried a bit too deep. It would be nice to have @@ -311,18 +315,18 @@ module.exports = function( resetContextStack(); nextUnitOfWork = createWorkInProgress( - highestPriorityRoot.current, - highestPriorityLevel, + earliestExpirationRoot.current, + earliestExpirationTime, ); - if (highestPriorityRoot !== nextRenderedTree) { + if (earliestExpirationRoot !== nextRenderedTree) { // We've switched trees. Reset the nested update counter. nestedUpdateCount = 0; - nextRenderedTree = highestPriorityRoot; + nextRenderedTree = earliestExpirationRoot; } return; } - nextPriorityLevel = NoWork; + nextRenderExpirationTime = Done; nextUnitOfWork = null; nextRenderedTree = null; return; @@ -400,7 +404,6 @@ module.exports = function( while (nextEffect !== null) { const effectTag = nextEffect.effectTag; - // Use Task priority for lifecycle updates if (effectTag & (Update | Callback)) { if (__DEV__) { recordEffect(); @@ -454,10 +457,7 @@ module.exports = function( 'in React. Please file an issue.', ); - if ( - nextPriorityLevel === SynchronousPriority || - nextPriorityLevel === TaskPriority - ) { + if (nextRenderExpirationTime <= mostRecentCurrentTime) { // Keep track of the number of iterations to prevent an infinite // update loop. nestedUpdateCount++; @@ -591,35 +591,36 @@ module.exports = function( commitPhaseBoundaries = null; } - // This tree is done. Reset the unit of work pointer to the next highest - // priority root. If there's no more work left, the pointer is set to null. + // This tree is done. Reset the unit of work pointer to the root that + // expires soonest. If there's no work left, the pointer is set to null. resetNextUnitOfWork(); } - function resetWorkPriority( + function resetExpirationTime( workInProgress: Fiber, - renderPriority: PriorityLevel, + renderTime: ExpirationTime, ) { - if ( - workInProgress.pendingWorkPriority !== NoWork && - workInProgress.pendingWorkPriority > renderPriority - ) { - // This was a down-prioritization. Don't bubble priority from children. + if (renderTime !== Never && workInProgress.expirationTime === Never) { + // The children of this component are hidden. Don't bubble their + // expiration times. return; } - // Check for pending update priority. - let newPriority = getUpdatePriority(workInProgress); + // Check for pending updates. + let newExpirationTime = getUpdateExpirationTime(workInProgress); // TODO: Coroutines need to visit stateNode + // Bubble up the earliest expiration time. let child = workInProgress.child; while (child !== null) { - // Ensure that remaining work priority bubbles up. - newPriority = largerPriority(newPriority, child.pendingWorkPriority); + newExpirationTime = earlierExpirationTime( + newExpirationTime, + child.expirationTime, + ); child = child.sibling; } - workInProgress.pendingWorkPriority = newPriority; + workInProgress.expirationTime = newExpirationTime; } function completeUnitOfWork(workInProgress: Fiber): Fiber | null { @@ -632,7 +633,11 @@ module.exports = function( if (__DEV__) { ReactDebugCurrentFiber.setCurrentFiber(workInProgress); } - const next = completeWork(current, workInProgress, nextPriorityLevel); + const next = completeWork( + current, + workInProgress, + nextRenderExpirationTime, + ); if (__DEV__) { ReactDebugCurrentFiber.resetCurrentFiber(); } @@ -640,7 +645,7 @@ module.exports = function( const returnFiber = workInProgress.return; const siblingFiber = workInProgress.sibling; - resetWorkPriority(workInProgress, nextPriorityLevel); + resetExpirationTime(workInProgress, nextRenderExpirationTime); if (next !== null) { if (__DEV__) { @@ -728,7 +733,7 @@ module.exports = function( startWorkTimer(workInProgress); ReactDebugCurrentFiber.setCurrentFiber(workInProgress); } - let next = beginWork(current, workInProgress, nextPriorityLevel); + let next = beginWork(current, workInProgress, nextRenderExpirationTime); if (__DEV__) { ReactDebugCurrentFiber.resetCurrentFiber(); } @@ -758,7 +763,11 @@ module.exports = function( startWorkTimer(workInProgress); ReactDebugCurrentFiber.setCurrentFiber(workInProgress); } - let next = beginFailedWork(current, workInProgress, nextPriorityLevel); + let next = beginFailedWork( + current, + workInProgress, + nextRenderExpirationTime, + ); if (__DEV__) { ReactDebugCurrentFiber.resetCurrentFiber(); } @@ -793,7 +802,8 @@ module.exports = function( if ( capturedErrors !== null && capturedErrors.size > 0 && - nextPriorityLevel === TaskPriority + nextRenderExpirationTime !== Done && + nextRenderExpirationTime <= mostRecentCurrentTime ) { while (nextUnitOfWork !== null) { if (hasCapturedError(nextUnitOfWork)) { @@ -809,14 +819,12 @@ module.exports = function( 'a bug in React. Please file an issue.', ); // We just completed a root. Commit it now. - priorityContext = TaskPriority; commitAllWork(pendingCommit); - priorityContext = nextPriorityLevel; - if ( capturedErrors === null || capturedErrors.size === 0 || - nextPriorityLevel !== TaskPriority + nextRenderExpirationTime === Done || + nextRenderExpirationTime > mostRecentCurrentTime ) { // There are no more unhandled errors. We can exit this special // work loop. If there's still additional work, we'll perform it @@ -830,28 +838,26 @@ module.exports = function( } function workLoop( - minPriorityLevel: PriorityLevel, + minExpirationTime: ExpirationTime, deadline: Deadline | null, ) { if (pendingCommit !== null) { - priorityContext = TaskPriority; commitAllWork(pendingCommit); handleCommitPhaseErrors(); } else if (nextUnitOfWork === null) { resetNextUnitOfWork(); } - if (nextPriorityLevel === NoWork || nextPriorityLevel > minPriorityLevel) { + if ( + nextRenderExpirationTime === Done || + nextRenderExpirationTime > minExpirationTime + ) { return; } - // During the render phase, updates should have the same priority at which - // we're rendering. - priorityContext = nextPriorityLevel; - loop: do { - if (nextPriorityLevel <= TaskPriority) { - // Flush all synchronous and task work. + if (nextRenderExpirationTime <= mostRecentCurrentTime) { + // Flush all expired work. while (nextUnitOfWork !== null) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); if (nextUnitOfWork === null) { @@ -861,24 +867,22 @@ module.exports = function( 'a bug in React. Please file an issue.', ); // We just completed a root. Commit it now. - priorityContext = TaskPriority; commitAllWork(pendingCommit); - priorityContext = nextPriorityLevel; // Clear any errors that were scheduled during the commit phase. handleCommitPhaseErrors(); - // The priority level may have changed. Check again. + // The render time may have changed. Check again. if ( - nextPriorityLevel === NoWork || - nextPriorityLevel > minPriorityLevel || - nextPriorityLevel > TaskPriority + nextRenderExpirationTime === Done || + nextRenderExpirationTime > minExpirationTime || + nextRenderExpirationTime > mostRecentCurrentTime ) { - // The priority level does not match. + // We've completed all the expired work. break; } } } } else if (deadline !== null) { - // Flush asynchronous work until the deadline expires. + // Flush asynchronous work until the deadline runs out of time. while (nextUnitOfWork !== null && !deadlineHasExpired) { if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); @@ -895,18 +899,16 @@ module.exports = function( // We just completed a root. If we have time, commit it now. // Otherwise, we'll commit it in the next frame. if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { - priorityContext = TaskPriority; commitAllWork(pendingCommit); - priorityContext = nextPriorityLevel; // Clear any errors that were scheduled during the commit phase. handleCommitPhaseErrors(); - // The priority level may have changed. Check again. + // The render time may have changed. Check again. if ( - nextPriorityLevel === NoWork || - nextPriorityLevel > minPriorityLevel || - nextPriorityLevel < HighPriority + nextRenderExpirationTime === Done || + nextRenderExpirationTime > minExpirationTime || + nextRenderExpirationTime <= mostRecentCurrentTime ) { - // The priority level does not match. + // We've completed all the async work. break; } } else { @@ -921,12 +923,16 @@ module.exports = function( // There might be work left. Depending on the priority, we should // either perform it now or schedule a callback to perform it later. - switch (nextPriorityLevel) { + const currentTime = recalculateCurrentTime(); + switch (expirationTimeToPriorityLevel( + currentTime, + nextRenderExpirationTime, + )) { case SynchronousPriority: case TaskPriority: // We have remaining synchronous or task work. Keep performing it, // regardless of whether we're inside a callback. - if (nextPriorityLevel <= minPriorityLevel) { + if (nextRenderExpirationTime <= minExpirationTime) { continue loop; } break loop; @@ -940,7 +946,10 @@ module.exports = function( break loop; } // We are inside a callback. - if (!deadlineHasExpired && nextPriorityLevel <= minPriorityLevel) { + if ( + !deadlineHasExpired && + nextRenderExpirationTime <= minExpirationTime + ) { // We still have time. Keep working. continue loop; } @@ -962,7 +971,7 @@ module.exports = function( function performWorkCatchBlock( failedWork: Fiber, boundary: Fiber, - minPriorityLevel: PriorityLevel, + minExpirationTime: ExpirationTime, deadline: Deadline | null, ) { // We're going to restart the error boundary that captured the error. @@ -978,7 +987,7 @@ module.exports = function( nextUnitOfWork = performFailedUnitOfWork(boundary); // Continue working. - workLoop(minPriorityLevel, deadline); + workLoop(minExpirationTime, deadline); } function performWork( @@ -996,21 +1005,32 @@ module.exports = function( ); isPerformingWork = true; - // The priority context changes during the render phase. We'll need to - // reset it at the end. + // Updates that occur during the commit phase should have task priority + // by default. (Render phase updates are special; getPriorityContext + // accounts for their behavior.) const previousPriorityContext = priorityContext; + priorityContext = TaskPriority; + + // Read the current time from the host environment. + const currentTime = recalculateCurrentTime(); + const minExpirationTime = priorityToExpirationTime( + currentTime, + minPriorityLevel, + ); + + nestedUpdateCount = 0; let didError = false; let error = null; if (__DEV__) { - invokeGuardedCallback(null, workLoop, null, minPriorityLevel, deadline); + invokeGuardedCallback(null, workLoop, null, minExpirationTime, deadline); if (hasCaughtError()) { didError = true; error = clearCaughtError(); } } else { try { - workLoop(minPriorityLevel, deadline); + workLoop(minExpirationTime, deadline); } catch (e) { didError = true; error = e; @@ -1057,7 +1077,7 @@ module.exports = function( null, failedWork, boundary, - minPriorityLevel, + minExpirationTime, deadline, ); if (hasCaughtError()) { @@ -1070,7 +1090,7 @@ module.exports = function( performWorkCatchBlock( failedWork, boundary, - minPriorityLevel, + minExpirationTime, deadline, ); error = null; @@ -1084,19 +1104,21 @@ module.exports = function( break; } - // Reset the priority context to its previous value. - priorityContext = previousPriorityContext; - // If we're inside a callback, set this to false, since we just flushed it. if (deadline !== null) { isCallbackScheduled = false; } // If there's remaining async work, make sure we schedule another callback. - if (nextPriorityLevel > TaskPriority && !isCallbackScheduled) { + if ( + nextRenderExpirationTime > mostRecentCurrentTime && + !isCallbackScheduled + ) { scheduleDeferredCallback(performDeferredWork); isCallbackScheduled = true; } + priorityContext = previousPriorityContext; + const errorToThrow = firstUncaughtError; // We're done performing work. Time to clean up. @@ -1361,8 +1383,8 @@ module.exports = function( } } - function scheduleRoot(root: FiberRoot, priorityLevel: PriorityLevel) { - if (priorityLevel === NoWork) { + function scheduleRoot(root: FiberRoot, expirationTime: ExpirationTime) { + if (expirationTime === Done) { return; } @@ -1380,13 +1402,13 @@ module.exports = function( } } - function scheduleUpdate(fiber: Fiber, priorityLevel: PriorityLevel) { - return scheduleUpdateImpl(fiber, priorityLevel, false); + function scheduleUpdate(fiber: Fiber, expirationTime: ExpirationTime) { + return scheduleUpdateImpl(fiber, expirationTime, false); } function scheduleUpdateImpl( fiber: Fiber, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, isErrorRecovery: boolean, ) { if (__DEV__) { @@ -1404,7 +1426,7 @@ module.exports = function( ); } - if (!isPerformingWork && priorityLevel <= nextPriorityLevel) { + if (!isPerformingWork && expirationTime <= nextRenderExpirationTime) { // We must reset the current unit of work pointer so that we restart the // search from the root during the next tick, in case there is now higher // priority work somewhere earlier than before. @@ -1421,59 +1443,58 @@ module.exports = function( let node = fiber; let shouldContinue = true; while (node !== null && shouldContinue) { - // Walk the parent path to the root and update each node's priority. Once - // we reach a node whose priority matches (and whose alternate's priority - // matches) we can exit safely knowing that the rest of the path is correct. + // Walk the parent path to the root and update each node's expiration + // time. Once we reach a node whose expiration matches (and whose + // alternate's expiration matches) we can exit safely knowing that the + // rest of the path is correct. shouldContinue = false; if ( - node.pendingWorkPriority === NoWork || - node.pendingWorkPriority > priorityLevel + node.expirationTime === Done || + node.expirationTime > expirationTime ) { - // Priority did not match. Update and keep going. + // Expiration time did not match. Update and keep going. shouldContinue = true; - node.pendingWorkPriority = priorityLevel; + node.expirationTime = expirationTime; } if (node.alternate !== null) { if ( - node.alternate.pendingWorkPriority === NoWork || - node.alternate.pendingWorkPriority > priorityLevel + node.alternate.expirationTime === Done || + node.alternate.expirationTime > expirationTime ) { - // Priority did not match. Update and keep going. + // Expiration time did not match. Update and keep going. shouldContinue = true; - node.alternate.pendingWorkPriority = priorityLevel; + node.alternate.expirationTime = expirationTime; } } if (node.return === null) { if (node.tag === HostRoot) { const root: FiberRoot = (node.stateNode: any); - scheduleRoot(root, priorityLevel); + scheduleRoot(root, expirationTime); if (!isPerformingWork) { - switch (priorityLevel) { - case SynchronousPriority: - // Perform this update now. - if (isUnbatchingUpdates) { - // We're inside unbatchedUpdates, which is inside either - // batchedUpdates or a lifecycle. We should only flush - // synchronous work, not task work. - performWork(SynchronousPriority, null); - } else { - // Flush both synchronous and task work. - performWork(TaskPriority, null); - } - break; - case TaskPriority: - invariant( - isBatchingUpdates, - 'Task updates can only be scheduled as a nested update or ' + - 'inside batchedUpdates.', - ); - break; - default: - // Schedule a callback to perform the work later. - if (!isCallbackScheduled) { - scheduleDeferredCallback(performDeferredWork); - isCallbackScheduled = true; - } + if (expirationTime < mostRecentCurrentTime) { + // This update is synchronous. Perform it now. + if (isUnbatchingUpdates) { + // We're inside unbatchedUpdates, which is inside either + // batchedUpdates or a lifecycle. We should only flush + // synchronous work, not task work. + performWork(SynchronousPriority, null); + } else { + // Flush both synchronous and task work. + performWork(TaskPriority, null); + } + } else if (expirationTime === mostRecentCurrentTime) { + invariant( + isBatchingUpdates, + 'Task updates can only be scheduled as a nested update or ' + + 'inside batchedUpdates. This error is likely caused by a ' + + 'bug in React. Please file an issue.', + ); + } else { + // This update is async. Schedule a callback. + if (!isCallbackScheduled) { + scheduleDeferredCallback(performDeferredWork); + isCallbackScheduled = true; + } } } } else { @@ -1492,7 +1513,13 @@ module.exports = function( function getPriorityContext( fiber: Fiber, forceAsync: boolean, - ): PriorityLevel { + ): PriorityLevel | null { + if (isPerformingWork && !isCommitting) { + // Updates during the render phase should expire at the same time as + // the work that is being rendered. Return null to indicate. + return null; + } + let priorityLevel = priorityContext; if (priorityLevel === NoWork) { if ( @@ -1517,13 +1544,29 @@ module.exports = function( return priorityLevel; } + function getExpirationTimeForPriority( + currentTime: ExpirationTime, + priorityLevel: PriorityLevel | null, + ): ExpirationTime { + if (priorityLevel === null) { + // A priorityLevel of null indicates that this update should expire at + // the same time as whatever is currently being rendered. + return nextRenderExpirationTime; + } + return priorityToExpirationTime(currentTime, priorityLevel); + } + function scheduleErrorRecovery(fiber: Fiber) { - scheduleUpdateImpl(fiber, TaskPriority, true); + scheduleUpdateImpl( + fiber, + priorityToExpirationTime(mostRecentCurrentTime, TaskPriority), + true, + ); } function recalculateCurrentTime(): ExpirationTime { - currentTime = msToExpirationTime(now()); - return currentTime; + mostRecentCurrentTime = msToExpirationTime(now()); + return mostRecentCurrentTime; } function batchedUpdates(fn: (a: A) => R, a: A): R { @@ -1589,6 +1632,7 @@ module.exports = function( scheduleUpdate: scheduleUpdate, getPriorityContext: getPriorityContext, recalculateCurrentTime: recalculateCurrentTime, + getExpirationTimeForPriority: getExpirationTimeForPriority, batchedUpdates: batchedUpdates, unbatchedUpdates: unbatchedUpdates, flushSync: flushSync, diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index 4b40e0e565d78..03d1532e168b6 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -16,13 +16,7 @@ import type {ExpirationTime} from 'ReactFiberExpirationTime'; const {Callback: CallbackEffect} = require('ReactTypeOfSideEffect'); -const { - NoWork, - SynchronousPriority, - TaskPriority, -} = require('ReactPriorityLevel'); - -const {Done, priorityToExpirationTime} = require('ReactFiberExpirationTime'); +const {Done} = require('ReactFiberExpirationTime'); const {ClassComponent, HostRoot} = require('ReactTypeOfWork'); @@ -39,7 +33,7 @@ type PartialState = type Callback = mixed; export type Update = { - priorityLevel: PriorityLevel, + priorityLevel: PriorityLevel | null, expirationTime: ExpirationTime, partialState: PartialState, callback: Callback | null, @@ -73,25 +67,6 @@ export type UpdateQueue = { let _queue1; let _queue2; -function comparePriority(a: PriorityLevel, b: PriorityLevel): number { - // When comparing update priorities, treat sync and Task work as equal. - // TODO: Could we avoid the need for this by always coercing sync priority - // to Task when scheduling an update? - if ( - (a === TaskPriority || a === SynchronousPriority) && - (b === TaskPriority || b === SynchronousPriority) - ) { - return 0; - } - if (a === NoWork && b !== NoWork) { - return -255; - } - if (a !== NoWork && b === NoWork) { - return 255; - } - return a - b; -} - function createUpdateQueue(): UpdateQueue { const queue: UpdateQueue = { first: null, @@ -127,9 +102,6 @@ function insertUpdateIntoQueue( insertBefore: Update | null, currentTime: ExpirationTime, ) { - const priorityLevel = update.priorityLevel; - - let coalescedTime: ExpirationTime | null = null; if (insertAfter !== null) { insertAfter.next = update; // If we receive multiple updates to the same fiber at the same priority @@ -137,10 +109,13 @@ function insertUpdateIntoQueue( // they all flush at the same time. Because this causes an interruption, it // could lead to starvation, so we stop coalescing once the time until the // expiration time reaches a certain threshold. - if (insertAfter !== null && insertAfter.priorityLevel === priorityLevel) { - const expirationTime = insertAfter.expirationTime; - if (expirationTime - currentTime > COALESCENCE_THRESHOLD) { - coalescedTime = expirationTime; + if ( + insertAfter !== null && + insertAfter.priorityLevel === update.priorityLevel + ) { + const coalescedTime = insertAfter.expirationTime; + if (coalescedTime - currentTime > COALESCENCE_THRESHOLD) { + update.expirationTime = coalescedTime; } } } else { @@ -148,11 +123,6 @@ function insertUpdateIntoQueue( update.next = queue.first; queue.first = update; } - update.expirationTime = coalescedTime !== null - ? coalescedTime - : // If we don't coalesce, calculate the expiration time using the - // current time. - priorityToExpirationTime(currentTime, priorityLevel); if (insertBefore !== null) { update.next = insertBefore; @@ -165,13 +135,10 @@ function insertUpdateIntoQueue( // Returns the update after which the incoming update should be inserted into // the queue, or null if it should be inserted at beginning. function findInsertionPosition(queue, update): Update | null { - const priorityLevel = update.priorityLevel; + const expirationTime = update.expirationTime; let insertAfter = null; let insertBefore = null; - if ( - queue.last !== null && - comparePriority(queue.last.priorityLevel, priorityLevel) <= 0 - ) { + if (queue.last !== null && queue.last.expirationTime <= expirationTime) { // Fast path for the common case where the update should be inserted at // the end of the queue. insertAfter = queue.last; @@ -179,7 +146,7 @@ function findInsertionPosition(queue, update): Update | null { insertBefore = queue.first; while ( insertBefore !== null && - comparePriority(insertBefore.priorityLevel, priorityLevel) <= 0 + insertBefore.expirationTime <= expirationTime ) { insertAfter = insertBefore; insertBefore = insertBefore.next; @@ -333,12 +300,13 @@ function addUpdate( fiber: Fiber, partialState: PartialState | null, callback: mixed, - priorityLevel: PriorityLevel, + priorityLevel: PriorityLevel | null, + expirationTime: ExpirationTime, currentTime: ExpirationTime, ): void { const update = { priorityLevel, - expirationTime: Done, + expirationTime, partialState, callback, isReplace: false, @@ -354,12 +322,13 @@ function addReplaceUpdate( fiber: Fiber, state: any | null, callback: Callback | null, - priorityLevel: PriorityLevel, + priorityLevel: PriorityLevel | null, + expirationTime: ExpirationTime, currentTime: ExpirationTime, ): void { const update = { priorityLevel, - expirationTime: Done, + expirationTime, partialState: state, callback, isReplace: true, @@ -374,12 +343,13 @@ exports.addReplaceUpdate = addReplaceUpdate; function addForceUpdate( fiber: Fiber, callback: Callback | null, - priorityLevel: PriorityLevel, + priorityLevel: PriorityLevel | null, + expirationTime: ExpirationTime, currentTime: ExpirationTime, ): void { const update = { priorityLevel, - expirationTime: Done, + expirationTime, partialState: null, callback, isReplace: false, @@ -391,30 +361,31 @@ function addForceUpdate( } exports.addForceUpdate = addForceUpdate; -function getUpdatePriority(fiber: Fiber): PriorityLevel { +function getUpdateExpirationTime(fiber: Fiber): ExpirationTime { const updateQueue = fiber.updateQueue; if (updateQueue === null) { - return NoWork; + return Done; } if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) { - return NoWork; + return Done; } - return updateQueue.first !== null ? updateQueue.first.priorityLevel : NoWork; + return updateQueue.first !== null ? updateQueue.first.expirationTime : Done; } -exports.getUpdatePriority = getUpdatePriority; +exports.getUpdateExpirationTime = getUpdateExpirationTime; function addTopLevelUpdate( fiber: Fiber, partialState: PartialState, callback: Callback | null, - priorityLevel: PriorityLevel, + priorityLevel: PriorityLevel | null, + expirationTime: ExpirationTime, currentTime: ExpirationTime, ): void { const isTopLevelUnmount = partialState.element === null; const update = { priorityLevel, - expirationTime: Done, + expirationTime, partialState, callback, isReplace: false, @@ -461,7 +432,7 @@ function beginUpdateQueue( instance: any, prevState: any, props: any, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ): any { if (current !== null && current.updateQueue === queue) { // We need to create a work-in-progress queue, by cloning the current queue. @@ -491,10 +462,7 @@ function beginUpdateQueue( let state = prevState; let dontMutatePrevState = true; let update = queue.first; - while ( - update !== null && - comparePriority(update.priorityLevel, priorityLevel) <= 0 - ) { + while (update !== null && update.expirationTime <= renderExpirationTime) { // Remove each update from the queue right before it is processed. That way // if setState is called from inside an updater function, the new update // will be inserted in the correct position. From 6d1e2d89ac9472368b5d52f167ca7b00b0f44bd0 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Aug 2017 09:03:49 -0700 Subject: [PATCH 03/24] Triangle Demo should use a class shouldComponentUpdate was removed from functional components. Running the demo shows, now that expiration is enabled, the demo does not starve. (Still won't run smoothly until we add back the ability to resume interrupted work.) --- fixtures/fiber-triangle/index.html | 81 ++++++++++--------- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 31 +++---- 2 files changed, 58 insertions(+), 54 deletions(-) diff --git a/fixtures/fiber-triangle/index.html b/fixtures/fiber-triangle/index.html index d2541eee1e93e..1b5941f09b317 100644 --- a/fixtures/fiber-triangle/index.html +++ b/fixtures/fiber-triangle/index.html @@ -76,51 +76,54 @@

Fiber Example

} } - function SierpinskiTriangle({ x, y, s, children }) { - if (s <= targetSize) { - return ( - + class SierpinskiTriangle extends React.Component { + shouldComponentUpdate(nextProps) { + var o = this.props; + var n = nextProps; + return !( + o.x === n.x && + o.y === n.y && + o.s === n.s && + o.children === n.children ); - return r; } - var newSize = s / 2; - var slowDown = true; - if (slowDown) { - var e = performance.now() + 0.8; - while (performance.now() < e) { - // Artificially long execution time. + render() { + let {x, y, s, children} = this.props; + if (s <= targetSize) { + return ( + + ); + return r; + } + var newSize = s / 2; + var slowDown = true; + if (slowDown) { + var e = performance.now() + 0.8; + while (performance.now() < e) { + // Artificially long execution time. + } } - } - s /= 2; + s /= 2; - return [ - - {children} - , - - {children} - , - - {children} - , - ]; + return [ + + {children} + , + + {children} + , + + {children} + , + ]; + } } - SierpinskiTriangle.shouldComponentUpdate = function(oldProps, newProps) { - var o = oldProps; - var n = newProps; - return !( - o.x === n.x && - o.y === n.y && - o.s === n.s && - o.children === n.children - ); - }; class ExampleApplication extends React.Component { constructor() { diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 5dbda35eb0b3c..0b09818ee60ef 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -163,20 +163,20 @@ function shouldAutoFocusHostComponent(type: string, props: Props): boolean { } // TODO: Better polyfill -// let now; -// if ( -// typeof window !== 'undefined' && -// window.performance && -// typeof window.performance.now === 'function' -// ) { -// now = function() { -// return performance.now(); -// }; -// } else { -// now = function() { -// return Date.now(); -// }; -// } +let now; +if ( + typeof window !== 'undefined' && + window.performance && + typeof window.performance.now === 'function' +) { + now = function() { + return performance.now(); + }; +} else { + now = function() { + return Date.now(); + }; +} var DOMRenderer = ReactFiberReconciler({ getRootHostContext(rootContainerInstance: Container): HostContext { @@ -455,7 +455,8 @@ var DOMRenderer = ReactFiberReconciler({ now() { // TODO: Use performance.now to enable expiration - return 0; + // return 0; + return now(); }, canHydrateInstance( From 383c50e1ec39ae14111cb8bf5272057f12041d3d Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Aug 2017 11:30:17 -0700 Subject: [PATCH 04/24] Use a magic value for task expiration time There are a few cases related to sync mode where we need to distinguish between work that is scheduled as task and work that is treated like task because it expires. For example, batchedUpdates. We don't want to perform any work until the end of the batch, regardless of how much time has elapsed. --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 6 +- .../shared/fiber/ReactFiberExpirationTime.js | 36 ++++++----- .../shared/fiber/ReactFiberScheduler.js | 64 ++++++++++--------- 3 files changed, 55 insertions(+), 51 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 0b09818ee60ef..8250d1773b43c 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -453,11 +453,7 @@ var DOMRenderer = ReactFiberReconciler({ } }, - now() { - // TODO: Use performance.now to enable expiration - // return 0; - return now(); - }, + now, canHydrateInstance( instance: Instance | TextInstance, diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js index e520995a7e9eb..9fd65f1c758ff 100644 --- a/src/renderers/shared/fiber/ReactFiberExpirationTime.js +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -26,11 +26,13 @@ const invariant = require('fbjs/lib/invariant'); export type ExpirationTime = number; const Done = 0; -exports.Done = Done; +const Sync = 1; +const Task = 2; +const Never = Number.MAX_SAFE_INTEGER; -const MAGIC_NUMBER_OFFSET = 2; +const MAGIC_NUMBER_OFFSET = 10; -const Never = Number.MAX_SAFE_INTEGER; +exports.Done = Done; exports.Never = Never; // 1 unit of expiration time represents 10ms. @@ -57,11 +59,9 @@ function priorityToExpirationTime( case NoWork: return Done; case SynchronousPriority: - // Return a number lower than the current time, but higher than Done. - return MAGIC_NUMBER_OFFSET - 1; + return Sync; case TaskPriority: - // Return the current time, so that this work completes in this batch. - return currentTime; + return Task; case HighPriority: // Should complete within ~100ms. 120ms max. return msToExpirationTime(ceiling(100, 20)); @@ -71,7 +71,6 @@ function priorityToExpirationTime( case OffscreenPriority: return Never; default: - console.log(priorityLevel); invariant( false, 'Switch statement should be exhuastive. ' + @@ -89,16 +88,19 @@ function expirationTimeToPriorityLevel( expirationTime: ExpirationTime, ): PriorityLevel { // First check for magic values - if (expirationTime === Done) { - return NoWork; - } - if (expirationTime === Never) { - return OffscreenPriority; - } - if (expirationTime < currentTime) { - return SynchronousPriority; + switch (expirationTime) { + case Done: + return NoWork; + case Sync: + return SynchronousPriority; + case Task: + return TaskPriority; + case Never: + return OffscreenPriority; + default: + break; } - if (expirationTime === currentTime) { + if (expirationTime <= currentTime) { return TaskPriority; } // TODO: We don't currently distinguish between high and low priority. diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 9fcbf2064bbd1..f7506449e9424 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -1013,7 +1013,7 @@ module.exports = function( // Read the current time from the host environment. const currentTime = recalculateCurrentTime(); - const minExpirationTime = priorityToExpirationTime( + const minExpirationTime = getExpirationTimeForPriority( currentTime, minPriorityLevel, ); @@ -1471,30 +1471,36 @@ module.exports = function( const root: FiberRoot = (node.stateNode: any); scheduleRoot(root, expirationTime); if (!isPerformingWork) { - if (expirationTime < mostRecentCurrentTime) { - // This update is synchronous. Perform it now. - if (isUnbatchingUpdates) { - // We're inside unbatchedUpdates, which is inside either - // batchedUpdates or a lifecycle. We should only flush - // synchronous work, not task work. - performWork(SynchronousPriority, null); - } else { - // Flush both synchronous and task work. - performWork(TaskPriority, null); - } - } else if (expirationTime === mostRecentCurrentTime) { - invariant( - isBatchingUpdates, - 'Task updates can only be scheduled as a nested update or ' + - 'inside batchedUpdates. This error is likely caused by a ' + - 'bug in React. Please file an issue.', - ); - } else { - // This update is async. Schedule a callback. - if (!isCallbackScheduled) { - scheduleDeferredCallback(performDeferredWork); - isCallbackScheduled = true; - } + const priorityLevel = expirationTimeToPriorityLevel( + mostRecentCurrentTime, + expirationTime, + ); + switch (priorityLevel) { + case SynchronousPriority: + if (isUnbatchingUpdates) { + // We're inside unbatchedUpdates, which is inside either + // batchedUpdates or a lifecycle. We should only flush + // synchronous work, not task work. + performWork(SynchronousPriority, null); + } else { + // Flush both synchronous and task work. + performWork(TaskPriority, null); + } + break; + case TaskPriority: + invariant( + isBatchingUpdates, + 'Task updates can only be scheduled as a nested update or ' + + 'inside batchedUpdates. This error is likely caused by a ' + + 'bug in React. Please file an issue.', + ); + break; + default: + // This update is async. Schedule a callback. + if (!isCallbackScheduled) { + scheduleDeferredCallback(performDeferredWork); + isCallbackScheduled = true; + } } } } else { @@ -1557,11 +1563,11 @@ module.exports = function( } function scheduleErrorRecovery(fiber: Fiber) { - scheduleUpdateImpl( - fiber, - priorityToExpirationTime(mostRecentCurrentTime, TaskPriority), - true, + const taskTime = getExpirationTimeForPriority( + mostRecentCurrentTime, + TaskPriority, ); + scheduleUpdateImpl(fiber, taskTime, true); } function recalculateCurrentTime(): ExpirationTime { From 466c746d8fe32a1bb50f540fdf2c41a92a612503 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Aug 2017 13:52:29 -0700 Subject: [PATCH 05/24] Use current time to calculate expiration time --- .../shared/fiber/ReactFiberExpirationTime.js | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js index 9fd65f1c758ff..851316454e5fa 100644 --- a/src/renderers/shared/fiber/ReactFiberExpirationTime.js +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -30,6 +30,7 @@ const Sync = 1; const Task = 2; const Never = Number.MAX_SAFE_INTEGER; +const UNIT_SIZE = 10; const MAGIC_NUMBER_OFFSET = 10; exports.Done = Done; @@ -38,12 +39,23 @@ exports.Never = Never; // 1 unit of expiration time represents 10ms. function msToExpirationTime(ms: number): ExpirationTime { // Always add an offset so that we don't clash with the magic number for Done. - return Math.round(ms / 10) + MAGIC_NUMBER_OFFSET; + return Math.round(ms / UNIT_SIZE) + MAGIC_NUMBER_OFFSET; } exports.msToExpirationTime = msToExpirationTime; -function ceiling(time: ExpirationTime, precision: number): ExpirationTime { - return Math.ceil(Math.ceil(time * precision) / precision); +function ceiling(num: number, precision: number): number { + return Math.ceil(Math.ceil(num * precision) / precision); +} + +function bucket( + currentTime: ExpirationTime, + expirationInMs: number, + precisionInMs: number, +): ExpirationTime { + return ceiling( + currentTime + expirationInMs / UNIT_SIZE, + precisionInMs / UNIT_SIZE, + ); } // Given the current clock time and a priority level, returns an expiration time @@ -62,12 +74,14 @@ function priorityToExpirationTime( return Sync; case TaskPriority: return Task; - case HighPriority: + case HighPriority: { // Should complete within ~100ms. 120ms max. - return msToExpirationTime(ceiling(100, 20)); - case LowPriority: + return bucket(currentTime, 100, 20); + } + case LowPriority: { // Should complete within ~1000ms. 1200ms max. - return msToExpirationTime(ceiling(1000, 200)); + return bucket(currentTime, 1000, 200); + } case OffscreenPriority: return Never; default: From 7900fed478fc4aaec6dfcf267fb859b47b2553db Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Aug 2017 14:44:54 -0700 Subject: [PATCH 06/24] Add unit tests for expiration and coalescing --- src/renderers/noop/ReactNoopEntry.js | 13 +- .../fiber/__tests__/ReactExpiration-test.js | 125 ++++++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/renderers/shared/fiber/__tests__/ReactExpiration-test.js diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index afd4eac99d19e..6a5ed0b11ed32 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -83,6 +83,8 @@ function removeChild( parentInstance.children.splice(index, 1); } +let elapsedTimeInMs = 0; + var NoopRenderer = ReactFiberReconciler({ getRootHostContext() { if (failInBeginPhase) { @@ -203,8 +205,7 @@ var NoopRenderer = ReactFiberReconciler({ resetAfterCommit(): void {}, now(): number { - // TODO: Add an API to advance time. - return 0; + return elapsedTimeInMs; }, }); @@ -341,6 +342,14 @@ var ReactNoop = { expect(actual).toEqual(expected); }, + expire(ms: number): void { + elapsedTimeInMs += ms; + }, + + flushExpired(): Array { + return ReactNoop.flushUnitsOfWork(0); + }, + yield(value: mixed) { if (yieldedValues === null) { yieldedValues = [value]; diff --git a/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js b/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js new file mode 100644 index 0000000000000..a8c35a1e05014 --- /dev/null +++ b/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +var React; +var ReactNoop; + +describe('ReactExpiration', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactNoop = require('react-noop-renderer'); + }); + + function span(prop) { + return {type: 'span', children: [], prop}; + } + + it('increases priority of updates as time progresses', () => { + ReactNoop.render(); + + expect(ReactNoop.getChildren()).toEqual([]); + + // Nothing has expired yet because time hasn't advanced. + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance by 300ms, not enough to expire the low pri update. + ReactNoop.expire(300); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance by another second. Now the update should expire and flush. + ReactNoop.expire(1000); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span('done')]); + }); + + it('coalesces updates to the same component', () => { + const foos = []; + class Foo extends React.Component { + constructor() { + super(); + this.state = {step: 0}; + foos.push(this); + } + render() { + return ; + } + } + + ReactNoop.render([, ]); + ReactNoop.flush(); + const [a, b] = foos; + + a.setState({step: 1}); + + // Advance time by 500ms. + ReactNoop.expire(500); + + // Update A again. This update should coalesce with the previous update. + a.setState({step: 2}); + // Update B. This is the first update, so it has nothing to coalesce with. + b.setState({step: 1}); + + // Advance time. This should be enough to flush both updates to A, but not + // the update to B. If only the first update to A flushes, but not the + // second, then it wasn't coalesced properly. + ReactNoop.expire(500); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span(2), span(0)]); + + // Now expire the update to B. + ReactNoop.expire(500); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span(2), span(1)]); + }); + + it('stops coalescing after a certain threshold', () => { + let instance; + class Foo extends React.Component { + state = {step: 0}; + render() { + instance = this; + return ; + } + } + + ReactNoop.render(); + ReactNoop.flush(); + + instance.setState({step: 1}); + + // Advance time by 500 ms. + ReactNoop.expire(500); + + // Update again. This update should coalesce with the previous update. + instance.setState({step: 2}); + + // Advance time by 480ms. Not enough to expire the updates. + ReactNoop.expire(480); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span(0)]); + + // Update again. This update should NOT be coalesced, because the + // previous updates have almost expired. + instance.setState({step: 3}); + + // Advance time. This should expire the first two updates, + // but not the third. + ReactNoop.expire(500); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span(2)]); + + // Now expire the remaining update. + ReactNoop.expire(1000); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span(3)]); + }); +}); From a0df0b6497fd2f91ff8d816144b2272dbd23c5d4 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Aug 2017 15:36:54 -0700 Subject: [PATCH 07/24] Delete unnecessary abstraction --- .../shared/fiber/ReactFiberExpirationTime.js | 8 -------- src/renderers/shared/fiber/ReactFiberScheduler.js | 11 ++++++----- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js index 851316454e5fa..789eeb89ab4e4 100644 --- a/src/renderers/shared/fiber/ReactFiberExpirationTime.js +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -121,11 +121,3 @@ function expirationTimeToPriorityLevel( return LowPriority; } exports.expirationTimeToPriorityLevel = expirationTimeToPriorityLevel; - -function earlierExpirationTime( - t1: ExpirationTime, - t2: ExpirationTime, -): ExpirationTime { - return t1 !== Done && (t2 === Done || t2 > t1) ? t1 : t2; -} -exports.earlierExpirationTime = earlierExpirationTime; diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index f7506449e9424..e45d0776e1ce4 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -67,7 +67,6 @@ var { Done, Never, msToExpirationTime, - earlierExpirationTime, priorityToExpirationTime, expirationTimeToPriorityLevel, } = require('ReactFiberExpirationTime'); @@ -614,10 +613,12 @@ module.exports = function( // Bubble up the earliest expiration time. let child = workInProgress.child; while (child !== null) { - newExpirationTime = earlierExpirationTime( - newExpirationTime, - child.expirationTime, - ); + if ( + child.expirationTime !== Done && + (newExpirationTime === Done || newExpirationTime > child.expirationTime) + ) { + newExpirationTime = child.expirationTime; + } child = child.sibling; } workInProgress.expirationTime = newExpirationTime; From 4cd7cbe87e344f62c5b2fc23018619638c46d04a Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Aug 2017 16:16:25 -0700 Subject: [PATCH 08/24] Move performance.now polyfill to ReactDOMFrameScheduling --- src/renderers/art/ReactARTFiberEntry.js | 5 +-- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 18 +------- .../shared/ReactDOMFrameScheduling.js | 45 +++++++++++++------ 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/renderers/art/ReactARTFiberEntry.js b/src/renderers/art/ReactARTFiberEntry.js index ea0c4d68649e6..ca97c71e8d7e5 100644 --- a/src/renderers/art/ReactARTFiberEntry.js +++ b/src/renderers/art/ReactARTFiberEntry.js @@ -532,10 +532,7 @@ const ARTRenderer = ReactFiberReconciler({ ); }, - now(): number { - // TODO: Enable expiration by implementing this method. - return 0; - }, + now: ReactDOMFrameScheduling.now, useSyncScheduling: true, }); diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 8250d1773b43c..01090873ad72b 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -162,22 +162,6 @@ function shouldAutoFocusHostComponent(type: string, props: Props): boolean { return false; } -// TODO: Better polyfill -let now; -if ( - typeof window !== 'undefined' && - window.performance && - typeof window.performance.now === 'function' -) { - now = function() { - return performance.now(); - }; -} else { - now = function() { - return Date.now(); - }; -} - var DOMRenderer = ReactFiberReconciler({ getRootHostContext(rootContainerInstance: Container): HostContext { let type; @@ -453,7 +437,7 @@ var DOMRenderer = ReactFiberReconciler({ } }, - now, + now: ReactDOMFrameScheduling.now, canHydrateInstance( instance: Instance | TextInstance, diff --git a/src/renderers/shared/ReactDOMFrameScheduling.js b/src/renderers/shared/ReactDOMFrameScheduling.js index de8d0da98f379..5a2fc01ca989a 100644 --- a/src/renderers/shared/ReactDOMFrameScheduling.js +++ b/src/renderers/shared/ReactDOMFrameScheduling.js @@ -37,6 +37,20 @@ if (__DEV__) { } } +const hasNativePerformanceNow = + typeof performance === 'object' && typeof performance.now === 'function'; + +let now; +if (hasNativePerformanceNow) { + now = function() { + return performance.now(); + }; +} else { + now = function() { + return Date.now(); + }; +} + // TODO: There's no way to cancel, because Fiber doesn't atm. let rIC: (callback: (deadline: Deadline) => void) => number; @@ -67,19 +81,23 @@ if (!ExecutionEnvironment.canUseDOM) { var previousFrameTime = 33; var activeFrameTime = 33; - var frameDeadlineObject = { - timeRemaining: typeof performance === 'object' && - typeof performance.now === 'function' - ? function() { - // We assume that if we have a performance timer that the rAF callback - // gets a performance timer value. Not sure if this is always true. - return frameDeadline - performance.now(); - } - : function() { - // As a fallback we use Date.now. - return frameDeadline - Date.now(); - }, - }; + var frameDeadlineObject; + if (hasNativePerformanceNow) { + frameDeadlineObject = { + timeRemaining() { + // We assume that if we have a performance timer that the rAF callback + // gets a performance timer value. Not sure if this is always true. + return frameDeadline - performance.now(); + }, + }; + } else { + frameDeadlineObject = { + timeRemaining() { + // Fallback to Date.now() + return frameDeadline - Date.now(); + }, + }; + } // We use the postMessage trick to defer idle work until after the repaint. var messageKey = '__reactIdleCallback$' + Math.random().toString(36).slice(2); @@ -153,4 +171,5 @@ if (!ExecutionEnvironment.canUseDOM) { rIC = requestIdleCallback; } +exports.now = now; exports.rIC = rIC; From e26e7370ca303787cd38f9d1191bbc6df68e74cb Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Aug 2017 20:32:30 -0700 Subject: [PATCH 09/24] Add expiration to fuzz tester --- .../__tests__/ReactIncrementalTriangle-test.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js index fdeefaa3d975a..d5cdd22ae431a 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js @@ -54,6 +54,14 @@ describe('ReactIncrementalTriangle', () => { }; } + const EXPIRE = 'EXPIRE'; + function expire(ms) { + return { + type: EXPIRE, + ms, + }; + } + function TriangleSimulator() { let triangles = []; let leafTriangles = []; @@ -212,6 +220,9 @@ describe('ReactIncrementalTriangle', () => { targetTriangle.activate(); } break; + case EXPIRE: + ReactNoop.expire(action.ms); + break; default: break; } @@ -251,7 +262,7 @@ describe('ReactIncrementalTriangle', () => { } function randomAction() { - switch (randomInteger(0, 4)) { + switch (randomInteger(0, 5)) { case 0: return flush(randomInteger(0, totalTriangles * 1.5)); case 1: @@ -260,6 +271,8 @@ describe('ReactIncrementalTriangle', () => { return interrupt(); case 3: return toggle(randomInteger(0, totalChildren)); + case 4: + return expire(randomInteger(0, 1500)); default: throw new Error('Switch statement should be exhaustive'); } @@ -290,6 +303,9 @@ describe('ReactIncrementalTriangle', () => { case TOGGLE: result += `toggle(${action.childIndex})`; break; + case EXPIRE: + result += `expire(${action.ms})`; + break; default: throw new Error('Switch statement should be exhaustive'); } From 2bbd1c51b2ab102024507cca37b848671e4c1430 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sat, 7 Oct 2017 20:34:07 -0700 Subject: [PATCH 10/24] Expiration nits - Rename Done -> NoWork - Use max int32 instead of max safe int - Use bitwise operations instead of Math functions --- src/renderers/shared/fiber/ReactFiber.js | 8 ++-- .../shared/fiber/ReactFiberBeginWork.js | 7 ++- .../shared/fiber/ReactFiberExpirationTime.js | 19 ++++---- .../shared/fiber/ReactFiberScheduler.js | 43 ++++++++++--------- .../shared/fiber/ReactFiberUpdateQueue.js | 8 ++-- .../fiber/__tests__/ReactExpiration-test.js | 2 +- 6 files changed, 45 insertions(+), 42 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index faf40a2d6526c..8bbffcc48b6bc 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -36,9 +36,9 @@ var { Fragment, } = require('ReactTypeOfWork'); -var {NoWork} = require('ReactPriorityLevel'); +var {NoWork: NoWorkPriority} = require('ReactPriorityLevel'); -var {Done} = require('ReactFiberExpirationTime'); +var {NoWork} = require('ReactFiberExpirationTime'); var {NoContext} = require('ReactTypeOfInternalContext'); @@ -193,7 +193,7 @@ function FiberNode( this.firstEffect = null; this.lastEffect = null; - this.expirationTime = Done; + this.expirationTime = NoWork; this.alternate = null; @@ -457,5 +457,5 @@ exports.largerPriority = function( p1: PriorityLevel, p2: PriorityLevel, ): PriorityLevel { - return p1 !== NoWork && (p2 === NoWork || p2 > p1) ? p1 : p2; + return p1 !== NoWorkPriority && (p2 === NoWorkPriority || p2 > p1) ? p1 : p2; }; diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 5d49bf7e89dd7..0627bc7f9b354 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -48,7 +48,7 @@ var { YieldComponent, Fragment, } = ReactTypeOfWork; -var {Done, Never} = require('ReactFiberExpirationTime'); +var {NoWork, Never} = require('ReactFiberExpirationTime'); var { PerformedWork, Placement, @@ -330,7 +330,6 @@ module.exports = function( } function updateHostRoot(current, workInProgress, renderExpirationTime) { - const root = (workInProgress.stateNode: FiberRoot); pushHostRootContext(workInProgress); const updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { @@ -741,7 +740,7 @@ module.exports = function( renderExpirationTime: ExpirationTime, ): Fiber | null { if ( - workInProgress.expirationTime === Done || + workInProgress.expirationTime === NoWork || workInProgress.expirationTime > renderExpirationTime ) { return bailoutOnLowPriority(current, workInProgress); @@ -839,7 +838,7 @@ module.exports = function( } if ( - workInProgress.expirationTime === Done || + workInProgress.expirationTime === NoWork || workInProgress.expirationTime > renderExpirationTime ) { return bailoutOnLowPriority(current, workInProgress); diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js index 789eeb89ab4e4..c6270858aac5f 100644 --- a/src/renderers/shared/fiber/ReactFiberExpirationTime.js +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -12,7 +12,7 @@ import type {PriorityLevel} from 'ReactPriorityLevel'; const { - NoWork, + NoWork: NoWorkPriority, SynchronousPriority, TaskPriority, HighPriority, @@ -25,26 +25,27 @@ const invariant = require('fbjs/lib/invariant'); // TODO: Use an opaque type once ESLint et al support the syntax export type ExpirationTime = number; -const Done = 0; +const NoWork = 0; const Sync = 1; const Task = 2; -const Never = Number.MAX_SAFE_INTEGER; +const Never = Math.pow(2, 31) - 1; // Max int32 const UNIT_SIZE = 10; const MAGIC_NUMBER_OFFSET = 10; -exports.Done = Done; +exports.NoWork = NoWork; exports.Never = Never; // 1 unit of expiration time represents 10ms. function msToExpirationTime(ms: number): ExpirationTime { - // Always add an offset so that we don't clash with the magic number for Done. - return Math.round(ms / UNIT_SIZE) + MAGIC_NUMBER_OFFSET; + // Always add an offset so that we don't clash with the magic number for NoWork. + return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET; } exports.msToExpirationTime = msToExpirationTime; function ceiling(num: number, precision: number): number { - return Math.ceil(Math.ceil(num * precision) / precision); + // return Math.ceil(Math.ceil(num * precision) / precision); + return (((((num * precision) | 0) + 1) | 0) + 1) / precision; } function bucket( @@ -69,7 +70,7 @@ function priorityToExpirationTime( ): ExpirationTime { switch (priorityLevel) { case NoWork: - return Done; + return NoWorkPriority; case SynchronousPriority: return Sync; case TaskPriority: @@ -103,7 +104,7 @@ function expirationTimeToPriorityLevel( ): PriorityLevel { // First check for magic values switch (expirationTime) { - case Done: + case NoWorkPriority: return NoWork; case Sync: return SynchronousPriority; diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index e45d0776e1ce4..52e6ebee2627c 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -64,7 +64,6 @@ var { } = require('ReactPriorityLevel'); var { - Done, Never, msToExpirationTime, priorityToExpirationTime, @@ -200,7 +199,8 @@ module.exports = function( } = config; // Represents the current time in ms. - let mostRecentCurrentTime: ExpirationTime = msToExpirationTime(now()); + const startTime = now(); + let mostRecentCurrentTime: ExpirationTime = msToExpirationTime(0); // The priority level to use when scheduling an update. We use NoWork to // represent the default priority. @@ -222,7 +222,7 @@ module.exports = function( // The next work in progress fiber that we're currently working on. let nextUnitOfWork: Fiber | null = null; // The time at which we're currently rendering work. - let nextRenderExpirationTime: ExpirationTime = Done; + let nextRenderExpirationTime: ExpirationTime = NoWork; // The next fiber with an effect that we're currently committing. let nextEffect: Fiber | null = null; @@ -269,7 +269,7 @@ module.exports = function( // Clear out roots with no more work on them, or if they have uncaught errors while ( nextScheduledRoot !== null && - nextScheduledRoot.current.expirationTime === Done + nextScheduledRoot.current.expirationTime === NoWork ) { // Unschedule this root. nextScheduledRoot.isScheduled = false; @@ -281,7 +281,7 @@ module.exports = function( if (nextScheduledRoot === lastScheduledRoot) { nextScheduledRoot = null; lastScheduledRoot = null; - nextRenderExpirationTime = Done; + nextRenderExpirationTime = NoWork; return null; } // Continue with the next root. @@ -291,11 +291,11 @@ module.exports = function( let root = nextScheduledRoot; let earliestExpirationRoot = null; - let earliestExpirationTime = Done; + let earliestExpirationTime = NoWork; while (root !== null) { if ( - root.current.expirationTime !== Done && - (earliestExpirationTime === Done || + root.current.expirationTime !== NoWork && + (earliestExpirationTime === NoWork || earliestExpirationTime > root.current.expirationTime) ) { earliestExpirationTime = root.current.expirationTime; @@ -325,7 +325,7 @@ module.exports = function( return; } - nextRenderExpirationTime = Done; + nextRenderExpirationTime = NoWork; nextUnitOfWork = null; nextRenderedTree = null; return; @@ -614,8 +614,9 @@ module.exports = function( let child = workInProgress.child; while (child !== null) { if ( - child.expirationTime !== Done && - (newExpirationTime === Done || newExpirationTime > child.expirationTime) + child.expirationTime !== NoWork && + (newExpirationTime === NoWork || + newExpirationTime > child.expirationTime) ) { newExpirationTime = child.expirationTime; } @@ -803,7 +804,7 @@ module.exports = function( if ( capturedErrors !== null && capturedErrors.size > 0 && - nextRenderExpirationTime !== Done && + nextRenderExpirationTime !== NoWork && nextRenderExpirationTime <= mostRecentCurrentTime ) { while (nextUnitOfWork !== null) { @@ -824,7 +825,7 @@ module.exports = function( if ( capturedErrors === null || capturedErrors.size === 0 || - nextRenderExpirationTime === Done || + nextRenderExpirationTime === NoWork || nextRenderExpirationTime > mostRecentCurrentTime ) { // There are no more unhandled errors. We can exit this special @@ -850,7 +851,7 @@ module.exports = function( } if ( - nextRenderExpirationTime === Done || + nextRenderExpirationTime === NoWork || nextRenderExpirationTime > minExpirationTime ) { return; @@ -873,7 +874,7 @@ module.exports = function( handleCommitPhaseErrors(); // The render time may have changed. Check again. if ( - nextRenderExpirationTime === Done || + nextRenderExpirationTime === NoWork || nextRenderExpirationTime > minExpirationTime || nextRenderExpirationTime > mostRecentCurrentTime ) { @@ -905,7 +906,7 @@ module.exports = function( handleCommitPhaseErrors(); // The render time may have changed. Check again. if ( - nextRenderExpirationTime === Done || + nextRenderExpirationTime === NoWork || nextRenderExpirationTime > minExpirationTime || nextRenderExpirationTime <= mostRecentCurrentTime ) { @@ -1385,7 +1386,7 @@ module.exports = function( } function scheduleRoot(root: FiberRoot, expirationTime: ExpirationTime) { - if (expirationTime === Done) { + if (expirationTime === NoWork) { return; } @@ -1450,7 +1451,7 @@ module.exports = function( // rest of the path is correct. shouldContinue = false; if ( - node.expirationTime === Done || + node.expirationTime === NoWork || node.expirationTime > expirationTime ) { // Expiration time did not match. Update and keep going. @@ -1459,7 +1460,7 @@ module.exports = function( } if (node.alternate !== null) { if ( - node.alternate.expirationTime === Done || + node.alternate.expirationTime === NoWork || node.alternate.expirationTime > expirationTime ) { // Expiration time did not match. Update and keep going. @@ -1572,7 +1573,9 @@ module.exports = function( } function recalculateCurrentTime(): ExpirationTime { - mostRecentCurrentTime = msToExpirationTime(now()); + // Subtract initial time so it fits inside 32bits + const ms = now() - startTime; + mostRecentCurrentTime = msToExpirationTime(ms); return mostRecentCurrentTime; } diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index 03d1532e168b6..596ed9d41c985 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -16,7 +16,7 @@ import type {ExpirationTime} from 'ReactFiberExpirationTime'; const {Callback: CallbackEffect} = require('ReactTypeOfSideEffect'); -const {Done} = require('ReactFiberExpirationTime'); +const {NoWork} = require('ReactFiberExpirationTime'); const {ClassComponent, HostRoot} = require('ReactTypeOfWork'); @@ -364,12 +364,12 @@ exports.addForceUpdate = addForceUpdate; function getUpdateExpirationTime(fiber: Fiber): ExpirationTime { const updateQueue = fiber.updateQueue; if (updateQueue === null) { - return Done; + return NoWork; } if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) { - return Done; + return NoWork; } - return updateQueue.first !== null ? updateQueue.first.expirationTime : Done; + return updateQueue.first !== null ? updateQueue.first.expirationTime : NoWork; } exports.getUpdateExpirationTime = getUpdateExpirationTime; diff --git a/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js b/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js index a8c35a1e05014..7b9121f19a209 100644 --- a/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js @@ -71,7 +71,7 @@ describe('ReactExpiration', () => { // Advance time. This should be enough to flush both updates to A, but not // the update to B. If only the first update to A flushes, but not the // second, then it wasn't coalesced properly. - ReactNoop.expire(500); + ReactNoop.expire(600); ReactNoop.flushExpired(); expect(ReactNoop.getChildren()).toEqual([span(2), span(0)]); From 70b82f7dafd5f03c285093b299887421ff3f0548 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 18 Aug 2017 16:08:34 -0700 Subject: [PATCH 11/24] Initial version of blocking & top-level resuming Refactors the complete phase to add support for blocking a tree from committing. Also adds a basic version of "resuming" for HostRoots. Neither of these features are actually implemented in this commit; will come later. --- .../shared/fiber/ReactFiberBeginWork.js | 11 +++ src/renderers/shared/fiber/ReactFiberRoot.js | 15 ++++ .../shared/fiber/ReactFiberScheduler.js | 75 ++++++++++++++++--- 3 files changed, 92 insertions(+), 9 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 0627bc7f9b354..8a050db1446b0 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -330,7 +330,18 @@ module.exports = function( } function updateHostRoot(current, workInProgress, renderExpirationTime) { + const root = (workInProgress.stateNode: FiberRoot); pushHostRootContext(workInProgress); + if (root.completedAt === renderExpirationTime) { + // The root is already complete. Bail out and commit. + // TODO: This is a limited version of resuming that only applies to + // the root, to account for the pathological case where a completed + // root must be completely restarted before it can commit. Once we + // implement resuming for real, this special branch shouldn't + // be neccessary. + return null; + } + const updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { const prevState = workInProgress.memoizedState; diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js index f78205d2bcae5..a026174fd108d 100644 --- a/src/renderers/shared/fiber/ReactFiberRoot.js +++ b/src/renderers/shared/fiber/ReactFiberRoot.js @@ -11,8 +11,10 @@ 'use strict'; import type {Fiber} from 'ReactFiber'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; const {createHostRootFiber} = require('ReactFiber'); +const {NoWork} = require('ReactFiberExpirationTime'); export type FiberRoot = { // Any additional information from the host associated with this root. @@ -21,6 +23,8 @@ export type FiberRoot = { current: Fiber, // Determines if this root has already been added to the schedule for work. isScheduled: boolean, + // The time at which this root completed. + completedAt: ExpirationTime, // The work schedule is a linked list. nextScheduledRoot: FiberRoot | null, // Top context object, used by renderSubtreeIntoContainer @@ -28,6 +32,16 @@ export type FiberRoot = { pendingContext: Object | null, }; +// Indicates whether the root is blocked from committing at a particular +// expiration time. +exports.isRootBlocked = function( + root: FiberRoot, + time: ExpirationTime, +): boolean { + // TODO: Implementation + return false; +}; + exports.createFiberRoot = function(containerInfo: any): FiberRoot { // Cyclic construction. This cheats the type system right now because // stateNode is any. @@ -36,6 +50,7 @@ exports.createFiberRoot = function(containerInfo: any): FiberRoot { current: uninitializedFiber, containerInfo: containerInfo, isScheduled: false, + completedAt: NoWork, nextScheduledRoot: null, context: null, pendingContext: null, diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 52e6ebee2627c..097a781065cec 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -52,6 +52,7 @@ var {ReactCurrentOwner} = require('ReactGlobalSharedState'); var getComponentName = require('getComponentName'); var {createWorkInProgress} = require('ReactFiber'); +var {isRootBlocked} = require('ReactFiberRoot'); var {onCommitRoot} = require('ReactFiberDevToolsHook'); var { @@ -266,10 +267,11 @@ module.exports = function( } function resetNextUnitOfWork() { - // Clear out roots with no more work on them, or if they have uncaught errors + // Clear out roots with no more work on them while ( nextScheduledRoot !== null && - nextScheduledRoot.current.expirationTime === NoWork + nextScheduledRoot.current.expirationTime === NoWork && + nextScheduledRoot.completedAt === NoWork ) { // Unschedule this root. nextScheduledRoot.isScheduled = false; @@ -293,10 +295,11 @@ module.exports = function( let earliestExpirationRoot = null; let earliestExpirationTime = NoWork; while (root !== null) { + let rootExpirationTime = shouldWorkOnRoot(root); if ( - root.current.expirationTime !== NoWork && + rootExpirationTime !== NoWork && (earliestExpirationTime === NoWork || - earliestExpirationTime > root.current.expirationTime) + earliestExpirationTime > rootExpirationTime) ) { earliestExpirationTime = root.current.expirationTime; earliestExpirationRoot = root; @@ -313,10 +316,26 @@ module.exports = function( // unfortunately this is it. resetContextStack(); - nextUnitOfWork = createWorkInProgress( - earliestExpirationRoot.current, - earliestExpirationTime, - ); + if (earliestExpirationRoot.completedAt === nextRenderExpirationTime) { + // If the root is already complete, reuse the existing work-in-progress. + // TODO: This is a limited version of resuming that only applies to + // the root, to account for the pathological case where a completed + // root must be completely restarted before it can commit. Once we + // implement resuming for real, this special branch shouldn't + // be neccessary. + nextUnitOfWork = earliestExpirationRoot.current.alternate; + invariant( + nextUnitOfWork !== null, + 'Expected a completed root to have a work-in-progress. This error ' + + 'is likely caused by a bug in React. Please file an issue.', + ); + } else { + earliestExpirationRoot.completedAt = NoWork; + nextUnitOfWork = createWorkInProgress( + earliestExpirationRoot.current, + earliestExpirationTime, + ); + } if (earliestExpirationRoot !== nextRenderedTree) { // We've switched trees. Reset the nested update counter. nestedUpdateCount = 0; @@ -331,6 +350,37 @@ module.exports = function( return; } + // Indicates whether the root should be worked on. Not the same as whether a + // root has work, because work could be blocked. + function shouldWorkOnRoot(root: FiberRoot): ExpirationTime { + const completedAt = root.completedAt; + const expirationTime = root.current.expirationTime; + + if (expirationTime === NoWork) { + // There's no work in this tree. + return NoWork; + } + + if (completedAt !== NoWork) { + // The root completed but was blocked from committing. + + if (expirationTime < completedAt) { + // We have work that expires earlier than the completed root. Regardless + // of whether the root is blocked, we should work on it. + return expirationTime; + } + + // There have been no higher priority updates since we completed the root. + // If it's still blocked, return NoWork, as if it has no more work. If it's + // no longer blocked, return the time at which it completed so that we + // can commit it. + const isBlocked = isRootBlocked(root, expirationTime); + return isBlocked ? NoWork : completedAt; + } + + return expirationTime; + } + function commitAllHostEffects() { while (nextEffect !== null) { if (__DEV__) { @@ -456,6 +506,8 @@ module.exports = function( 'in React. Please file an issue.', ); + root.completedAt = NoWork; + if (nextRenderExpirationTime <= mostRecentCurrentTime) { // Keep track of the number of iterations to prevent an infinite // update loop. @@ -712,7 +764,12 @@ module.exports = function( // We've reached the root. Mark the root as pending commit. Depending // on how much time we have left, we'll either commit it now or in // the next frame. - pendingCommit = workInProgress; + const root = workInProgress.stateNode; + if (isRootBlocked(root, nextRenderExpirationTime)) { + root.completedAt = workInProgress.expirationTime = nextRenderExpirationTime; + } else { + pendingCommit = workInProgress; + } return null; } } From 3d5c12b4c2bb5d25d67f81909139f6c230d28f07 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 18 Aug 2017 18:32:43 -0700 Subject: [PATCH 12/24] Change DOM renderer Container type to a union of String | DOMContainer We can perform work in the render and complete phases even before we have access to the DOM container. We only need the namespace. Once we get to the commit phase, throw if we don't have a DOM container. --- .../dom/fiber/ReactDOMFiberComponent.js | 59 +++------ src/renderers/dom/fiber/ReactDOMFiberEntry.js | 125 ++++++++++++------ .../dom/fiber/__tests__/ReactDOMFiber-test.js | 2 +- 3 files changed, 107 insertions(+), 79 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberComponent.js b/src/renderers/dom/fiber/ReactDOMFiberComponent.js index 07a84c242ad28..f415d1ffabe2c 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberComponent.js +++ b/src/renderers/dom/fiber/ReactDOMFiberComponent.js @@ -21,7 +21,6 @@ var ReactDOMFiberOption = require('ReactDOMFiberOption'); var ReactDOMFiberSelect = require('ReactDOMFiberSelect'); var ReactDOMFiberTextarea = require('ReactDOMFiberTextarea'); var {getCurrentFiberOwnerName} = require('ReactDebugCurrentFiber'); -var {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE} = require('HTMLNodeType'); var assertValidProps = require('assertValidProps'); var emptyFunction = require('fbjs/lib/emptyFunction'); @@ -183,24 +182,6 @@ if (__DEV__) { }; } -function ensureListeningTo(rootContainerElement, registrationName) { - var isDocumentOrFragment = - rootContainerElement.nodeType === DOCUMENT_NODE || - rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE; - var doc = isDocumentOrFragment - ? rootContainerElement - : rootContainerElement.ownerDocument; - listenTo(registrationName, doc); -} - -function getOwnerDocumentFromRootContainer( - rootContainerElement: Element | Document, -): Document { - return rootContainerElement.nodeType === DOCUMENT_NODE - ? (rootContainerElement: any) - : rootContainerElement.ownerDocument; -} - // There are so many media events, it makes sense to just // maintain a list rather than create a `trapBubbledEvent` for each var mediaEvents = { @@ -244,7 +225,7 @@ function trapClickOnNonInteractiveElement(node: HTMLElement) { function setInitialDOMProperties( domElement: Element, - rootContainerElement: Element | Document, + ownerDocument: Document, nextProps: Object, isCustomComponentTag: boolean, ): void { @@ -284,7 +265,7 @@ function setInitialDOMProperties( if (__DEV__ && typeof nextProp !== 'function') { warnForInvalidEventListener(propKey, nextProp); } - ensureListeningTo(rootContainerElement, propKey); + listenTo(propKey, ownerDocument); } } else if (isCustomComponentTag) { DOMPropertyOperations.setValueForAttribute(domElement, propKey, nextProp); @@ -338,14 +319,12 @@ var ReactDOMFiberComponent = { createElement( type: *, props: Object, - rootContainerElement: Element | Document, + ownerDocument: Document, parentNamespace: string, ): Element { // We create tags in the namespace of their parent container, except HTML // tags get no namespace. - var ownerDocument: Document = getOwnerDocumentFromRootContainer( - rootContainerElement, - ); + var domElement: Element; var namespaceURI = parentNamespace; if (namespaceURI === HTML_NAMESPACE) { @@ -408,17 +387,15 @@ var ReactDOMFiberComponent = { return domElement; }, - createTextNode(text: string, rootContainerElement: Element | Document): Text { - return getOwnerDocumentFromRootContainer( - rootContainerElement, - ).createTextNode(text); + createTextNode(text: string, ownerDocument: Document): Text { + return ownerDocument.createTextNode(text); }, setInitialProperties( domElement: Element, tag: string, rawProps: Object, - rootContainerElement: Element | Document, + ownerDocument: Document, ): void { var isCustomComponentTag = isCustomComponent(tag, rawProps); if (__DEV__) { @@ -513,7 +490,7 @@ var ReactDOMFiberComponent = { ); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. - ensureListeningTo(rootContainerElement, 'onChange'); + listenTo('onChange', ownerDocument); break; case 'option': ReactDOMFiberOption.validateProps(domElement, rawProps); @@ -529,7 +506,7 @@ var ReactDOMFiberComponent = { ); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. - ensureListeningTo(rootContainerElement, 'onChange'); + listenTo('onChange', ownerDocument); break; case 'textarea': ReactDOMFiberTextarea.initWrapperState(domElement, rawProps); @@ -541,7 +518,7 @@ var ReactDOMFiberComponent = { ); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. - ensureListeningTo(rootContainerElement, 'onChange'); + listenTo('onChange', ownerDocument); break; default: props = rawProps; @@ -551,7 +528,7 @@ var ReactDOMFiberComponent = { setInitialDOMProperties( domElement, - rootContainerElement, + ownerDocument, props, isCustomComponentTag, ); @@ -590,7 +567,7 @@ var ReactDOMFiberComponent = { tag: string, lastRawProps: Object, nextRawProps: Object, - rootContainerElement: Element | Document, + ownerDocument: Document, ): null | Array { if (__DEV__) { validatePropertiesInDevelopment(tag, nextRawProps); @@ -768,7 +745,7 @@ var ReactDOMFiberComponent = { if (__DEV__ && typeof nextProp !== 'function') { warnForInvalidEventListener(propKey, nextProp); } - ensureListeningTo(rootContainerElement, propKey); + listenTo(propKey, ownerDocument); } if (!updatePayload && lastProp !== nextProp) { // This is a special case. If any listener updates we need to ensure @@ -835,7 +812,7 @@ var ReactDOMFiberComponent = { tag: string, rawProps: Object, parentNamespace: string, - rootContainerElement: Element | Document, + ownerDocument: Document, ): null | Array { if (__DEV__) { var suppressHydrationWarning = @@ -924,7 +901,7 @@ var ReactDOMFiberComponent = { ); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. - ensureListeningTo(rootContainerElement, 'onChange'); + listenTo('onChange', ownerDocument); break; case 'option': ReactDOMFiberOption.validateProps(domElement, rawProps); @@ -938,7 +915,7 @@ var ReactDOMFiberComponent = { ); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. - ensureListeningTo(rootContainerElement, 'onChange'); + listenTo('onChange', ownerDocument); break; case 'textarea': ReactDOMFiberTextarea.initWrapperState(domElement, rawProps); @@ -949,7 +926,7 @@ var ReactDOMFiberComponent = { ); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. - ensureListeningTo(rootContainerElement, 'onChange'); + listenTo('onChange', ownerDocument); break; } @@ -1016,7 +993,7 @@ var ReactDOMFiberComponent = { if (__DEV__ && typeof nextProp !== 'function') { warnForInvalidEventListener(propKey, nextProp); } - ensureListeningTo(rootContainerElement, propKey); + listenTo(propKey, ownerDocument); } } else if (__DEV__) { // Validate that the properties correspond to their expected values. diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 01090873ad72b..75403b44b4124 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -96,7 +96,12 @@ type DOMContainer = _reactRootContainer: ?Object, }); -type Container = Element | Document; +type Container = + | Element + | Document + // If the DOM container is lazily provided, the container is the namespace uri + | string; + type Props = { autoFocus?: boolean, children?: mixed, @@ -116,6 +121,29 @@ type HostContext = HostContextDev | HostContextProd; let eventsEnabled: ?boolean = null; let selectionInformation: ?mixed = null; +function getOwnerDocument(container: Container): Document { + let ownerDocument; + if (typeof container === 'string') { + ownerDocument = document; + } else if (container.nodeType === DOCUMENT_NODE) { + ownerDocument = (container: any); + } else { + ownerDocument = container.ownerDocument; + } + return ownerDocument; +} + +function ensureDOMContainer(container: Container): Element | Document { + invariant( + typeof container !== 'string', + // TODO: Better error message. Probably should have errored already, when + // validating the result of getContainer. + 'Container should have resolved by now', + ); + const domContainer: Element | Document = (container: any); + return domContainer; +} + /** * True if the supplied DOM node is a valid node element. * @@ -166,23 +194,40 @@ var DOMRenderer = ReactFiberReconciler({ getRootHostContext(rootContainerInstance: Container): HostContext { let type; let namespace; - const nodeType = rootContainerInstance.nodeType; - switch (nodeType) { - case DOCUMENT_NODE: - case DOCUMENT_FRAGMENT_NODE: { - type = nodeType === DOCUMENT_NODE ? '#document' : '#fragment'; - let root = (rootContainerInstance: any).documentElement; - namespace = root ? root.namespaceURI : getChildNamespace(null, ''); - break; + if (typeof rootContainerInstance === 'string') { + namespace = rootContainerInstance; + if (__DEV__) { + return {namespace, ancestorInfo: null}; } - default: { - const container: any = nodeType === COMMENT_NODE - ? rootContainerInstance.parentNode - : rootContainerInstance; - const ownNamespace = container.namespaceURI || null; - type = container.tagName; - namespace = getChildNamespace(ownNamespace, type); - break; + return namespace; + } else { + switch ((rootContainerInstance: any).nodeType) { + case DOCUMENT_NODE: { + type = '#document'; + const root = (rootContainerInstance: any).documentElement; + namespace = root ? root.namespaceURI : getChildNamespace(null, ''); + break; + } + case DOCUMENT_FRAGMENT_NODE: { + type = '#fragment'; + const root = (rootContainerInstance: any).documentElement; + namespace = root ? root.namespaceURI : getChildNamespace(null, ''); + break; + } + case COMMENT_NODE: { + const container = (rootContainerInstance: any).parentNode; + const ownNamespace = container.namespaceURI || null; + type = container.tagName; + namespace = getChildNamespace(ownNamespace, type); + break; + } + default: { + const container = (rootContainerInstance: any); + const ownNamespace = container.namespaceURI || null; + type = container.tagName; + namespace = getChildNamespace(ownNamespace, type); + break; + } } } if (__DEV__) { @@ -259,7 +304,7 @@ var DOMRenderer = ReactFiberReconciler({ const domElement: Instance = createElement( type, props, - rootContainerInstance, + getOwnerDocument(rootContainerInstance), parentNamespace, ); precacheFiberNode(internalInstanceHandle, domElement); @@ -280,7 +325,8 @@ var DOMRenderer = ReactFiberReconciler({ props: Props, rootContainerInstance: Container, ): boolean { - setInitialProperties(domElement, type, props, rootContainerInstance); + const ownerDocument = getOwnerDocument(rootContainerInstance); + setInitialProperties(domElement, type, props, ownerDocument); return shouldAutoFocusHostComponent(type, props); }, @@ -308,13 +354,8 @@ var DOMRenderer = ReactFiberReconciler({ validateDOMNesting(null, string, ownAncestorInfo); } } - return diffProperties( - domElement, - type, - oldProps, - newProps, - rootContainerInstance, - ); + const ownerDocument = getOwnerDocument(rootContainerInstance); + return diffProperties(domElement, type, oldProps, newProps, ownerDocument); }, commitMount( @@ -374,7 +415,8 @@ var DOMRenderer = ReactFiberReconciler({ const hostContextDev = ((hostContext: any): HostContextDev); validateDOMNesting(null, text, hostContextDev.ancestorInfo); } - var textNode: TextInstance = createTextNode(text, rootContainerInstance); + const ownerDocument = getOwnerDocument(rootContainerInstance); + const textNode: TextInstance = createTextNode(text, ownerDocument); precacheFiberNode(internalInstanceHandle, textNode); return textNode; }, @@ -395,10 +437,11 @@ var DOMRenderer = ReactFiberReconciler({ container: Container, child: Instance | TextInstance, ): void { - if (container.nodeType === COMMENT_NODE) { - (container.parentNode: any).insertBefore(child, container); + const domContainer = ensureDOMContainer(container); + if (domContainer.nodeType === COMMENT_NODE) { + (domContainer.parentNode: any).insertBefore(child, container); } else { - container.appendChild(child); + domContainer.appendChild(child); } }, @@ -415,10 +458,11 @@ var DOMRenderer = ReactFiberReconciler({ child: Instance | TextInstance, beforeChild: Instance | TextInstance, ): void { - if (container.nodeType === COMMENT_NODE) { - (container.parentNode: any).insertBefore(child, beforeChild); + const domContainer = ensureDOMContainer(container); + if (domContainer.nodeType === COMMENT_NODE) { + (domContainer.parentNode: any).insertBefore(child, beforeChild); } else { - container.insertBefore(child, beforeChild); + domContainer.insertBefore(child, beforeChild); } }, @@ -430,10 +474,11 @@ var DOMRenderer = ReactFiberReconciler({ container: Container, child: Instance | TextInstance, ): void { - if (container.nodeType === COMMENT_NODE) { - (container.parentNode: any).removeChild(child); + const domContainer = ensureDOMContainer(container); + if (domContainer.nodeType === COMMENT_NODE) { + (domContainer.parentNode: any).removeChild(child); } else { - container.removeChild(child); + domContainer.removeChild(child); } }, @@ -479,6 +524,7 @@ var DOMRenderer = ReactFiberReconciler({ getFirstHydratableChild( parentInstance: Container | Instance, ): null | Instance | TextInstance { + parentInstance = ensureDOMContainer(parentInstance); let next = parentInstance.firstChild; // Skip non-hydratable nodes. while ( @@ -510,12 +556,13 @@ var DOMRenderer = ReactFiberReconciler({ } else { parentNamespace = ((hostContext: any): HostContextProd); } + const ownerDocument = getOwnerDocument(rootContainerInstance); return diffHydratedProperties( instance, type, props, parentNamespace, - rootContainerInstance, + ownerDocument, ); }, @@ -534,6 +581,7 @@ var DOMRenderer = ReactFiberReconciler({ text: string, ) { if (__DEV__) { + parentContainer = ensureDOMContainer(parentContainer); warnForUnmatchedText(textInstance, text); } }, @@ -555,6 +603,7 @@ var DOMRenderer = ReactFiberReconciler({ instance: Instance | TextInstance, ) { if (__DEV__) { + parentContainer = ensureDOMContainer(parentContainer); if (instance.nodeType === 1) { warnForDeletedHydratableElement(parentContainer, (instance: any)); } else { @@ -584,6 +633,7 @@ var DOMRenderer = ReactFiberReconciler({ props: Props, ) { if (__DEV__) { + parentContainer = ensureDOMContainer(parentContainer); warnForInsertedHydratedElement(parentContainer, type, props); } }, @@ -593,6 +643,7 @@ var DOMRenderer = ReactFiberReconciler({ text: string, ) { if (__DEV__) { + parentContainer = ensureDOMContainer(parentContainer); warnForInsertedHydratedText(parentContainer, text); } }, diff --git a/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js b/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js index 36f63a3b4f095..ba1aab836ae5b 100644 --- a/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js +++ b/src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js @@ -1142,7 +1142,7 @@ describe('ReactDOMFiber', () => { expect(iframeContainer.appendChild).toHaveBeenCalledTimes(1); }); - it('should mount into a document fragment', () => { + fit('should mount into a document fragment', () => { var fragment = document.createDocumentFragment(); ReactDOM.render(
foo
, fragment); expect(container.innerHTML).toBe(''); From 9154198c3b188f6c4db2d9094d57f397bd3f2154 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 25 Aug 2017 16:08:04 -0700 Subject: [PATCH 13/24] Split UpdateQueue functions to make it more generic We'll use an UpdateQueue to store top-level completion callbacks. --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 58 +++++ .../__tests__/ReactDOMAsyncRoot-test.js | 73 ++++++ .../shared/fiber/ReactFiberClassComponent.js | 48 ++-- .../shared/fiber/ReactFiberCommitWork.js | 34 ++- .../shared/fiber/ReactFiberReconciler.js | 114 ++++++++- src/renderers/shared/fiber/ReactFiberRoot.js | 5 + .../shared/fiber/ReactFiberUpdateQueue.js | 222 ++++++------------ 7 files changed, 363 insertions(+), 191 deletions(-) create mode 100644 src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 75403b44b4124..df33ae62b77db 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -10,6 +10,7 @@ 'use strict'; +import type {Work} from 'ReactFiberReconciler'; import type {ReactNodeList} from 'ReactTypes'; require('checkReact'); @@ -798,7 +799,64 @@ function createPortal( return ReactPortal.createPortal(children, container, null, key); } +type PublicRoot = { + render(children: ReactNodeList, callback: () => mixed): Work, + prerender(children: ReactNodeList, callback: () => mixed): Work, + unmount(callback: () => mixed): Work, + + _reactRootContainer: *, + _getComponent: () => DOMContainer, +}; + +function PublicRootNode( + container: DOMContainer | (() => DOMContainer), + namespace: ?string, +) { + if (typeof container === 'function') { + if (typeof namespace !== 'string') { + // Default to HTML namespace + namespace = DOMNamespaces.html; + } + this._reactRootContainer = DOMRenderer.createContainer(namespace); + this._getComponent = container; + } else { + // Assume this is a DOM container + const domContainer: DOMContainer = (container: any); + this._reactRootContainer = DOMRenderer.createContainer(domContainer); + this._getComponent = function() { + return domContainer; + }; + } +} +PublicRootNode.prototype.render = function( + children: ReactNodeList, + callback: () => mixed, +): Work { + return DOMRenderer.updateContainer( + children, + this._reactRootContainer, + null, + callback, + ); +}; +PublicRootNode.prototype.prerender = function() {}; +PublicRootNode.prototype.unmount = function() { + return DOMRenderer.updateContainer( + null, + this._reactRootContainer, + null, + null, + ); +}; + var ReactDOMFiber = { + unstable_create( + container: DOMContainer | (() => DOMContainer), + namespace: ?string, + ): PublicRoot { + return new PublicRootNode(container, namespace); + }, + createPortal, findDOMNode( diff --git a/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js b/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js new file mode 100644 index 0000000000000..7398e0740ceb0 --- /dev/null +++ b/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +let React; +let ReactDOM; +let ReactFeatureFlags; + +describe('ReactDOMAsyncMount', () => { + beforeEach(() => { + jest.resetModules(); + + React = require('react'); + ReactDOM = require('react-dom'); + ReactFeatureFlags = require('ReactFeatureFlags'); + ReactFeatureFlags.enableAsyncSubtreeAPI = true; + }); + + it('works in easy mode', () => { + const container = document.createElement('div'); + const root = ReactDOM.unstable_create(container); + root.render(
Foo
); + expect(container.textContent).toEqual('Foo'); + root.render(
Bar
); + expect(container.textContent).toEqual('Bar'); + root.unmount(); + expect(container.textContent).toEqual(''); + }); + + it('can pass callback to render', () => { + const container = document.createElement('div'); + const root = ReactDOM.unstable_create(container); + let called = false; + root.render(
Foo
, () => { + called = true; + }); + expect(container.textContent).toEqual('Foo'); + expect(called).toBe(true); + }); + + it('can await result of render method', async () => { + const container = document.createElement('div'); + const root = ReactDOM.unstable_create(container); + await root.render(
Foo
); + expect(container.textContent).toEqual('Foo'); + }); + + it('can defer commit using prerender', async () => { + const Async = React.unstable_AsyncComponent; + const container = document.createElement('div'); + const root = ReactDOM.unstable_create(container); + const work = root.prerender(Foo); + + // Hasn't updated yet + expect(container.textContent).toEqual(''); + + await work; + + // Tree has completed, but still hasn't updated yet + expect(container.textContent).toEqual(''); + + // Synchronsouly update DOM + work.commit(); + expect(container.textContent).toEqual('Foo'); + }); +}); diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 62f35742b0256..864034d51e5a2 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -26,9 +26,7 @@ var { isContextConsumer, } = require('ReactFiberContext'); var { - addUpdate, - addReplaceUpdate, - addForceUpdate, + insertUpdateIntoFiber, beginUpdateQueue, } = require('ReactFiberUpdateQueue'); var {hasContextChanged} = require('ReactFiberContext'); @@ -106,14 +104,17 @@ module.exports = function( if (__DEV__) { warnOnInvalidCallback(callback, 'setState'); } - addUpdate( - fiber, - partialState, - callback, + const update = { priorityLevel, expirationTime, - currentTime, - ); + partialState, + callback, + isReplace: false, + isForced: false, + isTopLevelUnmount: false, + next: null, + }; + insertUpdateIntoFiber(fiber, update, currentTime); scheduleUpdate(fiber, expirationTime); }, enqueueReplaceState(instance, state, callback) { @@ -128,14 +129,17 @@ module.exports = function( if (__DEV__) { warnOnInvalidCallback(callback, 'replaceState'); } - addReplaceUpdate( - fiber, - state, - callback, + const update = { priorityLevel, expirationTime, - currentTime, - ); + partialState: state, + callback, + isReplace: true, + isForced: false, + isTopLevelUnmount: false, + next: null, + }; + insertUpdateIntoFiber(fiber, update, currentTime); scheduleUpdate(fiber, expirationTime); }, enqueueForceUpdate(instance, callback) { @@ -150,13 +154,17 @@ module.exports = function( if (__DEV__) { warnOnInvalidCallback(callback, 'forceUpdate'); } - addForceUpdate( - fiber, - callback, + const update = { priorityLevel, expirationTime, - currentTime, - ); + partialState: null, + callback, + isReplace: false, + isForced: true, + isTopLevelUnmount: false, + next: null, + }; + insertUpdateIntoFiber(fiber, update, currentTime); scheduleUpdate(fiber, expirationTime); }, }; diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 98a8f3def9d1a..de52a071c6caa 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -22,7 +22,6 @@ var { HostPortal, CoroutineComponent, } = ReactTypeOfWork; -var {commitCallbacks} = require('ReactFiberUpdateQueue'); var {onCommitUnmount} = require('ReactFiberDevToolsHook'); var { invokeGuardedCallback, @@ -488,6 +487,19 @@ module.exports = function( } } + function commitCallbacks(callbackList, context) { + for (let i = 0; i < callbackList.length; i++) { + const callback = callbackList[i]; + invariant( + typeof callback === 'function', + 'Invalid argument passed as callback. Expected a function. Instead ' + + 'received: %s', + callback, + ); + callback.call(context); + } + } + function commitLifeCycles(current: Fiber | null, finishedWork: Fiber): void { switch (finishedWork.tag) { case ClassComponent: { @@ -521,15 +533,27 @@ module.exports = function( finishedWork.effectTag & Callback && finishedWork.updateQueue !== null ) { - commitCallbacks(finishedWork, finishedWork.updateQueue, instance); + const updateQueue = finishedWork.updateQueue; + if (updateQueue.callbackList !== null) { + // Set the list to null to make sure they don't get called more than once. + const callbackList = updateQueue.callbackList; + updateQueue.callbackList = null; + commitCallbacks(callbackList, instance); + } } return; } case HostRoot: { const updateQueue = finishedWork.updateQueue; - if (updateQueue !== null) { - const instance = finishedWork.child && finishedWork.child.stateNode; - commitCallbacks(finishedWork, updateQueue, instance); + if (updateQueue !== null && updateQueue.callbackList !== null) { + // Set the list to null to make sure they don't get called more + // than once. + const callbackList = updateQueue.callbackList; + updateQueue.callbackList = null; + const instance = finishedWork.child !== null + ? finishedWork.child.stateNode + : null; + commitCallbacks(callbackList, instance); } return; } diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index c672fe86a5c55..9c55388d749fd 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -12,11 +12,16 @@ import type {Fiber} from 'ReactFiber'; import type {FiberRoot} from 'ReactFiberRoot'; +import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; import type {ReactNodeList} from 'ReactTypes'; var ReactFeatureFlags = require('ReactFeatureFlags'); -var {addTopLevelUpdate} = require('ReactFiberUpdateQueue'); +var { + insertUpdateIntoFiber, + createUpdateQueue, +} = require('ReactFiberUpdateQueue'); var { findCurrentUnmaskedContext, @@ -49,6 +54,17 @@ export type Deadline = { type OpaqueHandle = Fiber; type OpaqueRoot = FiberRoot; +type Awaitable = { + then(resolve: (result: T) => mixed): void, +}; + +type Work = Awaitable & { + commit(): void, + + _reactRootContainer: *, + _expirationTime: ExpirationTime, +}; + export type HostConfig = { getRootHostContext(rootContainerInstance: C): CX, getChildHostContext(parentHostContext: CX, type: T, instance: C): CX, @@ -197,7 +213,7 @@ export type Reconciler = { container: OpaqueRoot, parentComponent: ?React$Component, callback: ?Function, - ): void, + ): Work, batchedUpdates(fn: () => A): A, unbatchedUpdates(fn: () => A): A, flushSync(fn: () => A): A, @@ -249,7 +265,7 @@ module.exports = function( current: Fiber, element: ReactNodeList, callback: ?Function, - ) { + ): ExpirationTime { if (__DEV__) { if ( ReactDebugCurrentFiber.phase === 'render' && @@ -293,28 +309,106 @@ module.exports = function( callback, ); } - addTopLevelUpdate( - current, - nextState, - callback, + const isTopLevelUnmount = nextState.element === null; + const update = { priorityLevel, expirationTime, - currentTime, - ); + partialState: nextState, + callback, + isReplace: false, + isForced: false, + isTopLevelUnmount, + next: null, + }; + const update2 = insertUpdateIntoFiber(current, update, currentTime); + + if (isTopLevelUnmount) { + // TODO: Redesign the top-level mount/update/unmount API to avoid this + // special case. + const queue1 = current.updateQueue; + const queue2 = current.alternate !== null + ? current.alternate.updateQueue + : null; + + // Drop all updates that are lower-priority, so that the tree is not + // remounted. We need to do this for both queues. + if (queue1 !== null && update.next !== null) { + update.next = null; + queue1.last = update; + } + if (queue2 !== null && update2 !== null && update2.next !== null) { + update2.next = null; + queue2.last = update; + } + } + scheduleUpdate(current, expirationTime); + return expirationTime; } + function WorkNode(root: OpaqueRoot, expirationTime: ExpirationTime) { + this._reactRootContainer = root; + this._expirationTime = expirationTime; + } + WorkNode.prototype.commit = function() {}; + WorkNode.prototype.then = function(resolve) { + // const fiber = this._reactRootContainer.current; + // const expirationTime = this._expirationTime; + // const currentTime = recalculateCurrentTime(); + // addCallback(fiber, resolve, null, expirationTime, currentTime); + // scheduleUpdate(fiber, expirationTime); + }; + return { createContainer(containerInfo: C): OpaqueRoot { return createFiberRoot(containerInfo); }, + updateRoot( + element: ReactNodeList, + container: OpaqueRoot, + parentComponent: ?React$Component, + didComplete: ?Function, + ) { + const current = container.current; + + if (__DEV__) { + if (ReactFiberInstrumentation.debugTool) { + if (current.alternate === null) { + ReactFiberInstrumentation.debugTool.onMountContainer(container); + } else if (element === null) { + ReactFiberInstrumentation.debugTool.onUnmountContainer(container); + } else { + ReactFiberInstrumentation.debugTool.onUpdateContainer(container); + } + } + } + + const context = getContextForSubtree(parentComponent); + if (container.context === null) { + container.context = context; + } else { + container.pendingContext = context; + } + + const expirationTime = scheduleTopLevelUpdate(current, element); + + let completionCallbacks = container.completionCallbacks; + if (completionCallbacks === null) { + completionCallbacks = createUpdateQueue(); + } + + // TODO: Add didComplete to root's completionCallbacks + + return new WorkNode(container, expirationTime); + }, + updateContainer( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component, callback: ?Function, - ): void { + ): Work { // TODO: If this is a nested container, this won't be the root. const current = container.current; diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js index a026174fd108d..6eef691900e2a 100644 --- a/src/renderers/shared/fiber/ReactFiberRoot.js +++ b/src/renderers/shared/fiber/ReactFiberRoot.js @@ -11,6 +11,7 @@ 'use strict'; import type {Fiber} from 'ReactFiber'; +import type {UpdateQueue} from 'ReactFiberUpdateQueue'; import type {ExpirationTime} from 'ReactFiberExpirationTime'; const {createHostRootFiber} = require('ReactFiber'); @@ -25,6 +26,9 @@ export type FiberRoot = { isScheduled: boolean, // The time at which this root completed. completedAt: ExpirationTime, + // A queue of callbacks that fire once their corresponding expiration time + // has completed. Only fired once. + completionCallbacks: UpdateQueue, // The work schedule is a linked list. nextScheduledRoot: FiberRoot | null, // Top context object, used by renderSubtreeIntoContainer @@ -51,6 +55,7 @@ exports.createFiberRoot = function(containerInfo: any): FiberRoot { containerInfo: containerInfo, isScheduled: false, completedAt: NoWork, + completionCallbacks: null, nextScheduledRoot: null, context: null, pendingContext: null, diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index 596ed9d41c985..72a0c83df3a25 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -20,7 +20,6 @@ const {NoWork} = require('ReactFiberExpirationTime'); const {ClassComponent, HostRoot} = require('ReactTypeOfWork'); -const invariant = require('fbjs/lib/invariant'); if (__DEV__) { var warning = require('fbjs/lib/warning'); } @@ -95,7 +94,7 @@ function cloneUpdate(update: Update): Update { const COALESCENCE_THRESHOLD: ExpirationTime = 10; -function insertUpdateIntoQueue( +function insertUpdateIntoPosition( queue: UpdateQueue, update: Update, insertAfter: Update | null, @@ -110,6 +109,8 @@ function insertUpdateIntoQueue( // could lead to starvation, so we stop coalescing once the time until the // expiration time reaches a certain threshold. if ( + // Only coalesce if a priority level is specified + update.priorityLevel !== null && insertAfter !== null && insertAfter.priorityLevel === update.priorityLevel ) { @@ -207,7 +208,7 @@ function ensureUpdateQueues(fiber: Fiber) { // we shouldn't make a copy. // // If the update is cloned, it returns the cloned update. -function insertUpdate( +function insertUpdateIntoFiber( fiber: Fiber, update: Update, currentTime: ExpirationTime, @@ -238,7 +239,7 @@ function insertUpdate( if (queue2 === null) { // If there's no alternate queue, there's nothing else to do but insert. - insertUpdateIntoQueue( + insertUpdateIntoPosition( queue1, update, insertAfter1, @@ -256,7 +257,7 @@ function insertUpdate( // Now we can insert into the first queue. This must come after finding both // insertion positions because it mutates the list. - insertUpdateIntoQueue( + insertUpdateIntoPosition( queue1, update, insertAfter1, @@ -285,7 +286,7 @@ function insertUpdate( // The insertion positions are different, so we need to clone the update and // insert the clone into the alternate queue. const update2 = cloneUpdate(update); - insertUpdateIntoQueue( + insertUpdateIntoPosition( queue2, update2, insertAfter2, @@ -295,71 +296,24 @@ function insertUpdate( return update2; } } +exports.insertUpdateIntoFiber = insertUpdateIntoFiber; -function addUpdate( - fiber: Fiber, - partialState: PartialState | null, - callback: mixed, - priorityLevel: PriorityLevel | null, - expirationTime: ExpirationTime, - currentTime: ExpirationTime, -): void { - const update = { - priorityLevel, - expirationTime, - partialState, - callback, - isReplace: false, - isForced: false, - isTopLevelUnmount: false, - next: null, - }; - insertUpdate(fiber, update, currentTime); -} -exports.addUpdate = addUpdate; - -function addReplaceUpdate( - fiber: Fiber, - state: any | null, - callback: Callback | null, - priorityLevel: PriorityLevel | null, - expirationTime: ExpirationTime, - currentTime: ExpirationTime, -): void { - const update = { - priorityLevel, - expirationTime, - partialState: state, - callback, - isReplace: true, - isForced: false, - isTopLevelUnmount: false, - next: null, - }; - insertUpdate(fiber, update, currentTime); -} -exports.addReplaceUpdate = addReplaceUpdate; - -function addForceUpdate( - fiber: Fiber, - callback: Callback | null, - priorityLevel: PriorityLevel | null, - expirationTime: ExpirationTime, +function insertUpdateIntoQueue( + queue: UpdateQueue, + update: Update, currentTime: ExpirationTime, -): void { - const update = { - priorityLevel, - expirationTime, - partialState: null, - callback, - isReplace: false, - isForced: true, - isTopLevelUnmount: false, - next: null, - }; - insertUpdate(fiber, update, currentTime); +) { + const insertAfter = findInsertionPosition(queue, update); + const insertBefore = insertAfter !== null ? insertAfter.next : null; + insertUpdateIntoPosition( + queue, + update, + insertAfter, + insertBefore, + currentTime, + ); } -exports.addForceUpdate = addForceUpdate; +exports.insertUpdateIntoQueue = insertUpdateIntoQueue; function getUpdateExpirationTime(fiber: Fiber): ExpirationTime { const updateQueue = fiber.updateQueue; @@ -373,48 +327,6 @@ function getUpdateExpirationTime(fiber: Fiber): ExpirationTime { } exports.getUpdateExpirationTime = getUpdateExpirationTime; -function addTopLevelUpdate( - fiber: Fiber, - partialState: PartialState, - callback: Callback | null, - priorityLevel: PriorityLevel | null, - expirationTime: ExpirationTime, - currentTime: ExpirationTime, -): void { - const isTopLevelUnmount = partialState.element === null; - - const update = { - priorityLevel, - expirationTime, - partialState, - callback, - isReplace: false, - isForced: false, - isTopLevelUnmount, - next: null, - }; - const update2 = insertUpdate(fiber, update, currentTime); - - if (isTopLevelUnmount) { - // TODO: Redesign the top-level mount/update/unmount API to avoid this - // special case. - const queue1 = _queue1; - const queue2 = _queue2; - - // Drop all updates that are lower-priority, so that the tree is not - // remounted. We need to do this for both queues. - if (queue1 !== null && update.next !== null) { - update.next = null; - queue1.last = update; - } - if (queue2 !== null && update2 !== null && update2.next !== null) { - update2.next = null; - queue2.last = update; - } - } -} -exports.addTopLevelUpdate = addTopLevelUpdate; - function getStateFromUpdate(update, instance, prevState, props) { const partialState = update.partialState; if (typeof partialState === 'function') { @@ -425,28 +337,13 @@ function getStateFromUpdate(update, instance, prevState, props) { } } -function beginUpdateQueue( - current: Fiber | null, - workInProgress: Fiber, +function processUpdateQueue( queue: UpdateQueue, - instance: any, - prevState: any, - props: any, + instance: mixed, + prevState: Object, + props: mixed, renderExpirationTime: ExpirationTime, -): any { - if (current !== null && current.updateQueue === queue) { - // We need to create a work-in-progress queue, by cloning the current queue. - const currentQueue = queue; - queue = workInProgress.updateQueue = { - first: currentQueue.first, - last: currentQueue.last, - // These fields are no longer valid because they were already committed. - // Reset them. - callbackList: null, - hasForceUpdate: false, - }; - } - +): mixed { if (__DEV__) { // Set this flag so we can warn if setState is called inside the update // function of another setState. @@ -497,7 +394,6 @@ function beginUpdateQueue( ) { callbackList = callbackList !== null ? callbackList : []; callbackList.push(update.callback); - workInProgress.effectTag |= CallbackEffect; } update = update.next; } @@ -505,11 +401,6 @@ function beginUpdateQueue( queue.callbackList = callbackList; queue.hasForceUpdate = hasForceUpdate; - if (queue.first === null && callbackList === null && !hasForceUpdate) { - // The queue is empty and there are no callbacks. We can reset it. - workInProgress.updateQueue = null; - } - if (__DEV__) { // No longer processing. queue.isProcessing = false; @@ -517,30 +408,49 @@ function beginUpdateQueue( return state; } -exports.beginUpdateQueue = beginUpdateQueue; +exports.insertUpdateIntoQueue = insertUpdateIntoQueue; -function commitCallbacks( - finishedWork: Fiber, +function beginUpdateQueue( + current: Fiber | null, + workInProgress: Fiber, queue: UpdateQueue, - context: mixed, -) { - const callbackList = queue.callbackList; - if (callbackList === null) { - return; + instance: any, + prevState: any, + props: any, + renderExpirationTime: ExpirationTime, +): any { + if (current !== null && current.updateQueue === queue) { + // We need to create a work-in-progress queue, by cloning the current queue. + const currentQueue = queue; + queue = workInProgress.updateQueue = { + first: currentQueue.first, + last: currentQueue.last, + // These fields are no longer valid because they were already committed. + // Reset them. + callbackList: null, + hasForceUpdate: false, + }; } - // Set the list to null to make sure they don't get called more than once. - queue.callbackList = null; + const state = processUpdateQueue( + queue, + instance, + prevState, + props, + renderExpirationTime, + ); - for (let i = 0; i < callbackList.length; i++) { - const callback = callbackList[i]; - invariant( - typeof callback === 'function', - 'Invalid argument passed as callback. Expected a function. Instead ' + - 'received: %s', - callback, - ); - callback.call(context); + const updatedQueue = workInProgress.updateQueue; + if (updatedQueue !== null) { + const callbackList = updatedQueue.callbackList; + if (callbackList !== null) { + workInProgress.effectTag |= CallbackEffect; + } else if (updatedQueue.first === null && !updatedQueue.hasForceUpdate) { + // The queue is empty. We can reset it. + workInProgress.updateQueue = null; + } } + + return state; } -exports.commitCallbacks = commitCallbacks; +exports.beginUpdateQueue = beginUpdateQueue; From 1fe313a9f9f9d7c701fa7f6a68378b8b4e8166d1 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 5 Sep 2017 23:25:10 -0700 Subject: [PATCH 14/24] API for prerendering a top-level update and deferring the commit `renderer.updateRoot` returns a Work object, which has methods `then` and `commit`. - `then` schedules a callback to fire once the update has completed. It resolves synchronously if the tree has already completed. - `commit` synchronously flushes all the remaining work. --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 44 +-- .../__tests__/ReactDOMAsyncRoot-test.js | 75 ++--- .../shared/fiber/ReactFiberReconciler.js | 95 ++++-- src/renderers/shared/fiber/ReactFiberRoot.js | 21 +- .../shared/fiber/ReactFiberScheduler.js | 280 +++++++++++------- .../shared/fiber/ReactFiberUpdateQueue.js | 57 ++-- 6 files changed, 357 insertions(+), 215 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index df33ae62b77db..4721f36e6304b 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -800,9 +800,9 @@ function createPortal( } type PublicRoot = { - render(children: ReactNodeList, callback: () => mixed): Work, - prerender(children: ReactNodeList, callback: () => mixed): Work, - unmount(callback: () => mixed): Work, + render(children: ReactNodeList, callback: ?() => mixed): void, + prerender(children: ReactNodeList): Work, + unmount(callback: ?() => mixed): Work, _reactRootContainer: *, _getComponent: () => DOMContainer, @@ -830,23 +830,29 @@ function PublicRootNode( } PublicRootNode.prototype.render = function( children: ReactNodeList, - callback: () => mixed, -): Work { - return DOMRenderer.updateContainer( - children, - this._reactRootContainer, - null, - callback, - ); + callback: ?() => mixed, +): void { + const work = DOMRenderer.updateRoot(children, this._reactRootContainer, null); + callback = callback === undefined ? null : callback; + work.then(() => { + work.commit(); + if (callback !== null) { + (callback: any)(); + } + }); }; -PublicRootNode.prototype.prerender = function() {}; -PublicRootNode.prototype.unmount = function() { - return DOMRenderer.updateContainer( - null, - this._reactRootContainer, - null, - null, - ); +PublicRootNode.prototype.prerender = function(children: ReactNodeList): Work { + return DOMRenderer.updateRoot(children, this._reactRootContainer, null); +}; +PublicRootNode.prototype.unmount = function(callback) { + const work = DOMRenderer.updateRoot(null, this._reactRootContainer, null); + callback = callback === undefined ? null : callback; + work.then(() => { + work.commit(); + if (callback !== null) { + (callback: any)(); + } + }); }; var ReactDOMFiber = { diff --git a/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js b/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js index 7398e0740ceb0..1f551af91815b 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js @@ -9,11 +9,13 @@ 'use strict'; +const ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); + let React; let ReactDOM; let ReactFeatureFlags; -describe('ReactDOMAsyncMount', () => { +describe('ReactDOMAsyncRoot', () => { beforeEach(() => { jest.resetModules(); @@ -23,51 +25,38 @@ describe('ReactDOMAsyncMount', () => { ReactFeatureFlags.enableAsyncSubtreeAPI = true; }); - it('works in easy mode', () => { - const container = document.createElement('div'); - const root = ReactDOM.unstable_create(container); - root.render(
Foo
); - expect(container.textContent).toEqual('Foo'); - root.render(
Bar
); - expect(container.textContent).toEqual('Bar'); - root.unmount(); - expect(container.textContent).toEqual(''); - }); - - it('can pass callback to render', () => { - const container = document.createElement('div'); - const root = ReactDOM.unstable_create(container); - let called = false; - root.render(
Foo
, () => { - called = true; + if (ReactDOMFeatureFlags.useFiber) { + it('works in easy mode', () => { + const container = document.createElement('div'); + const root = ReactDOM.unstable_create(container); + root.render(
Foo
); + expect(container.textContent).toEqual('Foo'); + root.render(
Bar
); + expect(container.textContent).toEqual('Bar'); + root.unmount(); + expect(container.textContent).toEqual(''); }); - expect(container.textContent).toEqual('Foo'); - expect(called).toBe(true); - }); - it('can await result of render method', async () => { - const container = document.createElement('div'); - const root = ReactDOM.unstable_create(container); - await root.render(
Foo
); - expect(container.textContent).toEqual('Foo'); - }); + it('can defer commit using prerender', () => { + const Async = React.unstable_AsyncComponent; + const container = document.createElement('div'); + const root = ReactDOM.unstable_create(container); + const work = root.prerender(Foo); - it('can defer commit using prerender', async () => { - const Async = React.unstable_AsyncComponent; - const container = document.createElement('div'); - const root = ReactDOM.unstable_create(container); - const work = root.prerender(Foo); + // Hasn't updated yet + expect(container.textContent).toEqual(''); - // Hasn't updated yet - expect(container.textContent).toEqual(''); + work.then(() => { + // Still hasn't updated + expect(container.textContent).toEqual(''); + // Should synchronously commit + work.commit(); + expect(container.textContent).toEqual('Foo'); + }); - await work; - - // Tree has completed, but still hasn't updated yet - expect(container.textContent).toEqual(''); - - // Synchronsouly update DOM - work.commit(); - expect(container.textContent).toEqual('Foo'); - }); + jest.runAllTimers(); + }); + } else { + it('does not apply to stack'); + } }); diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 9c55388d749fd..28034dcf67f37 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -20,7 +20,9 @@ var ReactFeatureFlags = require('ReactFeatureFlags'); var { insertUpdateIntoFiber, + insertUpdateIntoQueue, createUpdateQueue, + processUpdateQueue, } = require('ReactFiberUpdateQueue'); var { @@ -58,7 +60,7 @@ type Awaitable = { then(resolve: (result: T) => mixed): void, }; -type Work = Awaitable & { +export type Work = Awaitable & { commit(): void, _reactRootContainer: *, @@ -208,12 +210,17 @@ export type HostConfig = { export type Reconciler = { createContainer(containerInfo: C): OpaqueRoot, + updateRoot( + element: ReactNodeList, + container: OpaqueRoot, + parentComponent: ?React$Component, + ): Work, updateContainer( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component, callback: ?Function, - ): Work, + ): void, batchedUpdates
(fn: () => A): A, unbatchedUpdates(fn: () => A): A, flushSync(fn: () => A): A, @@ -255,6 +262,7 @@ module.exports = function( getPriorityContext, getExpirationTimeForPriority, recalculateCurrentTime, + expireWork, batchedUpdates, unbatchedUpdates, flushSync, @@ -262,8 +270,10 @@ module.exports = function( } = ReactFiberScheduler(config); function scheduleTopLevelUpdate( - current: Fiber, + root: FiberRoot, element: ReactNodeList, + currentTime: ExpirationTime, + isPrerender: boolean, callback: ?Function, ): ExpirationTime { if (__DEV__) { @@ -284,6 +294,8 @@ module.exports = function( } } + const current = root.current; + // Check if the top-level element is an async wrapper component. If so, treat // updates to the root as async. This is a bit weird but lets us avoid a separate // `renderAsync` API. @@ -294,7 +306,6 @@ module.exports = function( element.type.prototype != null && (element.type.prototype: any).unstable_isAsyncReactComponent === true; const priorityLevel = getPriorityContext(current, forceAsync); - const currentTime = recalculateCurrentTime(); const expirationTime = getExpirationTimeForPriority( currentTime, priorityLevel, @@ -342,6 +353,24 @@ module.exports = function( } } + if (isPrerender) { + // Block the root from committing at this expiration time. + if (root.blockers === null) { + root.blockers = createUpdateQueue(); + } + const blockUpdate = { + priorityLevel: null, + expirationTime, + partialState: nextState, + callback: null, + isReplace: false, + isForced: false, + isTopLevelUnmount: false, + next: null, + }; + insertUpdateIntoQueue(root.blockers, blockUpdate, currentTime); + } + scheduleUpdate(current, expirationTime); return expirationTime; } @@ -350,13 +379,38 @@ module.exports = function( this._reactRootContainer = root; this._expirationTime = expirationTime; } - WorkNode.prototype.commit = function() {}; - WorkNode.prototype.then = function(resolve) { - // const fiber = this._reactRootContainer.current; - // const expirationTime = this._expirationTime; - // const currentTime = recalculateCurrentTime(); - // addCallback(fiber, resolve, null, expirationTime, currentTime); - // scheduleUpdate(fiber, expirationTime); + WorkNode.prototype.commit = function() { + const root = this._reactRootContainer; + const expirationTime = this._expirationTime; + const blockers = root.blockers; + if (blockers === null) { + return; + } + processUpdateQueue(blockers, null, null, null, expirationTime); + expireWork(expirationTime); + }; + WorkNode.prototype.then = function(callback) { + const root = this._reactRootContainer; + const expirationTime = this._expirationTime; + + // Add callback to queue of callbacks on the root. It will be called once + // the root completes at the corresponding expiration time. + const update = { + priorityLevel: null, + expirationTime, + partialState: null, + callback, + isReplace: false, + isForced: false, + isTopLevelUnmount: false, + next: null, + }; + const currentTime = recalculateCurrentTime(); + if (root.completionCallbacks === null) { + root.completionCallbacks = createUpdateQueue(); + } + insertUpdateIntoQueue(root.completionCallbacks, update, currentTime); + scheduleUpdate(root.current, expirationTime); }; return { @@ -368,8 +422,7 @@ module.exports = function( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component, - didComplete: ?Function, - ) { + ): Work { const current = container.current; if (__DEV__) { @@ -391,15 +444,20 @@ module.exports = function( container.pendingContext = context; } - const expirationTime = scheduleTopLevelUpdate(current, element); + const currentTime = recalculateCurrentTime(); + const expirationTime = scheduleTopLevelUpdate( + container, + element, + currentTime, + true, + null, + ); let completionCallbacks = container.completionCallbacks; if (completionCallbacks === null) { completionCallbacks = createUpdateQueue(); } - // TODO: Add didComplete to root's completionCallbacks - return new WorkNode(container, expirationTime); }, @@ -408,7 +466,7 @@ module.exports = function( container: OpaqueRoot, parentComponent: ?React$Component, callback: ?Function, - ): Work { + ): void { // TODO: If this is a nested container, this won't be the root. const current = container.current; @@ -431,7 +489,8 @@ module.exports = function( container.pendingContext = context; } - scheduleTopLevelUpdate(current, element, callback); + const currentTime = recalculateCurrentTime(); + scheduleTopLevelUpdate(container, element, currentTime, false, callback); }, batchedUpdates, diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js index 6eef691900e2a..4a4a2c92c8b4a 100644 --- a/src/renderers/shared/fiber/ReactFiberRoot.js +++ b/src/renderers/shared/fiber/ReactFiberRoot.js @@ -15,6 +15,7 @@ import type {UpdateQueue} from 'ReactFiberUpdateQueue'; import type {ExpirationTime} from 'ReactFiberExpirationTime'; const {createHostRootFiber} = require('ReactFiber'); +const {getUpdateQueueExpirationTime} = require('ReactFiberUpdateQueue'); const {NoWork} = require('ReactFiberExpirationTime'); export type FiberRoot = { @@ -26,9 +27,12 @@ export type FiberRoot = { isScheduled: boolean, // The time at which this root completed. completedAt: ExpirationTime, + // A queue that represents times at which this root is blocked + // from committing. + blockers: UpdateQueue | null, // A queue of callbacks that fire once their corresponding expiration time // has completed. Only fired once. - completionCallbacks: UpdateQueue, + completionCallbacks: UpdateQueue | null, // The work schedule is a linked list. nextScheduledRoot: FiberRoot | null, // Top context object, used by renderSubtreeIntoContainer @@ -36,14 +40,16 @@ export type FiberRoot = { pendingContext: Object | null, }; -// Indicates whether the root is blocked from committing at a particular -// expiration time. exports.isRootBlocked = function( root: FiberRoot, - time: ExpirationTime, -): boolean { - // TODO: Implementation - return false; + expirationTime: ExpirationTime, +) { + const blockers = root.blockers; + if (blockers === null) { + return false; + } + const blockedAt = getUpdateQueueExpirationTime(blockers); + return blockedAt !== NoWork && blockedAt <= expirationTime; }; exports.createFiberRoot = function(containerInfo: any): FiberRoot { @@ -55,6 +61,7 @@ exports.createFiberRoot = function(containerInfo: any): FiberRoot { containerInfo: containerInfo, isScheduled: false, completedAt: NoWork, + blockers: null, completionCallbacks: null, nextScheduledRoot: null, context: null, diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 097a781065cec..f288992e9b24f 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -92,7 +92,10 @@ var { ClassComponent, } = require('ReactTypeOfWork'); -var {getUpdateExpirationTime} = require('ReactFiberUpdateQueue'); +var { + getUpdateExpirationTime, + processUpdateQueue, +} = require('ReactFiberUpdateQueue'); var {resetContext} = require('ReactFiberContext'); @@ -224,12 +227,17 @@ module.exports = function( let nextUnitOfWork: Fiber | null = null; // The time at which we're currently rendering work. let nextRenderExpirationTime: ExpirationTime = NoWork; + // If not null, all work up to and including this time should be + // flushed before the end of the current batch. + let forceExpire: ExpirationTime | null = null; // The next fiber with an effect that we're currently committing. let nextEffect: Fiber | null = null; let pendingCommit: Fiber | null = null; + let rootCompletionCallbackList: Array<() => mixed> | null = null; + // Linked list of roots with scheduled work on them. let nextScheduledRoot: FiberRoot | null = null; let lastScheduledRoot: FiberRoot | null = null; @@ -352,6 +360,8 @@ module.exports = function( // Indicates whether the root should be worked on. Not the same as whether a // root has work, because work could be blocked. + // TODO: Find a better name for this function. It also schedules completion + // callbacks, if a root is blocked. function shouldWorkOnRoot(root: FiberRoot): ExpirationTime { const completedAt = root.completedAt; const expirationTime = root.current.expirationTime; @@ -374,8 +384,38 @@ module.exports = function( // If it's still blocked, return NoWork, as if it has no more work. If it's // no longer blocked, return the time at which it completed so that we // can commit it. - const isBlocked = isRootBlocked(root, expirationTime); - return isBlocked ? NoWork : completedAt; + if (isRootBlocked(root, completedAt)) { + // Process pending completion callbacks so that they are called at + // the end of the current batch. + const completionCallbacks = root.completionCallbacks; + if (completionCallbacks !== null) { + processUpdateQueue( + completionCallbacks, + null, + null, + null, + completedAt, + ); + const callbackList = completionCallbacks.callbackList; + if (callbackList !== null) { + // Add new callbacks to list of completion callbacks + if (rootCompletionCallbackList === null) { + rootCompletionCallbackList = callbackList; + } else { + for (let i = 0; i < callbackList.length; i++) { + rootCompletionCallbackList.push(callbackList[i]); + } + } + completionCallbacks.callbackList = null; + if (completionCallbacks.first === null) { + root.completionCallbacks = null; + } + } + } + return NoWork; + } + + return completedAt; } return expirationTime; @@ -761,13 +801,16 @@ module.exports = function( workInProgress = returnFiber; continue; } else { - // We've reached the root. Mark the root as pending commit. Depending - // on how much time we have left, we'll either commit it now or in - // the next frame. const root = workInProgress.stateNode; + // We've reached the root. Mark the root as complete. Depending on how + // much time we have left, we'll either commit it now or in the + // next frame. if (isRootBlocked(root, nextRenderExpirationTime)) { + // The root is blocked from committing. Mark it as complete so we + // know we can commit it later without starting new work. root.completedAt = workInProgress.expirationTime = nextRenderExpirationTime; } else { + // The root is not blocked, so we can commit it now. pendingCommit = workInProgress; } return null; @@ -872,13 +915,12 @@ module.exports = function( nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } if (nextUnitOfWork === null) { - invariant( - pendingCommit !== null, - 'Should have a pending commit. This error is likely caused by ' + - 'a bug in React. Please file an issue.', - ); - // We just completed a root. Commit it now. - commitAllWork(pendingCommit); + if (pendingCommit !== null) { + // We just completed a root. Commit it now. + commitAllWork(pendingCommit); + } else { + resetNextUnitOfWork(); + } if ( capturedErrors === null || capturedErrors.size === 0 || @@ -896,8 +938,52 @@ module.exports = function( } } + function shouldContinueWorking( + minPriorityLevel: PriorityLevel, + nextPriorityLevel: PriorityLevel, + deadline: Deadline | null, + ): boolean { + // There might be work left. Depending on the priority, we should + // either perform it now or schedule a callback to perform it later. + switch (nextPriorityLevel) { + case SynchronousPriority: + case TaskPriority: + // We have remaining synchronous or task work. Keep performing it, + // regardless of whether we're inside a callback. + if (nextPriorityLevel <= minPriorityLevel) { + return true; + } + return false; + case HighPriority: + case LowPriority: + case OffscreenPriority: + // We have remaining async work. + if (deadline === null) { + // We're not inside a callback. Exit and perform the work during + // the next callback. + return false; + } + // We are inside a callback. + if (!deadlineHasExpired && nextPriorityLevel <= minPriorityLevel) { + // We still have time. Keep working. + return true; + } + // We've run out of time. Exit. + return false; + case NoWork: + // No work left. We can exit. + return false; + default: + invariant( + false, + 'Switch statement should be exhuastive. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + } + } + function workLoop( - minExpirationTime: ExpirationTime, + minPriorityLevel: PriorityLevel, deadline: Deadline | null, ) { if (pendingCommit !== null) { @@ -907,33 +993,36 @@ module.exports = function( resetNextUnitOfWork(); } - if ( - nextRenderExpirationTime === NoWork || - nextRenderExpirationTime > minExpirationTime - ) { - return; - } + let nextPriorityLevel = expirationTimeToPriorityLevel( + recalculateCurrentTime(), + nextRenderExpirationTime, + ); - loop: do { + loop: while ( + shouldContinueWorking(minPriorityLevel, nextPriorityLevel, deadline) + ) { if (nextRenderExpirationTime <= mostRecentCurrentTime) { // Flush all expired work. while (nextUnitOfWork !== null) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); if (nextUnitOfWork === null) { - invariant( - pendingCommit !== null, - 'Should have a pending commit. This error is likely caused by ' + - 'a bug in React. Please file an issue.', - ); - // We just completed a root. Commit it now. - commitAllWork(pendingCommit); - // Clear any errors that were scheduled during the commit phase. - handleCommitPhaseErrors(); + if (pendingCommit !== null) { + // We just completed a root. Commit it now. + commitAllWork(pendingCommit); + // Clear any errors that were scheduled during the commit phase. + handleCommitPhaseErrors(); + } else { + resetNextUnitOfWork(); + } // The render time may have changed. Check again. + nextPriorityLevel = expirationTimeToPriorityLevel( + recalculateCurrentTime(), + nextRenderExpirationTime, + ); if ( - nextRenderExpirationTime === NoWork || - nextRenderExpirationTime > minExpirationTime || - nextRenderExpirationTime > mostRecentCurrentTime + nextPriorityLevel === NoWork || + nextPriorityLevel > minPriorityLevel || + nextPriorityLevel > TaskPriority ) { // We've completed all the expired work. break; @@ -950,22 +1039,24 @@ module.exports = function( // omit either of the checks in the following condition, but we need // both to satisfy Flow. if (nextUnitOfWork === null) { - invariant( - pendingCommit !== null, - 'Should have a pending commit. This error is likely caused by ' + - 'a bug in React. Please file an issue.', - ); - // We just completed a root. If we have time, commit it now. - // Otherwise, we'll commit it in the next frame. if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { - commitAllWork(pendingCommit); - // Clear any errors that were scheduled during the commit phase. - handleCommitPhaseErrors(); + if (pendingCommit !== null) { + // We just completed a root. Commit it now. + commitAllWork(pendingCommit); + // Clear any errors that were scheduled during the commit phase. + handleCommitPhaseErrors(); + } else { + resetNextUnitOfWork(); + } // The render time may have changed. Check again. + nextPriorityLevel = expirationTimeToPriorityLevel( + recalculateCurrentTime(), + nextRenderExpirationTime, + ); if ( - nextRenderExpirationTime === NoWork || - nextRenderExpirationTime > minExpirationTime || - nextRenderExpirationTime <= mostRecentCurrentTime + nextPriorityLevel === NoWork || + nextPriorityLevel > minPriorityLevel || + nextPriorityLevel <= TaskPriority ) { // We've completed all the async work. break; @@ -979,58 +1070,13 @@ module.exports = function( } } } - - // There might be work left. Depending on the priority, we should - // either perform it now or schedule a callback to perform it later. - const currentTime = recalculateCurrentTime(); - switch (expirationTimeToPriorityLevel( - currentTime, - nextRenderExpirationTime, - )) { - case SynchronousPriority: - case TaskPriority: - // We have remaining synchronous or task work. Keep performing it, - // regardless of whether we're inside a callback. - if (nextRenderExpirationTime <= minExpirationTime) { - continue loop; - } - break loop; - case HighPriority: - case LowPriority: - case OffscreenPriority: - // We have remaining async work. - if (deadline === null) { - // We're not inside a callback. Exit and perform the work during - // the next callback. - break loop; - } - // We are inside a callback. - if ( - !deadlineHasExpired && - nextRenderExpirationTime <= minExpirationTime - ) { - // We still have time. Keep working. - continue loop; - } - // We've run out of time. Exit. - break loop; - case NoWork: - // No work left. We can exit. - break loop; - default: - invariant( - false, - 'Switch statement should be exhuastive. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); - } - } while (true); + } } function performWorkCatchBlock( failedWork: Fiber, boundary: Fiber, - minExpirationTime: ExpirationTime, + minPriorityLevel: PriorityLevel, deadline: Deadline | null, ) { // We're going to restart the error boundary that captured the error. @@ -1046,7 +1092,7 @@ module.exports = function( nextUnitOfWork = performFailedUnitOfWork(boundary); // Continue working. - workLoop(minExpirationTime, deadline); + workLoop(minPriorityLevel, deadline); } function performWork( @@ -1064,32 +1110,27 @@ module.exports = function( ); isPerformingWork = true; + rootCompletionCallbackList = null; + // Updates that occur during the commit phase should have task priority // by default. (Render phase updates are special; getPriorityContext // accounts for their behavior.) const previousPriorityContext = priorityContext; priorityContext = TaskPriority; - // Read the current time from the host environment. - const currentTime = recalculateCurrentTime(); - const minExpirationTime = getExpirationTimeForPriority( - currentTime, - minPriorityLevel, - ); - nestedUpdateCount = 0; let didError = false; let error = null; if (__DEV__) { - invokeGuardedCallback(null, workLoop, null, minExpirationTime, deadline); + invokeGuardedCallback(null, workLoop, null, minPriorityLevel, deadline); if (hasCaughtError()) { didError = true; error = clearCaughtError(); } } else { try { - workLoop(minExpirationTime, deadline); + workLoop(minPriorityLevel, deadline); } catch (e) { didError = true; error = e; @@ -1136,7 +1177,7 @@ module.exports = function( null, failedWork, boundary, - minExpirationTime, + minPriorityLevel, deadline, ); if (hasCaughtError()) { @@ -1149,7 +1190,7 @@ module.exports = function( performWorkCatchBlock( failedWork, boundary, - minExpirationTime, + minPriorityLevel, deadline, ); error = null; @@ -1198,6 +1239,15 @@ module.exports = function( if (errorToThrow !== null) { throw errorToThrow; } + + // Call completion callbacks. These callbacks may call performWork. This + // is the one place where recursion is allowed. + if (rootCompletionCallbackList !== null) { + const list = rootCompletionCallbackList; + for (let i = 0; i < list.length; i++) { + list[i](); + } + } } // Returns the boundary that captured the error, or null if the error is ignored @@ -1630,12 +1680,31 @@ module.exports = function( } function recalculateCurrentTime(): ExpirationTime { + if (forceExpire !== null) { + return forceExpire; + } // Subtract initial time so it fits inside 32bits const ms = now() - startTime; mostRecentCurrentTime = msToExpirationTime(ms); return mostRecentCurrentTime; } + function expireWork(expirationTime: ExpirationTime): void { + invariant( + !isPerformingWork, + 'Cannot commit while already performing work.', + ); + // Override the current time with the given time. This has the effect of + // expiring all work up to and including that time. + forceExpire = mostRecentCurrentTime = expirationTime; + try { + performWork(TaskPriority, null); + } finally { + forceExpire = null; + recalculateCurrentTime(); + } + } + function batchedUpdates(fn: (a: A) => R, a: A): R { const previousIsBatchingUpdates = isBatchingUpdates; isBatchingUpdates = true; @@ -1700,6 +1769,7 @@ module.exports = function( getPriorityContext: getPriorityContext, recalculateCurrentTime: recalculateCurrentTime, getExpirationTimeForPriority: getExpirationTimeForPriority, + expireWork: expireWork, batchedUpdates: batchedUpdates, unbatchedUpdates: unbatchedUpdates, flushSync: flushSync, diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index 72a0c83df3a25..ca3e9c461bad1 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -31,15 +31,15 @@ type PartialState = // Callbacks are not validated until invocation type Callback = mixed; -export type Update = { +export type Update = { priorityLevel: PriorityLevel | null, expirationTime: ExpirationTime, - partialState: PartialState, + partialState: PartialState, callback: Callback | null, isReplace: boolean, isForced: boolean, isTopLevelUnmount: boolean, - next: Update | null, + next: Update | null, }; // Singly linked-list of updates. When an update is scheduled, it is added to @@ -53,9 +53,9 @@ export type Update = { // The work-in-progress queue is always a subset of the current queue. // // When the tree is committed, the work-in-progress becomes the current. -export type UpdateQueue = { - first: Update | null, - last: Update | null, +export type UpdateQueue = { + first: Update | null, + last: Update | null, hasForceUpdate: boolean, callbackList: null | Array, @@ -66,7 +66,7 @@ export type UpdateQueue = { let _queue1; let _queue2; -function createUpdateQueue(): UpdateQueue { +function createUpdateQueue(): UpdateQueue { const queue: UpdateQueue = { first: null, last: null, @@ -78,8 +78,9 @@ function createUpdateQueue(): UpdateQueue { } return queue; } +exports.createUpdateQueue = createUpdateQueue; -function cloneUpdate(update: Update): Update { +function cloneUpdate(update: Update): Update { return { priorityLevel: update.priorityLevel, expirationTime: update.expirationTime, @@ -95,10 +96,10 @@ function cloneUpdate(update: Update): Update { const COALESCENCE_THRESHOLD: ExpirationTime = 10; function insertUpdateIntoPosition( - queue: UpdateQueue, - update: Update, - insertAfter: Update | null, - insertBefore: Update | null, + queue: UpdateQueue, + update: Update, + insertAfter: Update | null, + insertBefore: Update | null, currentTime: ExpirationTime, ) { if (insertAfter !== null) { @@ -135,7 +136,10 @@ function insertUpdateIntoPosition( // Returns the update after which the incoming update should be inserted into // the queue, or null if it should be inserted at beginning. -function findInsertionPosition(queue, update): Update | null { +function findInsertionPosition( + queue: UpdateQueue, + update: Update, +): Update | null { const expirationTime = update.expirationTime; let insertAfter = null; let insertBefore = null; @@ -210,9 +214,9 @@ function ensureUpdateQueues(fiber: Fiber) { // If the update is cloned, it returns the cloned update. function insertUpdateIntoFiber( fiber: Fiber, - update: Update, + update: Update, currentTime: ExpirationTime, -): Update | null { +): Update | null { // We'll have at least one and at most two distinct update queues. ensureUpdateQueues(fiber); const queue1 = _queue1; @@ -300,7 +304,7 @@ exports.insertUpdateIntoFiber = insertUpdateIntoFiber; function insertUpdateIntoQueue( queue: UpdateQueue, - update: Update, + update: Update, currentTime: ExpirationTime, ) { const insertAfter = findInsertionPosition(queue, update); @@ -323,10 +327,17 @@ function getUpdateExpirationTime(fiber: Fiber): ExpirationTime { if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) { return NoWork; } - return updateQueue.first !== null ? updateQueue.first.expirationTime : NoWork; + return getUpdateQueueExpirationTime(updateQueue); } exports.getUpdateExpirationTime = getUpdateExpirationTime; +function getUpdateQueueExpirationTime( + updateQueue: UpdateQueue, +): ExpirationTime { + return updateQueue.first !== null ? updateQueue.first.expirationTime : NoWork; +} +exports.getUpdateQueueExpirationTime = getUpdateQueueExpirationTime; + function getStateFromUpdate(update, instance, prevState, props) { const partialState = update.partialState; if (typeof partialState === 'function') { @@ -338,12 +349,12 @@ function getStateFromUpdate(update, instance, prevState, props) { } function processUpdateQueue( - queue: UpdateQueue, + queue: UpdateQueue, instance: mixed, - prevState: Object, + prevState: State, props: mixed, renderExpirationTime: ExpirationTime, -): mixed { +): State { if (__DEV__) { // Set this flag so we can warn if setState is called inside the update // function of another setState. @@ -408,17 +419,17 @@ function processUpdateQueue( return state; } -exports.insertUpdateIntoQueue = insertUpdateIntoQueue; +exports.processUpdateQueue = processUpdateQueue; function beginUpdateQueue( current: Fiber | null, workInProgress: Fiber, - queue: UpdateQueue, + queue: UpdateQueue, instance: any, prevState: any, props: any, renderExpirationTime: ExpirationTime, -): any { +): State { if (current !== null && current.updateQueue === queue) { // We need to create a work-in-progress queue, by cloning the current queue. const currentQueue = queue; From 792c81024aa805213ae8b4933d4e5e6cde1e7544 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 6 Sep 2017 14:50:25 -0700 Subject: [PATCH 15/24] Fix UpdateQueue flow types This makes the types for UpdateQueue a bit more sound though it's still not quite right. Not sure how to properly State so that it works with partial state updates, replaceState, and updater functions. This at least gives us more safety for non-Fiber update queues, like FiberRoot's completionCallbacks queue. --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 2 +- src/renderers/noop/ReactNoopEntry.js | 2 +- src/renderers/shared/fiber/ReactFiber.js | 2 +- .../shared/fiber/ReactFiberReconciler.js | 2 +- .../shared/fiber/ReactFiberScheduler.js | 2 +- .../shared/fiber/ReactFiberUpdateQueue.js | 23 ++++++++++--------- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 4721f36e6304b..6141d8cc7e745 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -802,7 +802,7 @@ function createPortal( type PublicRoot = { render(children: ReactNodeList, callback: ?() => mixed): void, prerender(children: ReactNodeList): Work, - unmount(callback: ?() => mixed): Work, + unmount(callback: ?() => mixed): void, _reactRootContainer: *, _getComponent: () => DOMContainer, diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index 6a5ed0b11ed32..65379d719ac4d 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -403,7 +403,7 @@ var ReactNoop = { logHostInstances(container.children, depth + 1); } - function logUpdateQueue(updateQueue: UpdateQueue, depth) { + function logUpdateQueue(updateQueue: UpdateQueue, depth) { log(' '.repeat(depth + 1) + 'QUEUED UPDATES'); const firstUpdate = updateQueue.first; if (!firstUpdate) { diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 8bbffcc48b6bc..4edc1d37a09f3 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -112,7 +112,7 @@ export type Fiber = {| memoizedProps: any, // The props used to create the output. // A queue of state updates and callbacks. - updateQueue: UpdateQueue | null, + updateQueue: UpdateQueue | null, // The state used to create the output memoizedState: any, diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 28034dcf67f37..6cdc1c81ac69b 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -361,7 +361,7 @@ module.exports = function( const blockUpdate = { priorityLevel: null, expirationTime, - partialState: nextState, + partialState: null, callback: null, isReplace: false, isForced: false, diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index f288992e9b24f..fc716b5d3bbd2 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -998,7 +998,7 @@ module.exports = function( nextRenderExpirationTime, ); - loop: while ( + while ( shouldContinueWorking(minPriorityLevel, nextPriorityLevel, deadline) ) { if (nextRenderExpirationTime <= mostRecentCurrentTime) { diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index ca3e9c461bad1..f515097716978 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -28,8 +28,7 @@ type PartialState = | $Subtype | ((prevState: State, props: Props) => $Subtype); -// Callbacks are not validated until invocation -type Callback = mixed; +type Callback = () => mixed; export type Update = { priorityLevel: PriorityLevel | null, @@ -67,7 +66,7 @@ let _queue1; let _queue2; function createUpdateQueue(): UpdateQueue { - const queue: UpdateQueue = { + const queue: UpdateQueue = { first: null, last: null, hasForceUpdate: false, @@ -80,7 +79,7 @@ function createUpdateQueue(): UpdateQueue { } exports.createUpdateQueue = createUpdateQueue; -function cloneUpdate(update: Update): Update { +function cloneUpdate(update: Update): Update { return { priorityLevel: update.priorityLevel, expirationTime: update.expirationTime, @@ -95,7 +94,7 @@ function cloneUpdate(update: Update): Update { const COALESCENCE_THRESHOLD: ExpirationTime = 10; -function insertUpdateIntoPosition( +function insertUpdateIntoPosition( queue: UpdateQueue, update: Update, insertAfter: Update | null, @@ -136,7 +135,7 @@ function insertUpdateIntoPosition( // Returns the update after which the incoming update should be inserted into // the queue, or null if it should be inserted at beginning. -function findInsertionPosition( +function findInsertionPosition( queue: UpdateQueue, update: Update, ): Update | null { @@ -212,7 +211,7 @@ function ensureUpdateQueues(fiber: Fiber) { // we shouldn't make a copy. // // If the update is cloned, it returns the cloned update. -function insertUpdateIntoFiber( +function insertUpdateIntoFiber( fiber: Fiber, update: Update, currentTime: ExpirationTime, @@ -302,8 +301,8 @@ function insertUpdateIntoFiber( } exports.insertUpdateIntoFiber = insertUpdateIntoFiber; -function insertUpdateIntoQueue( - queue: UpdateQueue, +function insertUpdateIntoQueue( + queue: UpdateQueue, update: Update, currentTime: ExpirationTime, ) { @@ -342,13 +341,14 @@ function getStateFromUpdate(update, instance, prevState, props) { const partialState = update.partialState; if (typeof partialState === 'function') { const updateFn = partialState; + // $FlowFixMe - Idk how to type State correctly. return updateFn.call(instance, prevState, props); } else { return partialState; } } -function processUpdateQueue( +function processUpdateQueue( queue: UpdateQueue, instance: mixed, prevState: State, @@ -387,6 +387,7 @@ function processUpdateQueue( partialState = getStateFromUpdate(update, instance, state, props); if (partialState) { if (dontMutatePrevState) { + // $FlowFixMe - Idk how to type State properly. state = Object.assign({}, state, partialState); } else { state = Object.assign(state, partialState); @@ -421,7 +422,7 @@ function processUpdateQueue( } exports.processUpdateQueue = processUpdateQueue; -function beginUpdateQueue( +function beginUpdateQueue( current: Fiber | null, workInProgress: Fiber, queue: UpdateQueue, From 4ea93cd7d79cb11aefbf6cc50f51c564ea044011 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 8 Sep 2017 13:34:31 -0700 Subject: [PATCH 16/24] Completion callbacks resolve synchronously if tree is already complete More unit tests. These completion callbacks (as I'm calling them) have some interesting properties. --- src/renderers/noop/ReactNoopEntry.js | 22 +++++ .../shared/fiber/ReactFiberReconciler.js | 26 +---- .../shared/fiber/ReactFiberScheduler.js | 57 ++++++++++- .../__tests__/ReactIncrementalRoot-test.js | 99 +++++++++++++++++++ 4 files changed, 178 insertions(+), 26 deletions(-) create mode 100644 src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index 65379d719ac4d..4889e8d185ac2 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -24,6 +24,7 @@ var ReactFiberInstrumentation = require('ReactFiberInstrumentation'); var ReactFiberReconciler = require('ReactFiberReconciler'); var ReactInstanceMap = require('ReactInstanceMap'); var emptyObject = require('fbjs/lib/emptyObject'); +var invariant = require('fbjs/lib/invariant'); var expect = require('jest-matchers'); @@ -283,6 +284,27 @@ var ReactNoop = { } }, + create(rootID: string) { + rootID = typeof rootID === 'string' ? rootID : DEFAULT_ROOT_ID; + invariant( + !roots.has(rootID), + 'Root with id %s already exists. Choose a different id.', + rootID, + ); + const container = {rootID: rootID, children: []}; + rootContainers.set(rootID, container); + const root = NoopRenderer.createContainer(container); + roots.set(rootID, root); + return { + prerender(children: any) { + return NoopRenderer.updateRoot(children, root, null); + }, + getChildren() { + return ReactNoop.getChildren(rootID); + }, + }; + }, + findInstance( componentOrElement: Element | ?React$Component, ): null | Instance | TextInstance { diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 6cdc1c81ac69b..2835092aaccb1 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -12,7 +12,6 @@ import type {Fiber} from 'ReactFiber'; import type {FiberRoot} from 'ReactFiberRoot'; -import type {PriorityLevel} from 'ReactPriorityLevel'; import type {ExpirationTime} from 'ReactFiberExpirationTime'; import type {ReactNodeList} from 'ReactTypes'; @@ -259,6 +258,7 @@ module.exports = function( var { scheduleUpdate, + scheduleCompletionCallback, getPriorityContext, getExpirationTimeForPriority, recalculateCurrentTime, @@ -358,7 +358,7 @@ module.exports = function( if (root.blockers === null) { root.blockers = createUpdateQueue(); } - const blockUpdate = { + const block = { priorityLevel: null, expirationTime, partialState: null, @@ -368,7 +368,7 @@ module.exports = function( isTopLevelUnmount: false, next: null, }; - insertUpdateIntoQueue(root.blockers, blockUpdate, currentTime); + insertUpdateIntoQueue(root.blockers, block, currentTime); } scheduleUpdate(current, expirationTime); @@ -392,25 +392,7 @@ module.exports = function( WorkNode.prototype.then = function(callback) { const root = this._reactRootContainer; const expirationTime = this._expirationTime; - - // Add callback to queue of callbacks on the root. It will be called once - // the root completes at the corresponding expiration time. - const update = { - priorityLevel: null, - expirationTime, - partialState: null, - callback, - isReplace: false, - isForced: false, - isTopLevelUnmount: false, - next: null, - }; - const currentTime = recalculateCurrentTime(); - if (root.completionCallbacks === null) { - root.completionCallbacks = createUpdateQueue(); - } - insertUpdateIntoQueue(root.completionCallbacks, update, currentTime); - scheduleUpdate(root.current, expirationTime); + scheduleCompletionCallback(root, callback, expirationTime); }; return { diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index fc716b5d3bbd2..38dfe1f36d786 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -95,6 +95,8 @@ var { var { getUpdateExpirationTime, processUpdateQueue, + createUpdateQueue, + insertUpdateIntoQueue, } = require('ReactFiberUpdateQueue'); var {resetContext} = require('ReactFiberContext'); @@ -373,13 +375,28 @@ module.exports = function( if (completedAt !== NoWork) { // The root completed but was blocked from committing. - if (expirationTime < completedAt) { - // We have work that expires earlier than the completed root. Regardless - // of whether the root is blocked, we should work on it. + // We have work that expires earlier than the completed root. return expirationTime; } + // If the expiration time of the pending work is equal to the time at + // which we completed the work-in-progress, it's possible additional + // work was scheduled that happens to fall within the same expiration + // bucket. We need to check the work-in-progress fiber. + if (expirationTime === completedAt) { + const workInProgress = root.current.alternate; + if ( + workInProgress !== null && + (workInProgress.expirationTime !== NoWork && + workInProgress.expirationTime <= expirationTime) + ) { + // We have more work. Restart the completed tree. + root.completedAt = NoWork; + return expirationTime; + } + } + // There have been no higher priority updates since we completed the root. // If it's still blocked, return NoWork, as if it has no more work. If it's // no longer blocked, return the time at which it completed so that we @@ -808,7 +825,7 @@ module.exports = function( if (isRootBlocked(root, nextRenderExpirationTime)) { // The root is blocked from committing. Mark it as complete so we // know we can commit it later without starting new work. - root.completedAt = workInProgress.expirationTime = nextRenderExpirationTime; + root.completedAt = nextRenderExpirationTime; } else { // The root is not blocked, so we can commit it now. pendingCommit = workInProgress; @@ -1625,6 +1642,37 @@ module.exports = function( } } + function scheduleCompletionCallback( + root: FiberRoot, + callback: () => mixed, + expirationTime: ExpirationTime, + ) { + // Add callback to queue of callbacks on the root. It will be called once + // the root completes at the corresponding expiration time. + const update = { + priorityLevel: null, + expirationTime, + partialState: null, + callback, + isReplace: false, + isForced: false, + isTopLevelUnmount: false, + next: null, + }; + const currentTime = recalculateCurrentTime(); + if (root.completionCallbacks === null) { + root.completionCallbacks = createUpdateQueue(); + } + insertUpdateIntoQueue(root.completionCallbacks, update, currentTime); + if (expirationTime === root.completedAt) { + // The tree already completed at this expiration time. Resolve the + // callback synchronously. + performWork(TaskPriority, null); + } else { + scheduleUpdate(root.current, expirationTime); + } + } + function getPriorityContext( fiber: Fiber, forceAsync: boolean, @@ -1766,6 +1814,7 @@ module.exports = function( return { scheduleUpdate: scheduleUpdate, + scheduleCompletionCallback: scheduleCompletionCallback, getPriorityContext: getPriorityContext, recalculateCurrentTime: recalculateCurrentTime, getExpirationTimeForPriority: getExpirationTimeForPriority, diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js new file mode 100644 index 0000000000000..d50fc3156867d --- /dev/null +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +var React; +var ReactNoop; + +describe('ReactIncrementalRoot', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactNoop = require('react-noop-renderer'); + }); + + function span(prop) { + return {type: 'span', children: [], prop}; + } + + it('prerenders roots', () => { + const root = ReactNoop.create(); + const work = root.prerender(); + expect(root.getChildren()).toEqual([]); + work.commit(); + expect(root.getChildren()).toEqual([span('A')]); + }); + + it('resolves `then` callback synchronously if tree is already completed', () => { + const root = ReactNoop.create(); + const work = root.prerender(); + ReactNoop.flush(); + let wasCalled = false; + work.then(() => { + wasCalled = true; + }); + expect(wasCalled).toBe(true); + }); + + it('does not restart a completed tree if there were no additional updates', () => { + let ops = []; + function Foo(props) { + ops.push('Foo'); + return ; + } + const root = ReactNoop.create(); + const work = root.prerender(Hi); + + ReactNoop.flush(); + expect(ops).toEqual(['Foo']); + expect(root.getChildren([])); + + work.then(() => { + ops.push('Root completed'); + work.commit(); + ops.push('Root committed'); + }); + + expect(ops).toEqual([ + 'Foo', + 'Root completed', + // Should not re-render Foo + 'Root committed', + ]); + expect(root.getChildren([span('Hi')])); + }); + + it('works on a blocked tree if the expiration time is less than or equal to the blocked update', () => { + let ops = []; + function Foo(props) { + ops.push('Foo: ' + props.children); + return ; + } + const root = ReactNoop.create(); + root.prerender(A); + ReactNoop.flush(); + + expect(ops).toEqual(['Foo: A']); + expect(root.getChildren()).toEqual([]); + + // workA and workB have the same expiration time + root.prerender(B); + ReactNoop.flush(); + + // Should have re-rendered the root, even though it's blocked + // from committing. + expect(ops).toEqual(['Foo: A', 'Foo: B']); + expect(root.getChildren()).toEqual([]); + }); + + it( + 'does not work on on a blocked tree if the expiration time is greater than the blocked update', + ); +}); From e73da46b5486b022837aa347d2dc4f6670d742cf Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 8 Sep 2017 14:18:58 -0700 Subject: [PATCH 17/24] renderer.create -> renderer.createRoot A bit more descriptive --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 2 +- .../dom/shared/__tests__/ReactDOMAsyncRoot-test.js | 4 ++-- src/renderers/noop/ReactNoopEntry.js | 2 +- .../shared/fiber/__tests__/ReactIncrementalRoot-test.js | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 6141d8cc7e745..e524ceb69df51 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -856,7 +856,7 @@ PublicRootNode.prototype.unmount = function(callback) { }; var ReactDOMFiber = { - unstable_create( + unstable_createRoot( container: DOMContainer | (() => DOMContainer), namespace: ?string, ): PublicRoot { diff --git a/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js b/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js index 1f551af91815b..36d822a406737 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js @@ -28,7 +28,7 @@ describe('ReactDOMAsyncRoot', () => { if (ReactDOMFeatureFlags.useFiber) { it('works in easy mode', () => { const container = document.createElement('div'); - const root = ReactDOM.unstable_create(container); + const root = ReactDOM.unstable_createRoot(container); root.render(
Foo
); expect(container.textContent).toEqual('Foo'); root.render(
Bar
); @@ -40,7 +40,7 @@ describe('ReactDOMAsyncRoot', () => { it('can defer commit using prerender', () => { const Async = React.unstable_AsyncComponent; const container = document.createElement('div'); - const root = ReactDOM.unstable_create(container); + const root = ReactDOM.unstable_createRoot(container); const work = root.prerender(Foo); // Hasn't updated yet diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index 4889e8d185ac2..72da9035db089 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -284,7 +284,7 @@ var ReactNoop = { } }, - create(rootID: string) { + createRoot(rootID: string) { rootID = typeof rootID === 'string' ? rootID : DEFAULT_ROOT_ID; invariant( !roots.has(rootID), diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js index d50fc3156867d..1642bbb09c2dd 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js @@ -24,7 +24,7 @@ describe('ReactIncrementalRoot', () => { } it('prerenders roots', () => { - const root = ReactNoop.create(); + const root = ReactNoop.createRoot(); const work = root.prerender(); expect(root.getChildren()).toEqual([]); work.commit(); @@ -32,7 +32,7 @@ describe('ReactIncrementalRoot', () => { }); it('resolves `then` callback synchronously if tree is already completed', () => { - const root = ReactNoop.create(); + const root = ReactNoop.createRoot(); const work = root.prerender(); ReactNoop.flush(); let wasCalled = false; @@ -48,7 +48,7 @@ describe('ReactIncrementalRoot', () => { ops.push('Foo'); return ; } - const root = ReactNoop.create(); + const root = ReactNoop.createRoot(); const work = root.prerender(Hi); ReactNoop.flush(); @@ -76,7 +76,7 @@ describe('ReactIncrementalRoot', () => { ops.push('Foo: ' + props.children); return ; } - const root = ReactNoop.create(); + const root = ReactNoop.createRoot(); root.prerender(A); ReactNoop.flush(); From d3fa7112c1d5bfabbeb54175ad410440c0d8eedd Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 8 Sep 2017 14:19:53 -0700 Subject: [PATCH 18/24] Change optional type to optional parameter --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index e524ceb69df51..6c522a8e4ec24 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -810,7 +810,7 @@ type PublicRoot = { function PublicRootNode( container: DOMContainer | (() => DOMContainer), - namespace: ?string, + namespace?: string, ) { if (typeof container === 'function') { if (typeof namespace !== 'string') { @@ -858,7 +858,7 @@ PublicRootNode.prototype.unmount = function(callback) { var ReactDOMFiber = { unstable_createRoot( container: DOMContainer | (() => DOMContainer), - namespace: ?string, + namespace?: string, ): PublicRoot { return new PublicRootNode(container, namespace); }, From 14cf748b24c9bcb2adb7bcfc3717853d45b2ea45 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 8 Sep 2017 17:00:50 -0700 Subject: [PATCH 19/24] Process completion callbacks immediately after completing a root We need to process completion callbacks in two places. The first is intuitive: right after a root completes. It might seem like that is sufficient. But if a completion callback is scheduled on an already completed root, it's possible we won't complete that root again. So we also need to process completion callbacks whenever we skip over an already completed root. --- .../shared/fiber/ReactFiberScheduler.js | 68 ++++++++++--------- .../__tests__/ReactIncrementalRoot-test.js | 61 ++++++++++++++++- 2 files changed, 93 insertions(+), 36 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 38dfe1f36d786..4d6c262bd4380 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -402,33 +402,10 @@ module.exports = function( // no longer blocked, return the time at which it completed so that we // can commit it. if (isRootBlocked(root, completedAt)) { - // Process pending completion callbacks so that they are called at - // the end of the current batch. - const completionCallbacks = root.completionCallbacks; - if (completionCallbacks !== null) { - processUpdateQueue( - completionCallbacks, - null, - null, - null, - completedAt, - ); - const callbackList = completionCallbacks.callbackList; - if (callbackList !== null) { - // Add new callbacks to list of completion callbacks - if (rootCompletionCallbackList === null) { - rootCompletionCallbackList = callbackList; - } else { - for (let i = 0; i < callbackList.length; i++) { - rootCompletionCallbackList.push(callbackList[i]); - } - } - completionCallbacks.callbackList = null; - if (completionCallbacks.first === null) { - root.completionCallbacks = null; - } - } - } + // We usually process completion callbacks right after a root is + // completed. But this root already completed, and it's possible that + // we received new completion callbacks since then. + processCompletionCallbacks(root, completedAt); return NoWork; } @@ -438,6 +415,33 @@ module.exports = function( return expirationTime; } + function processCompletionCallbacks( + root: FiberRoot, + completedAt: ExpirationTime, + ) { + // Process pending completion callbacks so that they are called at + // the end of the current batch. + const completionCallbacks = root.completionCallbacks; + if (completionCallbacks !== null) { + processUpdateQueue(completionCallbacks, null, null, null, completedAt); + const callbackList = completionCallbacks.callbackList; + if (callbackList !== null) { + // Add new callbacks to list of completion callbacks + if (rootCompletionCallbackList === null) { + rootCompletionCallbackList = callbackList; + } else { + for (let i = 0; i < callbackList.length; i++) { + rootCompletionCallbackList.push(callbackList[i]); + } + } + completionCallbacks.callbackList = null; + if (completionCallbacks.first === null) { + root.completionCallbacks = null; + } + } + } + } + function commitAllHostEffects() { while (nextEffect !== null) { if (__DEV__) { @@ -830,6 +834,7 @@ module.exports = function( // The root is not blocked, so we can commit it now. pendingCommit = workInProgress; } + processCompletionCallbacks(root, nextRenderExpirationTime); return null; } } @@ -1614,12 +1619,9 @@ module.exports = function( } break; case TaskPriority: - invariant( - isBatchingUpdates, - 'Task updates can only be scheduled as a nested update or ' + - 'inside batchedUpdates. This error is likely caused by a ' + - 'bug in React. Please file an issue.', - ); + if (!isPerformingWork && !isBatchingUpdates) { + performWork(TaskPriority, null); + } break; default: // This update is async. Schedule a callback. diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js index 1642bbb09c2dd..1430092152d62 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js @@ -93,7 +93,62 @@ describe('ReactIncrementalRoot', () => { expect(root.getChildren()).toEqual([]); }); - it( - 'does not work on on a blocked tree if the expiration time is greater than the blocked update', - ); + it('does not work on on a blocked tree if the expiration time is greater than the blocked update', () => { + let ops = []; + function Foo(props) { + ops.push('Foo: ' + props.children); + return ; + } + const root = ReactNoop.createRoot(); + root.prerender(A); + ReactNoop.flush(); + + expect(ops).toEqual(['Foo: A']); + expect(root.getChildren()).toEqual([]); + + // workB has a later expiration time + ReactNoop.expire(1000); + root.prerender(B); + ReactNoop.flush(); + + // Should not have re-rendered the root at the later expiration time + expect(ops).toEqual(['Foo: A']); + expect(root.getChildren()).toEqual([]); + }); + + it('commits earlier work without committing later work', () => { + const root = ReactNoop.createRoot(); + const work1 = root.prerender(); + ReactNoop.flush(); + + expect(root.getChildren()).toEqual([]); + + // Second prerender has a later expiration time + ReactNoop.expire(1000); + root.prerender(); + + work1.commit(); + + // Should not have re-rendered the root at the later expiration time + expect(root.getChildren()).toEqual([span('A')]); + }); + + it('flushes ealier work if later work is committed', () => { + let ops = []; + const root = ReactNoop.createRoot(); + const work1 = root.prerender(); + // Second prerender has a later expiration time + ReactNoop.expire(1000); + const work2 = root.prerender(); + + work1.then(() => ops.push('complete 1')); + work2.then(() => ops.push('complete 2')); + + work2.commit(); + + // Because the later prerender was committed, the earlier one should have + // committed, too. + expect(root.getChildren()).toEqual([span('B')]); + expect(ops).toEqual(['complete 1', 'complete 2']); + }); }); From e2abf2e69262d72468d0f81a0a594e5190450f4f Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 8 Sep 2017 17:46:35 -0700 Subject: [PATCH 20/24] Add test for `then` resolving synchronously for sync updates This is the main reason Work is a thenable and not a promise. --- .../dom/shared/__tests__/ReactDOMAsyncRoot-test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js b/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js index 36d822a406737..4747b20ca406c 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js @@ -56,6 +56,18 @@ describe('ReactDOMAsyncRoot', () => { jest.runAllTimers(); }); + + it('resolves `then` callback synchronously if update is sync', () => { + const container = document.createElement('div'); + const root = ReactDOM.unstable_createRoot(container); + const work = root.prerender(
Hi
); + work.then(() => { + work.commit(); + expect(container.textContent).toEqual('Hi'); + }); + // `then` should have synchronously resolved + expect(container.textContent).toEqual('Hi'); + }); } else { it('does not apply to stack'); } From a4be91efb821d9f4281af2dd4ed15830215440ab Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 11 Sep 2017 15:58:13 -0700 Subject: [PATCH 21/24] Committing a update on one root should not flush work on another root `forceExpire` is tracked per root rather than per scheduler. --- .../shared/fiber/ReactFiberReconciler.js | 2 +- src/renderers/shared/fiber/ReactFiberRoot.js | 4 +++ .../shared/fiber/ReactFiberScheduler.js | 24 ++++++++------- .../__tests__/ReactIncrementalRoot-test.js | 30 ++++++++++++++++++- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 2835092aaccb1..ca3569c2308e1 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -387,7 +387,7 @@ module.exports = function( return; } processUpdateQueue(blockers, null, null, null, expirationTime); - expireWork(expirationTime); + expireWork(root, expirationTime); }; WorkNode.prototype.then = function(callback) { const root = this._reactRootContainer; diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js index 4a4a2c92c8b4a..d8e9684e7dba0 100644 --- a/src/renderers/shared/fiber/ReactFiberRoot.js +++ b/src/renderers/shared/fiber/ReactFiberRoot.js @@ -33,6 +33,9 @@ export type FiberRoot = { // A queue of callbacks that fire once their corresponding expiration time // has completed. Only fired once. completionCallbacks: UpdateQueue | null, + // When set, indicates that all work in this tree with this time or earlier + // should be flushed by the end of the batch, as if it has task priority. + forceExpire: null | ExpirationTime, // The work schedule is a linked list. nextScheduledRoot: FiberRoot | null, // Top context object, used by renderSubtreeIntoContainer @@ -63,6 +66,7 @@ exports.createFiberRoot = function(containerInfo: any): FiberRoot { completedAt: NoWork, blockers: null, completionCallbacks: null, + forceExpire: null, nextScheduledRoot: null, context: null, pendingContext: null, diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 4d6c262bd4380..963471baaa795 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -229,9 +229,6 @@ module.exports = function( let nextUnitOfWork: Fiber | null = null; // The time at which we're currently rendering work. let nextRenderExpirationTime: ExpirationTime = NoWork; - // If not null, all work up to and including this time should be - // flushed before the end of the current batch. - let forceExpire: ExpirationTime | null = null; // The next fiber with an effect that we're currently committing. let nextEffect: Fiber | null = null; @@ -311,7 +308,7 @@ module.exports = function( (earliestExpirationTime === NoWork || earliestExpirationTime > rootExpirationTime) ) { - earliestExpirationTime = root.current.expirationTime; + earliestExpirationTime = rootExpirationTime; earliestExpirationRoot = root; } // We didn't find anything to do in this root, so let's try the next one. @@ -1730,8 +1727,15 @@ module.exports = function( } function recalculateCurrentTime(): ExpirationTime { - if (forceExpire !== null) { - return forceExpire; + if (nextRenderedTree !== null) { + // Check if the current root is being force expired. + const forceExpire = nextRenderedTree.forceExpire; + if (forceExpire !== null) { + // Override the current time with the `forceExpire` time. This has the + // effect of expiring all work up to and including that time. + mostRecentCurrentTime = forceExpire; + return forceExpire; + } } // Subtract initial time so it fits inside 32bits const ms = now() - startTime; @@ -1739,18 +1743,16 @@ module.exports = function( return mostRecentCurrentTime; } - function expireWork(expirationTime: ExpirationTime): void { + function expireWork(root: FiberRoot, expirationTime: ExpirationTime): void { invariant( !isPerformingWork, 'Cannot commit while already performing work.', ); - // Override the current time with the given time. This has the effect of - // expiring all work up to and including that time. - forceExpire = mostRecentCurrentTime = expirationTime; + root.forceExpire = expirationTime; try { performWork(TaskPriority, null); } finally { - forceExpire = null; + root.forceExpire = null; recalculateCurrentTime(); } } diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js index 1430092152d62..ec5d126974a67 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalRoot-test.js @@ -133,7 +133,7 @@ describe('ReactIncrementalRoot', () => { expect(root.getChildren()).toEqual([span('A')]); }); - it('flushes ealier work if later work is committed', () => { + it('flushes earlier work if later work is committed', () => { let ops = []; const root = ReactNoop.createRoot(); const work1 = root.prerender(); @@ -151,4 +151,32 @@ describe('ReactIncrementalRoot', () => { expect(root.getChildren()).toEqual([span('B')]); expect(ops).toEqual(['complete 1', 'complete 2']); }); + + it('committing work on one tree does not commit or expire work in a separate tree', () => { + let ops = []; + + const rootA = ReactNoop.createRoot('A'); + const rootB = ReactNoop.createRoot('B'); + + function Foo(props) { + ops.push(props.label); + return ; + } + + // Prerender work on two separate roots + const workA = rootA.prerender(); + rootB.prerender(); + + expect(rootA.getChildren()).toEqual([]); + expect(rootB.getChildren()).toEqual([]); + expect(ops).toEqual([]); + + // Commit root A. This forces the remaining work on root A to expire, but + // should not expire work on root B. + workA.commit(); + + expect(rootA.getChildren()).toEqual([span('A')]); + expect(rootB.getChildren()).toEqual([]); + expect(ops).toEqual(['A']); + }); }); From a8f3335b1b453e318ead460e8ae8bfe318a4d932 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 13 Sep 2017 16:40:43 -0700 Subject: [PATCH 22/24] Add hydration API Hydration should be disabled by default. It's also incompatible with lazy containers, since you can only hydrate a container that has already resolved. After considering these constraints, we came up with this API: createRoot(container: Element, ?{hydrate?: boolean}) createLazyRoot(container: () => Element, ?{namespace?: string, ownerDocument?: Document}) --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 109 ++++++++++++------ .../__tests__/ReactDOMAsyncRoot-test.js | 77 ++++++++++++- .../shared/fiber/ReactFiberBeginWork.js | 1 + src/renderers/shared/fiber/ReactFiberRoot.js | 3 + 4 files changed, 149 insertions(+), 41 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 6c522a8e4ec24..7f10e5ecf3e98 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -91,17 +91,20 @@ ReactControlledComponent.injection.injectFiberControlledHostComponent( type DOMContainer = | (Element & { - _reactRootContainer: ?Object, + _reactRootContainer?: Object | null, }) | (Document & { - _reactRootContainer: ?Object, + _reactRootContainer?: Object | null, }); -type Container = - | Element - | Document - // If the DOM container is lazily provided, the container is the namespace uri - | string; +type LazyContainer = { + namespace: string, + ownerDocument: Document, + getContainer: () => Element | DOMContainer, + _reactRootContainer?: Object | null, +}; + +type Container = DOMContainer | LazyContainer; type Props = { autoFocus?: boolean, @@ -122,10 +125,15 @@ type HostContext = HostContextDev | HostContextProd; let eventsEnabled: ?boolean = null; let selectionInformation: ?mixed = null; +function isLazyContainer(container: Container): boolean { + return typeof (container: any).getContainer === 'function'; +} + function getOwnerDocument(container: Container): Document { let ownerDocument; - if (typeof container === 'string') { - ownerDocument = document; + if (isLazyContainer(container)) { + const lazyContainer: LazyContainer = (container: any); + ownerDocument = lazyContainer.ownerDocument; } else if (container.nodeType === DOCUMENT_NODE) { ownerDocument = (container: any); } else { @@ -134,14 +142,17 @@ function getOwnerDocument(container: Container): Document { return ownerDocument; } -function ensureDOMContainer(container: Container): Element | Document { +function ensureDOMContainer(container: Container): DOMContainer { + if (!isLazyContainer(container)) { + return ((container: any): DOMContainer); + } + const lazyContainer: LazyContainer = (container: any); + const domContainer = lazyContainer.getContainer(); invariant( - typeof container !== 'string', - // TODO: Better error message. Probably should have errored already, when - // validating the result of getContainer. + container !== null && container !== undefined, + // TODO: Better error message. 'Container should have resolved by now', ); - const domContainer: Element | Document = (container: any); return domContainer; } @@ -195,8 +206,9 @@ var DOMRenderer = ReactFiberReconciler({ getRootHostContext(rootContainerInstance: Container): HostContext { let type; let namespace; - if (typeof rootContainerInstance === 'string') { - namespace = rootContainerInstance; + + if (isLazyContainer(rootContainerInstance)) { + namespace = ((rootContainerInstance: any): LazyContainer).namespace; if (__DEV__) { return {namespace, ancestorInfo: null}; } @@ -805,28 +817,21 @@ type PublicRoot = { unmount(callback: ?() => mixed): void, _reactRootContainer: *, - _getComponent: () => DOMContainer, }; -function PublicRootNode( - container: DOMContainer | (() => DOMContainer), +type RootOptions = { + hydrate?: boolean, +}; + +type LazyRootOptions = { namespace?: string, -) { - if (typeof container === 'function') { - if (typeof namespace !== 'string') { - // Default to HTML namespace - namespace = DOMNamespaces.html; - } - this._reactRootContainer = DOMRenderer.createContainer(namespace); - this._getComponent = container; - } else { - // Assume this is a DOM container - const domContainer: DOMContainer = (container: any); - this._reactRootContainer = DOMRenderer.createContainer(domContainer); - this._getComponent = function() { - return domContainer; - }; - } + ownerDocument?: Document, +}; + +function PublicRootNode(container: Container, hydrate: boolean) { + const root = DOMRenderer.createContainer(container); + root.hydrate = hydrate; + this._reactRootContainer = root; } PublicRootNode.prototype.render = function( children: ReactNodeList, @@ -857,10 +862,38 @@ PublicRootNode.prototype.unmount = function(callback) { var ReactDOMFiber = { unstable_createRoot( - container: DOMContainer | (() => DOMContainer), - namespace?: string, + container: DOMContainer, + options?: RootOptions, ): PublicRoot { - return new PublicRootNode(container, namespace); + let hydrate = false; + if (options != null && options.hydrate !== undefined) { + hydrate = options.hydrate; + } + return new PublicRootNode(container, hydrate); + }, + + unstable_createLazyRoot( + getContainer: () => DOMContainer, + options?: LazyRootOptions, + ): PublicRoot { + // Default to HTML namespace + let namespace = DOMNamespaces.html; + // Default to global document + let ownerDocument = document; + if (options != null) { + if (options.namespace != null) { + namespace = options.namespace; + } + if (options.ownerDocument != null) { + ownerDocument = options.ownerDocument; + } + } + const container = { + getContainer, + namespace, + ownerDocument, + }; + return new PublicRootNode(container, false); }, createPortal, diff --git a/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js b/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js index 4747b20ca406c..885a868cd68f7 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js @@ -13,6 +13,7 @@ const ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); let React; let ReactDOM; +let ReactDOMServer; let ReactFeatureFlags; describe('ReactDOMAsyncRoot', () => { @@ -21,6 +22,7 @@ describe('ReactDOMAsyncRoot', () => { React = require('react'); ReactDOM = require('react-dom'); + ReactDOMServer = require('react-dom/server'); ReactFeatureFlags = require('ReactFeatureFlags'); ReactFeatureFlags.enableAsyncSubtreeAPI = true; }); @@ -46,27 +48,96 @@ describe('ReactDOMAsyncRoot', () => { // Hasn't updated yet expect(container.textContent).toEqual(''); + let ops = []; work.then(() => { // Still hasn't updated - expect(container.textContent).toEqual(''); + ops.push(container.textContent); // Should synchronously commit work.commit(); - expect(container.textContent).toEqual('Foo'); + ops.push(container.textContent); }); - + // Flush async work jest.runAllTimers(); + expect(ops).toEqual(['', 'Foo']); }); it('resolves `then` callback synchronously if update is sync', () => { const container = document.createElement('div'); const root = ReactDOM.unstable_createRoot(container); const work = root.prerender(
Hi
); + + let ops = []; work.then(() => { work.commit(); + ops.push(container.textContent); expect(container.textContent).toEqual('Hi'); }); // `then` should have synchronously resolved + expect(ops).toEqual(['Hi']); + }); + + it('supports hydration', async () => { + const markup = await new Promise(resolve => + resolve( + ReactDOMServer.renderToString(
), + ), + ); + + spyOn(console, 'error'); + + // Does not hydrate by default + const container1 = document.createElement('div'); + container1.innerHTML = markup; + const root1 = ReactDOM.unstable_createRoot(container1); + root1.render(
); + expect(console.error.calls.count()).toBe(0); + + // Accepts `hydrate` option + const container2 = document.createElement('div'); + container2.innerHTML = markup; + const root2 = ReactDOM.unstable_createRoot(container2, {hydrate: true}); + root2.render(
); + expect(console.error.calls.count()).toBe(1); + expect(console.error.calls.argsFor(0)[0]).toMatch('Extra attributes'); + }); + + it('supports lazy containers', () => { + let ops = []; + function Foo(props) { + ops.push('Foo'); + return props.children; + } + + let container; + const root = ReactDOM.unstable_createLazyRoot(() => container); + const work = root.prerender(Hi); + expect(ops).toEqual(['Foo']); + + // Set container + container = document.createElement('div'); + + ops = []; + + work.commit(); expect(container.textContent).toEqual('Hi'); + // Should not have re-rendered Foo + expect(ops).toEqual([]); + }); + + it('can specify namespace of a lazy container', () => { + const namespace = 'http://www.w3.org/2000/svg'; + + let container; + const root = ReactDOM.unstable_createLazyRoot(() => container, { + namespace, + }); + const work = root.prerender(); + + // Set container + container = document.createElementNS(namespace, 'svg'); + work.commit(); + // Child should have svg namespace + expect(container.firstChild.namespaceURI).toBe(namespace); }); } else { it('does not apply to stack'); diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 8a050db1446b0..66e748d1b8b2e 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -362,6 +362,7 @@ module.exports = function( } const element = state.element; if ( + root.hydrate && (current === null || current.child === null) && enterHydrationState(workInProgress) ) { diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js index d8e9684e7dba0..d7794e8f390d6 100644 --- a/src/renderers/shared/fiber/ReactFiberRoot.js +++ b/src/renderers/shared/fiber/ReactFiberRoot.js @@ -41,6 +41,8 @@ export type FiberRoot = { // Top context object, used by renderSubtreeIntoContainer context: Object | null, pendingContext: Object | null, + // Determines if we should attempt to hydrate on the initial mount + hydrate: boolean, }; exports.isRootBlocked = function( @@ -70,6 +72,7 @@ exports.createFiberRoot = function(containerInfo: any): FiberRoot { nextScheduledRoot: null, context: null, pendingContext: null, + hydrate: true, }; uninitializedFiber.stateNode = root; return root; From 9a2ac612c4b1c552b950783c0e9d82fb28bf0168 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 4 Oct 2017 15:01:20 -0700 Subject: [PATCH 23/24] Split scheduleUpdate into scheduleUpdate and scheduleWork. scheduleWork updates the priority of the fiber and its ancestors and schedules work to be performed. scheduleUpdate inserts an update into a fiber's update queue and calls then schedules work with scheduleWork. Now we don't have to export so many things from the scheduler. --- .../shared/fiber/ReactFiberBeginWork.js | 25 ++---- .../shared/fiber/ReactFiberClassComponent.js | 78 +++---------------- .../shared/fiber/ReactFiberReconciler.js | 4 +- .../shared/fiber/ReactFiberScheduler.js | 43 +++++++--- 4 files changed, 53 insertions(+), 97 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 66e748d1b8b2e..2d890408b5e0c 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -16,7 +16,6 @@ import type {HostContext} from 'ReactFiberHostContext'; import type {HydrationContext} from 'ReactFiberHydrationContext'; import type {FiberRoot} from 'ReactFiberRoot'; import type {HostConfig} from 'ReactFiberReconciler'; -import type {PriorityLevel} from 'ReactPriorityLevel'; import type {ExpirationTime} from 'ReactFiberExpirationTime'; var { @@ -72,16 +71,13 @@ module.exports = function( config: HostConfig, hostContext: HostContext, hydrationContext: HydrationContext, - scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void, - getPriorityContext: ( + scheduleUpdate: ( fiber: Fiber, - forceAsync: boolean, - ) => PriorityLevel | null, - recalculateCurrentTime: () => ExpirationTime, - getExpirationTimeForPriority: ( - currentTime: ExpirationTime, - priorityLevel: PriorityLevel | null, - ) => ExpirationTime, + partialState: mixed, + callback: (() => mixed) | null, + isReplace: boolean, + isForced: boolean, + ) => void, ) { const { shouldSetTextContent, @@ -103,14 +99,7 @@ module.exports = function( mountClassInstance, // resumeMountClassInstance, updateClassInstance, - } = ReactFiberClassComponent( - scheduleUpdate, - getPriorityContext, - memoizeProps, - memoizeState, - recalculateCurrentTime, - getExpirationTimeForPriority, - ); + } = ReactFiberClassComponent(scheduleUpdate, memoizeProps, memoizeState); // TODO: Remove this and use reconcileChildrenAtExpirationTime directly. function reconcileChildren(current, workInProgress, nextChildren) { diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 864034d51e5a2..6b7cf7f8812d5 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -11,7 +11,6 @@ 'use strict'; import type {Fiber} from 'ReactFiber'; -import type {PriorityLevel} from 'ReactPriorityLevel'; import type {ExpirationTime} from 'ReactFiberExpirationTime'; var {Update} = require('ReactTypeOfSideEffect'); @@ -25,10 +24,7 @@ var { getUnmaskedContext, isContextConsumer, } = require('ReactFiberContext'); -var { - insertUpdateIntoFiber, - beginUpdateQueue, -} = require('ReactFiberUpdateQueue'); +var {beginUpdateQueue} = require('ReactFiberUpdateQueue'); var {hasContextChanged} = require('ReactFiberContext'); var {isMounted} = require('ReactFiberTreeReflection'); var ReactInstanceMap = require('ReactInstanceMap'); @@ -76,96 +72,42 @@ if (__DEV__) { } module.exports = function( - scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void, - getPriorityContext: ( + scheduleUpdate: ( fiber: Fiber, - forceAsync: boolean, - ) => PriorityLevel | null, + partialState: mixed, + callback: (() => mixed) | null, + isReplace: boolean, + isForced: boolean, + ) => void, memoizeProps: (workInProgress: Fiber, props: any) => void, memoizeState: (workInProgress: Fiber, state: any) => void, - recalculateCurrentTime: () => ExpirationTime, - getExpirationTimeForPriority: ( - currentTime: ExpirationTime, - priorityLevel: PriorityLevel | null, - ) => ExpirationTime, ) { // Class component state updater const updater = { isMounted, enqueueSetState(instance, partialState, callback) { const fiber = ReactInstanceMap.get(instance); - const priorityLevel = getPriorityContext(fiber, false); - const currentTime = recalculateCurrentTime(); - const expirationTime = getExpirationTimeForPriority( - currentTime, - priorityLevel, - ); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'setState'); } - const update = { - priorityLevel, - expirationTime, - partialState, - callback, - isReplace: false, - isForced: false, - isTopLevelUnmount: false, - next: null, - }; - insertUpdateIntoFiber(fiber, update, currentTime); - scheduleUpdate(fiber, expirationTime); + scheduleUpdate(fiber, partialState, callback, false, false); }, enqueueReplaceState(instance, state, callback) { const fiber = ReactInstanceMap.get(instance); - const priorityLevel = getPriorityContext(fiber, false); - const currentTime = recalculateCurrentTime(); - const expirationTime = getExpirationTimeForPriority( - currentTime, - priorityLevel, - ); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'replaceState'); } - const update = { - priorityLevel, - expirationTime, - partialState: state, - callback, - isReplace: true, - isForced: false, - isTopLevelUnmount: false, - next: null, - }; - insertUpdateIntoFiber(fiber, update, currentTime); - scheduleUpdate(fiber, expirationTime); + scheduleUpdate(fiber, state, callback, true, false); }, enqueueForceUpdate(instance, callback) { const fiber = ReactInstanceMap.get(instance); - const priorityLevel = getPriorityContext(fiber, false); - const currentTime = recalculateCurrentTime(); - const expirationTime = getExpirationTimeForPriority( - currentTime, - priorityLevel, - ); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'forceUpdate'); } - const update = { - priorityLevel, - expirationTime, - partialState: null, - callback, - isReplace: false, - isForced: true, - isTopLevelUnmount: false, - next: null, - }; - insertUpdateIntoFiber(fiber, update, currentTime); - scheduleUpdate(fiber, expirationTime); + scheduleUpdate(fiber, null, callback, false, true); }, }; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index ca3569c2308e1..114c0ffbe9c18 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -257,7 +257,7 @@ module.exports = function( var {getPublicInstance} = config; var { - scheduleUpdate, + scheduleWork, scheduleCompletionCallback, getPriorityContext, getExpirationTimeForPriority, @@ -371,7 +371,7 @@ module.exports = function( insertUpdateIntoQueue(root.blockers, block, currentTime); } - scheduleUpdate(current, expirationTime); + scheduleWork(current, expirationTime); return expirationTime; } diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 963471baaa795..e5e0fef912597 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -97,6 +97,7 @@ var { processUpdateQueue, createUpdateQueue, insertUpdateIntoQueue, + insertUpdateIntoFiber, } = require('ReactFiberUpdateQueue'); var {resetContext} = require('ReactFiberContext'); @@ -179,9 +180,6 @@ module.exports = function( hostContext, hydrationContext, scheduleUpdate, - getPriorityContext, - recalculateCurrentTime, - getExpirationTimeForPriority, ); const {completeWork} = ReactFiberCompleteWork( config, @@ -1530,11 +1528,38 @@ module.exports = function( } } - function scheduleUpdate(fiber: Fiber, expirationTime: ExpirationTime) { - return scheduleUpdateImpl(fiber, expirationTime, false); + function scheduleUpdate( + fiber: Fiber, + partialState: mixed, + callback: (() => mixed) | null, + isReplace: boolean, + isForced: boolean, + ) { + const priorityLevel = getPriorityContext(fiber, false); + const currentTime = recalculateCurrentTime(); + const expirationTime = getExpirationTimeForPriority( + currentTime, + priorityLevel, + ); + const update = { + priorityLevel, + expirationTime, + partialState, + callback, + isReplace, + isForced, + nextCallback: null, + next: null, + }; + insertUpdateIntoFiber(fiber, update, currentTime); + scheduleWork(fiber, expirationTime); + } + + function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) { + return scheduleWorkImpl(fiber, expirationTime, false); } - function scheduleUpdateImpl( + function scheduleWorkImpl( fiber: Fiber, expirationTime: ExpirationTime, isErrorRecovery: boolean, @@ -1668,7 +1693,7 @@ module.exports = function( // callback synchronously. performWork(TaskPriority, null); } else { - scheduleUpdate(root.current, expirationTime); + scheduleWork(root.current, expirationTime); } } @@ -1723,7 +1748,7 @@ module.exports = function( mostRecentCurrentTime, TaskPriority, ); - scheduleUpdateImpl(fiber, taskTime, true); + scheduleWorkImpl(fiber, taskTime, true); } function recalculateCurrentTime(): ExpirationTime { @@ -1817,7 +1842,7 @@ module.exports = function( } return { - scheduleUpdate: scheduleUpdate, + scheduleWork: scheduleWork, scheduleCompletionCallback: scheduleCompletionCallback, getPriorityContext: getPriorityContext, recalculateCurrentTime: recalculateCurrentTime, From 929705256784ca690072c418ecc0043959c1ff16 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 5 Oct 2017 17:41:23 -0700 Subject: [PATCH 24/24] Simplify top-level blockers After thinking about how to implement blockers in general, I figured out how to simplify top-level blockers, too. --- .../shared/fiber/ReactFiberCompleteWork.js | 8 +- .../shared/fiber/ReactFiberReconciler.js | 12 +- src/renderers/shared/fiber/ReactFiberRoot.js | 24 ++-- .../shared/fiber/ReactFiberScheduler.js | 122 ++++++------------ 4 files changed, 64 insertions(+), 102 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index edb6048e91338..0aaa44f0f579d 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -18,6 +18,7 @@ import type {HydrationContext} from 'ReactFiberHydrationContext'; import type {FiberRoot} from 'ReactFiberRoot'; import type {HostConfig} from 'ReactFiberReconciler'; +var {topLevelBlockedAt} = require('ReactFiberRoot'); var {reconcileChildFibers} = require('ReactChildFiber'); var { popContextProvider, @@ -40,7 +41,7 @@ var { Fragment, } = ReactTypeOfWork; var {Placement, Ref, Update} = ReactTypeOfSideEffect; -var {Never} = ReactFiberExpirationTime; +var {NoWork, Never} = ReactFiberExpirationTime; var invariant = require('fbjs/lib/invariant'); @@ -219,6 +220,11 @@ module.exports = function( // TODO: Delete this when we delete isMounted and findDOMNode. workInProgress.effectTag &= ~Placement; } + + // Check if the root is blocked by a top-level update. + const blockedAt = topLevelBlockedAt(fiberRoot); + fiberRoot.isBlocked = + blockedAt !== NoWork && blockedAt <= renderExpirationTime; return null; } case HostComponent: { diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 114c0ffbe9c18..e19b7db48bc9c 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -355,8 +355,8 @@ module.exports = function( if (isPrerender) { // Block the root from committing at this expiration time. - if (root.blockers === null) { - root.blockers = createUpdateQueue(); + if (root.topLevelBlockers === null) { + root.topLevelBlockers = createUpdateQueue(); } const block = { priorityLevel: null, @@ -368,7 +368,7 @@ module.exports = function( isTopLevelUnmount: false, next: null, }; - insertUpdateIntoQueue(root.blockers, block, currentTime); + insertUpdateIntoQueue(root.topLevelBlockers, block, currentTime); } scheduleWork(current, expirationTime); @@ -382,11 +382,11 @@ module.exports = function( WorkNode.prototype.commit = function() { const root = this._reactRootContainer; const expirationTime = this._expirationTime; - const blockers = root.blockers; - if (blockers === null) { + const topLevelBlockers = root.topLevelBlockers; + if (topLevelBlockers === null) { return; } - processUpdateQueue(blockers, null, null, null, expirationTime); + processUpdateQueue(topLevelBlockers, null, null, null, expirationTime); expireWork(root, expirationTime); }; WorkNode.prototype.then = function(callback) { diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js index d7794e8f390d6..775f3b0919dc0 100644 --- a/src/renderers/shared/fiber/ReactFiberRoot.js +++ b/src/renderers/shared/fiber/ReactFiberRoot.js @@ -25,11 +25,14 @@ export type FiberRoot = { current: Fiber, // Determines if this root has already been added to the schedule for work. isScheduled: boolean, + // A queue that represents times at which the root is blocked by a + // top-level update. + topLevelBlockers: UpdateQueue | null, // The time at which this root completed. completedAt: ExpirationTime, - // A queue that represents times at which this root is blocked + // If this root completed, isBlocked indicates whether it's blocked // from committing. - blockers: UpdateQueue | null, + isBlocked: boolean, // A queue of callbacks that fire once their corresponding expiration time // has completed. Only fired once. completionCallbacks: UpdateQueue | null, @@ -45,16 +48,12 @@ export type FiberRoot = { hydrate: boolean, }; -exports.isRootBlocked = function( - root: FiberRoot, - expirationTime: ExpirationTime, -) { - const blockers = root.blockers; - if (blockers === null) { - return false; +exports.topLevelBlockedAt = function(root: FiberRoot) { + const topLevelBlockers = root.topLevelBlockers; + if (topLevelBlockers === null) { + return NoWork; } - const blockedAt = getUpdateQueueExpirationTime(blockers); - return blockedAt !== NoWork && blockedAt <= expirationTime; + return getUpdateQueueExpirationTime(topLevelBlockers); }; exports.createFiberRoot = function(containerInfo: any): FiberRoot { @@ -66,8 +65,9 @@ exports.createFiberRoot = function(containerInfo: any): FiberRoot { containerInfo: containerInfo, isScheduled: false, completedAt: NoWork, - blockers: null, + isBlocked: false, completionCallbacks: null, + topLevelBlockers: null, forceExpire: null, nextScheduledRoot: null, context: null, diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index e5e0fef912597..689f0aaf19ffd 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -52,7 +52,6 @@ var {ReactCurrentOwner} = require('ReactGlobalSharedState'); var getComponentName = require('getComponentName'); var {createWorkInProgress} = require('ReactFiber'); -var {isRootBlocked} = require('ReactFiberRoot'); var {onCommitRoot} = require('ReactFiberDevToolsHook'); var { @@ -335,12 +334,15 @@ module.exports = function( 'is likely caused by a bug in React. Please file an issue.', ); } else { - earliestExpirationRoot.completedAt = NoWork; nextUnitOfWork = createWorkInProgress( earliestExpirationRoot.current, earliestExpirationTime, ); } + + earliestExpirationRoot.completedAt = NoWork; + earliestExpirationRoot.isBlocked = false; + if (earliestExpirationRoot !== nextRenderedTree) { // We've switched trees. Reset the nested update counter. nestedUpdateCount = 0; @@ -360,53 +362,18 @@ module.exports = function( // TODO: Find a better name for this function. It also schedules completion // callbacks, if a root is blocked. function shouldWorkOnRoot(root: FiberRoot): ExpirationTime { - const completedAt = root.completedAt; const expirationTime = root.current.expirationTime; - if (expirationTime === NoWork) { // There's no work in this tree. return NoWork; } - - if (completedAt !== NoWork) { - // The root completed but was blocked from committing. - if (expirationTime < completedAt) { - // We have work that expires earlier than the completed root. - return expirationTime; - } - - // If the expiration time of the pending work is equal to the time at - // which we completed the work-in-progress, it's possible additional - // work was scheduled that happens to fall within the same expiration - // bucket. We need to check the work-in-progress fiber. - if (expirationTime === completedAt) { - const workInProgress = root.current.alternate; - if ( - workInProgress !== null && - (workInProgress.expirationTime !== NoWork && - workInProgress.expirationTime <= expirationTime) - ) { - // We have more work. Restart the completed tree. - root.completedAt = NoWork; - return expirationTime; - } - } - - // There have been no higher priority updates since we completed the root. - // If it's still blocked, return NoWork, as if it has no more work. If it's - // no longer blocked, return the time at which it completed so that we - // can commit it. - if (isRootBlocked(root, completedAt)) { - // We usually process completion callbacks right after a root is - // completed. But this root already completed, and it's possible that - // we received new completion callbacks since then. - processCompletionCallbacks(root, completedAt); - return NoWork; - } - - return completedAt; + if (root.isBlocked) { + // We usually process completion callbacks right after a root is + // completed. But this root already completed, and it's possible that + // we received new completion callbacks since then. + processCompletionCallbacks(root, root.completedAt); + return NoWork; } - return expirationTime; } @@ -817,16 +784,12 @@ module.exports = function( workInProgress = returnFiber; continue; } else { + // We've reached the root. Mark it as complete. const root = workInProgress.stateNode; - // We've reached the root. Mark the root as complete. Depending on how - // much time we have left, we'll either commit it now or in the - // next frame. - if (isRootBlocked(root, nextRenderExpirationTime)) { - // The root is blocked from committing. Mark it as complete so we - // know we can commit it later without starting new work. - root.completedAt = nextRenderExpirationTime; - } else { - // The root is not blocked, so we can commit it now. + root.completedAt = nextRenderExpirationTime; + // If the root isn't blocked, it's ready to commit. If it is blocked, + // we'll come back to it later. + if (!root.isBlocked) { pendingCommit = workInProgress; } processCompletionCallbacks(root, nextRenderExpirationTime); @@ -1509,25 +1472,6 @@ module.exports = function( } } - function scheduleRoot(root: FiberRoot, expirationTime: ExpirationTime) { - if (expirationTime === NoWork) { - return; - } - - if (!root.isScheduled) { - root.isScheduled = true; - if (lastScheduledRoot) { - // Schedule ourselves to the end. - lastScheduledRoot.nextScheduledRoot = root; - lastScheduledRoot = root; - } else { - // We're the only work scheduled. - nextScheduledRoot = root; - lastScheduledRoot = root; - } - } - } - function scheduleUpdate( fiber: Fiber, partialState: mixed, @@ -1549,6 +1493,7 @@ module.exports = function( isReplace, isForced, nextCallback: null, + isTopLevelUnmount: false, next: null, }; insertUpdateIntoFiber(fiber, update, currentTime); @@ -1594,19 +1539,11 @@ module.exports = function( } let node = fiber; - let shouldContinue = true; - while (node !== null && shouldContinue) { - // Walk the parent path to the root and update each node's expiration - // time. Once we reach a node whose expiration matches (and whose - // alternate's expiration matches) we can exit safely knowing that the - // rest of the path is correct. - shouldContinue = false; + while (node !== null) { if ( node.expirationTime === NoWork || node.expirationTime > expirationTime ) { - // Expiration time did not match. Update and keep going. - shouldContinue = true; node.expirationTime = expirationTime; } if (node.alternate !== null) { @@ -1614,15 +1551,33 @@ module.exports = function( node.alternate.expirationTime === NoWork || node.alternate.expirationTime > expirationTime ) { - // Expiration time did not match. Update and keep going. - shouldContinue = true; node.alternate.expirationTime = expirationTime; } } if (node.return === null) { if (node.tag === HostRoot) { const root: FiberRoot = (node.stateNode: any); - scheduleRoot(root, expirationTime); + + // Add the root to the work schedule. + if (expirationTime !== NoWork) { + root.isBlocked = false; + if (!root.isScheduled) { + root.isScheduled = true; + if (lastScheduledRoot) { + // Schedule ourselves to the end. + lastScheduledRoot.nextScheduledRoot = root; + lastScheduledRoot = root; + } else { + // We're the only work scheduled. + nextScheduledRoot = root; + lastScheduledRoot = root; + } + } + } + + // If we're not current performing work, we need to either start + // working now (if the update is synchronous) or schedule a callback + // to perform work later. if (!isPerformingWork) { const priorityLevel = expirationTimeToPriorityLevel( mostRecentCurrentTime, @@ -1774,6 +1729,7 @@ module.exports = function( 'Cannot commit while already performing work.', ); root.forceExpire = expirationTime; + root.isBlocked = false; try { performWork(TaskPriority, null); } finally {