From 93e078ddf274636b0a40bd5501ce3549aec700fa Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sat, 2 May 2020 17:09:31 -0700 Subject: [PATCH] Initial Lanes implementation (#18796) See PR #18796 for more information. All of the changes I've made in this commit are behind the `enableNewReconciler` flag. Merging this to master will not affect the open source builds or the build that we ship to Facebook. The only build that is affected is the `ReactDOMForked` build, which is deployed to Facebook **behind an experimental flag (currently disabled for all users)**. We will use this flag to gradually roll out the new reconciler, and quickly roll it back if we find any problems. Because we have those protections in place, what I'm aiming for with this initial PR is the **smallest possible atomic change that lands cleanly and doesn't rely on too many hacks**. The goal has not been to get every single test or feature passing, and it definitely is not to implement all the features that we intend to build on top of the new model. When possible, I have chosen to preserve existing semantics and defer changes to follow-up steps. (Listed in the section below.) (I did not end up having to disable any tests, although if I had, that should not have necessarily been a merge blocker.) For example, even though one of the primary goals of this project is to improve our model for parallel Suspense transitions, in this initial implementation, I have chosen to keep the same core heuristics for sequencing and flushing that existed in the ExpirationTimes model: low priority updates cannot finish without also finishing high priority ones. Despite all these precautions, **because the scope of this refactor is inherently large, I do expect we will find regressions.** The flip side is that I also expect the new model to improve the stability of the codebase and make it easier to fix bugs when they arise. --- .../ReactDOMServerIntegrationHooks-test.js | 103 +- .../src/ReactChildFiber.new.js | 176 +-- .../react-reconciler/src/ReactFiber.new.js | 120 +- .../src/ReactFiberBeginWork.new.js | 812 ++++------- .../src/ReactFiberClassComponent.new.js | 50 +- .../src/ReactFiberCommitWork.new.js | 4 +- .../src/ReactFiberCompleteWork.new.js | 25 +- .../src/ReactFiberDeprecatedEvents.new.js | 4 +- .../src/ReactFiberExpirationTime.new.js | 92 -- .../src/ReactFiberHooks.new.js | 145 +- .../src/ReactFiberHotReloading.new.js | 5 +- .../src/ReactFiberHydrationContext.new.js | 4 +- .../react-reconciler/src/ReactFiberLane.js | 692 +++++++++- .../src/ReactFiberNewContext.new.js | 115 +- .../src/ReactFiberOffscreenComponent.js | 6 +- .../src/ReactFiberReconciler.new.js | 80 +- .../src/ReactFiberRoot.new.js | 180 +-- .../src/ReactFiberSuspenseComponent.new.js | 11 +- .../src/ReactFiberThrow.new.js | 78 +- .../src/ReactFiberUnwindWork.new.js | 13 +- .../src/ReactFiberWorkLoop.new.js | 1187 +++++++---------- .../src/ReactInternalTypes.js | 36 +- .../src/ReactMutableSource.new.js | 45 - .../src/ReactUpdateQueue.new.js | 43 +- .../__tests__/ReactIncrementalUpdates-test.js | 126 +- .../__tests__/ReactSuspense-test.internal.js | 25 +- .../ReactSuspenseCallback-test.internal.js | 45 +- scripts/error-codes/codes.json | 5 +- 28 files changed, 1956 insertions(+), 2271 deletions(-) delete mode 100644 packages/react-reconciler/src/ReactFiberExpirationTime.new.js diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js index 65e4188dc9c4..c087ec93765c 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js @@ -1306,44 +1306,25 @@ describe('ReactDOMServerHooks', () => { // State update should trigger the ID to update, which changes the props // of ChildWithID. This should cause ChildWithID to hydrate before Children - gate(flags => { - if (__DEV__) { - expect(Scheduler).toFlushAndYieldThrough([ - 'Child with ID', - // Fallbacks are immdiately committed in TestUtils version - // of act - // 'Child with ID', - // 'Child with ID', - 'Child One', - 'Child Two', - ]); - } else if (flags.new) { - // Upgrading a dehyrdating boundary works a little differently in - // the new reconciler. After the update on the boundary is - // scheduled, it waits until the end of the current time slice - // before restarting at the higher priority. - expect(Scheduler).toFlushAndYieldThrough([ - 'Child with ID', - 'Child with ID', - 'Child with ID', - 'Child with ID', - 'Child One', - 'Child Two', - ]); - } else { - // Whereas the old reconciler relies on a Scheduler hack to - // interrupt the current task. It's not clear if this is any - // better or worse, though. Regardless it's not a big deal since - // the time slices aren't that big. - expect(Scheduler).toFlushAndYieldThrough([ - 'Child with ID', - 'Child with ID', - 'Child with ID', - 'Child One', - 'Child Two', - ]); - } - }); + expect(Scheduler).toFlushAndYieldThrough( + __DEV__ + ? [ + 'Child with ID', + // Fallbacks are immediately committed in TestUtils version + // of act + // 'Child with ID', + // 'Child with ID', + 'Child One', + 'Child Two', + ] + : [ + 'Child with ID', + 'Child with ID', + 'Child with ID', + 'Child One', + 'Child Two', + ], + ); expect(child1Ref.current).toBe(null); expect(childWithIDRef.current).toEqual( @@ -1691,15 +1672,24 @@ describe('ReactDOMServerHooks', () => { ReactDOM.createRoot(container, {hydrate: true}).render(); - expect(() => - expect(() => Scheduler.unstable_flushAll()).toThrow( + if (gate(flags => flags.new)) { + expect(() => Scheduler.unstable_flushAll()).toErrorDev([ 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + 'Do not read the value directly.', - ), - ).toErrorDev([ - 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + - 'Do not read the value directly.', - ]); + ]); + } else { + // In the old reconciler, the error isn't surfaced to the user. That + // part isn't important, as long as It warns. + expect(() => + expect(() => Scheduler.unstable_flushAll()).toThrow( + 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + + 'Do not read the value directly.', + ), + ).toErrorDev([ + 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + + 'Do not read the value directly.', + ]); + } }); it('useOpaqueIdentifier throws if you try to add the result as a number in a child component wrapped in a Suspense', async () => { @@ -1724,15 +1714,24 @@ describe('ReactDOMServerHooks', () => { ReactDOM.createRoot(container, {hydrate: true}).render(); - expect(() => - expect(() => Scheduler.unstable_flushAll()).toThrow( + if (gate(flags => flags.new)) { + expect(() => Scheduler.unstable_flushAll()).toErrorDev([ 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + 'Do not read the value directly.', - ), - ).toErrorDev([ - 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + - 'Do not read the value directly.', - ]); + ]); + } else { + // In the old reconciler, the error isn't surfaced to the user. That + // part isn't important, as long as It warns. + expect(() => + expect(() => Scheduler.unstable_flushAll()).toThrow( + 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + + 'Do not read the value directly.', + ), + ).toErrorDev([ + 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' + + 'Do not read the value directly.', + ]); + } }); it('useOpaqueIdentifier with two opaque identifiers on the same page', () => { diff --git a/packages/react-reconciler/src/ReactChildFiber.new.js b/packages/react-reconciler/src/ReactChildFiber.new.js index e993e4d571e8..5b0e858d6af9 100644 --- a/packages/react-reconciler/src/ReactChildFiber.new.js +++ b/packages/react-reconciler/src/ReactChildFiber.new.js @@ -12,7 +12,7 @@ import type {ReactPortal} from 'shared/ReactTypes'; import type {BlockComponent} from 'react/src/ReactBlock'; import type {LazyComponent} from 'react/src/ReactLazy'; import type {Fiber} from './ReactInternalTypes'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; +import type {Lanes} from './ReactFiberLane'; import getComponentName from 'shared/getComponentName'; import {Placement, Deletion} from './ReactSideEffectTags'; @@ -377,15 +377,11 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, textContent: string, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ) { if (current === null || current.tag !== HostText) { // Insert - const created = createFiberFromText( - textContent, - returnFiber.mode, - expirationTime, - ); + const created = createFiberFromText(textContent, returnFiber.mode, lanes); created.return = returnFiber; return created; } else { @@ -400,7 +396,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, element: ReactElement, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ): Fiber { if (current !== null) { if ( @@ -442,11 +438,7 @@ function ChildReconciler(shouldTrackSideEffects) { } } // Insert - const created = createFiberFromElement( - element, - returnFiber.mode, - expirationTime, - ); + const created = createFiberFromElement(element, returnFiber.mode, lanes); created.ref = coerceRef(returnFiber, current, element); created.return = returnFiber; return created; @@ -456,7 +448,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, portal: ReactPortal, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ): Fiber { if ( current === null || @@ -465,11 +457,7 @@ function ChildReconciler(shouldTrackSideEffects) { current.stateNode.implementation !== portal.implementation ) { // Insert - const created = createFiberFromPortal( - portal, - returnFiber.mode, - expirationTime, - ); + const created = createFiberFromPortal(portal, returnFiber.mode, lanes); created.return = returnFiber; return created; } else { @@ -484,7 +472,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, fragment: Iterable<*>, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, key: null | string, ): Fiber { if (current === null || current.tag !== Fragment) { @@ -492,7 +480,7 @@ function ChildReconciler(shouldTrackSideEffects) { const created = createFiberFromFragment( fragment, returnFiber.mode, - expirationTime, + lanes, key, ); created.return = returnFiber; @@ -508,7 +496,7 @@ function ChildReconciler(shouldTrackSideEffects) { function createChild( returnFiber: Fiber, newChild: any, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ): Fiber | null { if (typeof newChild === 'string' || typeof newChild === 'number') { // Text nodes don't have keys. If the previous node is implicitly keyed @@ -517,7 +505,7 @@ function ChildReconciler(shouldTrackSideEffects) { const created = createFiberFromText( '' + newChild, returnFiber.mode, - expirationTime, + lanes, ); created.return = returnFiber; return created; @@ -529,7 +517,7 @@ function ChildReconciler(shouldTrackSideEffects) { const created = createFiberFromElement( newChild, returnFiber.mode, - expirationTime, + lanes, ); created.ref = coerceRef(returnFiber, null, newChild); created.return = returnFiber; @@ -539,7 +527,7 @@ function ChildReconciler(shouldTrackSideEffects) { const created = createFiberFromPortal( newChild, returnFiber.mode, - expirationTime, + lanes, ); created.return = returnFiber; return created; @@ -550,7 +538,7 @@ function ChildReconciler(shouldTrackSideEffects) { const created = createFiberFromFragment( newChild, returnFiber.mode, - expirationTime, + lanes, null, ); created.return = returnFiber; @@ -573,7 +561,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber: Fiber, oldFiber: Fiber | null, newChild: any, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ): Fiber | null { // Update the fiber if the keys match, otherwise return null. @@ -586,12 +574,7 @@ function ChildReconciler(shouldTrackSideEffects) { if (key !== null) { return null; } - return updateTextNode( - returnFiber, - oldFiber, - '' + newChild, - expirationTime, - ); + return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes); } if (typeof newChild === 'object' && newChild !== null) { @@ -603,28 +586,18 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber, oldFiber, newChild.props.children, - expirationTime, + lanes, key, ); } - return updateElement( - returnFiber, - oldFiber, - newChild, - expirationTime, - ); + return updateElement(returnFiber, oldFiber, newChild, lanes); } else { return null; } } case REACT_PORTAL_TYPE: { if (newChild.key === key) { - return updatePortal( - returnFiber, - oldFiber, - newChild, - expirationTime, - ); + return updatePortal(returnFiber, oldFiber, newChild, lanes); } else { return null; } @@ -636,13 +609,7 @@ function ChildReconciler(shouldTrackSideEffects) { return null; } - return updateFragment( - returnFiber, - oldFiber, - newChild, - expirationTime, - null, - ); + return updateFragment(returnFiber, oldFiber, newChild, lanes, null); } throwOnInvalidObjectType(returnFiber, newChild); @@ -662,18 +629,13 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber: Fiber, newIdx: number, newChild: any, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ): Fiber | null { if (typeof newChild === 'string' || typeof newChild === 'number') { // Text nodes don'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, - expirationTime, - ); + return updateTextNode(returnFiber, matchedFiber, '' + newChild, lanes); } if (typeof newChild === 'object' && newChild !== null) { @@ -688,40 +650,24 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber, matchedFiber, newChild.props.children, - expirationTime, + lanes, newChild.key, ); } - return updateElement( - returnFiber, - matchedFiber, - newChild, - expirationTime, - ); + return updateElement(returnFiber, matchedFiber, newChild, lanes); } case REACT_PORTAL_TYPE: { const matchedFiber = existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; - return updatePortal( - returnFiber, - matchedFiber, - newChild, - expirationTime, - ); + return updatePortal(returnFiber, matchedFiber, newChild, lanes); } } if (isArray(newChild) || getIteratorFn(newChild)) { const matchedFiber = existingChildren.get(newIdx) || null; - return updateFragment( - returnFiber, - matchedFiber, - newChild, - expirationTime, - null, - ); + return updateFragment(returnFiber, matchedFiber, newChild, lanes, null); } throwOnInvalidObjectType(returnFiber, newChild); @@ -785,7 +731,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, newChildren: Array<*>, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ): Fiber | null { // This algorithm can't optimize by searching from both ends since we // don't have backpointers on fibers. I'm trying to see how far we can get @@ -833,7 +779,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber, oldFiber, newChildren[newIdx], - expirationTime, + lanes, ); if (newFiber === null) { // TODO: This breaks on empty slots like null children. That's @@ -877,11 +823,7 @@ function ChildReconciler(shouldTrackSideEffects) { // If we don't have any more existing children we can choose a fast path // since the rest will all be insertions. for (; newIdx < newChildren.length; newIdx++) { - const newFiber = createChild( - returnFiber, - newChildren[newIdx], - expirationTime, - ); + const newFiber = createChild(returnFiber, newChildren[newIdx], lanes); if (newFiber === null) { continue; } @@ -907,7 +849,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber, newIdx, newChildren[newIdx], - expirationTime, + lanes, ); if (newFiber !== null) { if (shouldTrackSideEffects) { @@ -944,7 +886,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, newChildrenIterable: Iterable<*>, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ): Fiber | null { // This is the same implementation as reconcileChildrenArray(), // but using the iterator instead. @@ -1023,12 +965,7 @@ function ChildReconciler(shouldTrackSideEffects) { } else { nextOldFiber = oldFiber.sibling; } - const newFiber = updateSlot( - returnFiber, - oldFiber, - step.value, - expirationTime, - ); + const newFiber = updateSlot(returnFiber, oldFiber, step.value, lanes); 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 @@ -1071,7 +1008,7 @@ function ChildReconciler(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, expirationTime); + const newFiber = createChild(returnFiber, step.value, lanes); if (newFiber === null) { continue; } @@ -1097,7 +1034,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber, newIdx, step.value, - expirationTime, + lanes, ); if (newFiber !== null) { if (shouldTrackSideEffects) { @@ -1134,7 +1071,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, textContent: string, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ): Fiber { // There's no need to check for keys on text nodes since we don't have a // way to define them. @@ -1149,11 +1086,7 @@ function ChildReconciler(shouldTrackSideEffects) { // The existing first child is not a text node so we need to create one // and delete the existing ones. deleteRemainingChildren(returnFiber, currentFirstChild); - const created = createFiberFromText( - textContent, - returnFiber.mode, - expirationTime, - ); + const created = createFiberFromText(textContent, returnFiber.mode, lanes); created.return = returnFiber; return created; } @@ -1162,7 +1095,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ): Fiber { const key = element.key; let child = currentFirstChild; @@ -1245,17 +1178,13 @@ function ChildReconciler(shouldTrackSideEffects) { const created = createFiberFromFragment( element.props.children, returnFiber.mode, - expirationTime, + lanes, element.key, ); created.return = returnFiber; return created; } else { - const created = createFiberFromElement( - element, - returnFiber.mode, - expirationTime, - ); + const created = createFiberFromElement(element, returnFiber.mode, lanes); created.ref = coerceRef(returnFiber, currentFirstChild, element); created.return = returnFiber; return created; @@ -1266,7 +1195,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, portal: ReactPortal, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ): Fiber { const key = portal.key; let child = currentFirstChild; @@ -1293,11 +1222,7 @@ function ChildReconciler(shouldTrackSideEffects) { child = child.sibling; } - const created = createFiberFromPortal( - portal, - returnFiber.mode, - expirationTime, - ); + const created = createFiberFromPortal(portal, returnFiber.mode, lanes); created.return = returnFiber; return created; } @@ -1309,7 +1234,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, newChild: any, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ): Fiber | null { // This function is not recursive. // If the top level item is an array, we treat it as a set of children, @@ -1339,7 +1264,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - expirationTime, + lanes, ), ); case REACT_PORTAL_TYPE: @@ -1348,7 +1273,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - expirationTime, + lanes, ), ); } @@ -1360,7 +1285,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber, currentFirstChild, '' + newChild, - expirationTime, + lanes, ), ); } @@ -1370,7 +1295,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - expirationTime, + lanes, ); } @@ -1379,7 +1304,7 @@ function ChildReconciler(shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - expirationTime, + lanes, ); } @@ -1462,13 +1387,10 @@ export function cloneChildFibers( } // Reset a workInProgress child set to prepare it for a second pass. -export function resetChildFibers( - workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, -): void { +export function resetChildFibers(workInProgress: Fiber, lanes: Lanes): void { let child = workInProgress.child; while (child !== null) { - resetWorkInProgress(child, renderExpirationTime); + resetWorkInProgress(child, lanes); child = child.sibling; } } diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js index 741083a8650c..b428e11374f4 100644 --- a/packages/react-reconciler/src/ReactFiber.new.js +++ b/packages/react-reconciler/src/ReactFiber.new.js @@ -18,7 +18,7 @@ import type {Fiber} from './ReactInternalTypes'; import type {RootTag} from './ReactRootTags'; import type {WorkTag} from './ReactWorkTags'; import type {TypeOfMode} from './ReactTypeOfMode'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; +import type {Lanes} from './ReactFiberLane'; import type {SuspenseInstance} from './ReactFiberHostConfig'; import type {OffscreenProps} from './ReactFiberOffscreenComponent'; @@ -66,7 +66,7 @@ import { resolveFunctionForHotReloading, resolveForwardRefForHotReloading, } from './ReactFiberHotReloading.new'; -import {NoWork} from './ReactFiberExpirationTime.new'; +import {NoLanes} from './ReactFiberLane'; import { NoMode, ConcurrentMode, @@ -150,8 +150,8 @@ function FiberNode( this.firstEffect = null; this.lastEffect = null; - this.expirationTime_opaque = NoWork; - this.childExpirationTime_opaque = NoWork; + this.lanes = NoLanes; + this.childLanes = NoLanes; this.alternate = null; @@ -314,9 +314,8 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { } } - workInProgress.childExpirationTime_opaque = - current.childExpirationTime_opaque; - workInProgress.expirationTime_opaque = current.expirationTime_opaque; + workInProgress.childLanes = current.childLanes; + workInProgress.lanes = current.lanes; workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; @@ -330,7 +329,7 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { currentDependencies === null ? null : { - expirationTime: currentDependencies.expirationTime, + lanes: currentDependencies.lanes, firstContext: currentDependencies.firstContext, responders: currentDependencies.responders, }; @@ -368,10 +367,7 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { } // Used to reuse a Fiber for a second pass. -export function resetWorkInProgress( - workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, -) { +export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) { // This resets the Fiber to what createFiber or createWorkInProgress would // have set the values to before during the first pass. Ideally this wouldn't // be necessary but unfortunately many code paths reads from the workInProgress @@ -392,8 +388,8 @@ export function resetWorkInProgress( const current = workInProgress.alternate; if (current === null) { // Reset to createFiber's initial values. - workInProgress.childExpirationTime_opaque = NoWork; - workInProgress.expirationTime_opaque = renderExpirationTime; + workInProgress.childLanes = NoLanes; + workInProgress.lanes = renderLanes; workInProgress.child = null; workInProgress.memoizedProps = null; @@ -412,9 +408,8 @@ export function resetWorkInProgress( } } else { // Reset to the cloned values that createWorkInProgress would've. - workInProgress.childExpirationTime_opaque = - current.childExpirationTime_opaque; - workInProgress.expirationTime_opaque = current.expirationTime_opaque; + workInProgress.childLanes = current.childLanes; + workInProgress.lanes = current.lanes; workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; @@ -428,7 +423,7 @@ export function resetWorkInProgress( currentDependencies === null ? null : { - expirationTime: currentDependencies.expirationTime, + lanes: currentDependencies.lanes, firstContext: currentDependencies.firstContext, responders: currentDependencies.responders, }; @@ -470,7 +465,7 @@ export function createFiberFromTypeAndProps( pendingProps: any, owner: null | Fiber, mode: TypeOfMode, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ): Fiber { let fiberTag = IndeterminateComponent; // The resolved type is set if we know what the final type will be. I.e. it's not lazy. @@ -491,12 +486,7 @@ export function createFiberFromTypeAndProps( } else { getTag: switch (type) { case REACT_FRAGMENT_TYPE: - return createFiberFromFragment( - pendingProps.children, - mode, - expirationTime, - key, - ); + return createFiberFromFragment(pendingProps.children, mode, lanes, key); case REACT_DEBUG_TRACING_MODE_TYPE: fiberTag = Mode; mode |= DebugTracingMode; @@ -506,30 +496,16 @@ export function createFiberFromTypeAndProps( mode |= StrictMode; break; case REACT_PROFILER_TYPE: - return createFiberFromProfiler(pendingProps, mode, expirationTime, key); + return createFiberFromProfiler(pendingProps, mode, lanes, key); case REACT_SUSPENSE_TYPE: - return createFiberFromSuspense(pendingProps, mode, expirationTime, key); + return createFiberFromSuspense(pendingProps, mode, lanes, key); case REACT_SUSPENSE_LIST_TYPE: - return createFiberFromSuspenseList( - pendingProps, - mode, - expirationTime, - key, - ); + return createFiberFromSuspenseList(pendingProps, mode, lanes, key); case REACT_OFFSCREEN_TYPE: - return createFiberFromOffscreen( - pendingProps, - mode, - expirationTime, - key, - ); + return createFiberFromOffscreen(pendingProps, mode, lanes, key); case REACT_LEGACY_HIDDEN_TYPE: - return createFiberFromLegacyHidden( - pendingProps, - mode, - expirationTime, - key, - ); + return createFiberFromLegacyHidden(pendingProps, mode, lanes, key); + default: { if (typeof type === 'object' && type !== null) { switch (type.$$typeof) { @@ -562,7 +538,7 @@ export function createFiberFromTypeAndProps( type, pendingProps, mode, - expirationTime, + lanes, key, ); } @@ -573,7 +549,7 @@ export function createFiberFromTypeAndProps( type, pendingProps, mode, - expirationTime, + lanes, key, ); } @@ -612,7 +588,7 @@ export function createFiberFromTypeAndProps( const fiber = createFiber(fiberTag, pendingProps, key, mode); fiber.elementType = type; fiber.type = resolvedType; - fiber.expirationTime_opaque = expirationTime; + fiber.lanes = lanes; return fiber; } @@ -620,7 +596,7 @@ export function createFiberFromTypeAndProps( export function createFiberFromElement( element: ReactElement, mode: TypeOfMode, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ): Fiber { let owner = null; if (__DEV__) { @@ -635,7 +611,7 @@ export function createFiberFromElement( pendingProps, owner, mode, - expirationTime, + lanes, ); if (__DEV__) { fiber._debugSource = element._source; @@ -647,11 +623,11 @@ export function createFiberFromElement( export function createFiberFromFragment( elements: ReactFragment, mode: TypeOfMode, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, key: null | string, ): Fiber { const fiber = createFiber(Fragment, elements, key, mode); - fiber.expirationTime_opaque = expirationTime; + fiber.lanes = lanes; return fiber; } @@ -659,13 +635,13 @@ export function createFiberFromFundamental( fundamentalComponent: ReactFundamentalComponent, pendingProps: any, mode: TypeOfMode, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, key: null | string, ): Fiber { const fiber = createFiber(FundamentalComponent, pendingProps, key, mode); fiber.elementType = fundamentalComponent; fiber.type = fundamentalComponent; - fiber.expirationTime_opaque = expirationTime; + fiber.lanes = lanes; return fiber; } @@ -673,20 +649,20 @@ function createFiberFromScope( scope: ReactScope, pendingProps: any, mode: TypeOfMode, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, key: null | string, ) { const fiber = createFiber(ScopeComponent, pendingProps, key, mode); fiber.type = scope; fiber.elementType = scope; - fiber.expirationTime_opaque = expirationTime; + fiber.lanes = lanes; return fiber; } function createFiberFromProfiler( pendingProps: any, mode: TypeOfMode, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, key: null | string, ): Fiber { if (__DEV__) { @@ -699,7 +675,7 @@ function createFiberFromProfiler( // TODO: The Profiler fiber shouldn't have a type. It has a tag. fiber.elementType = REACT_PROFILER_TYPE; fiber.type = REACT_PROFILER_TYPE; - fiber.expirationTime_opaque = expirationTime; + fiber.lanes = lanes; if (enableProfilerTimer) { fiber.stateNode = { @@ -714,7 +690,7 @@ function createFiberFromProfiler( export function createFiberFromSuspense( pendingProps: any, mode: TypeOfMode, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, key: null | string, ) { const fiber = createFiber(SuspenseComponent, pendingProps, key, mode); @@ -725,14 +701,14 @@ export function createFiberFromSuspense( fiber.type = REACT_SUSPENSE_TYPE; fiber.elementType = REACT_SUSPENSE_TYPE; - fiber.expirationTime_opaque = expirationTime; + fiber.lanes = lanes; return fiber; } export function createFiberFromSuspenseList( pendingProps: any, mode: TypeOfMode, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, key: null | string, ) { const fiber = createFiber(SuspenseListComponent, pendingProps, key, mode); @@ -743,14 +719,14 @@ export function createFiberFromSuspenseList( fiber.type = REACT_SUSPENSE_LIST_TYPE; } fiber.elementType = REACT_SUSPENSE_LIST_TYPE; - fiber.expirationTime_opaque = expirationTime; + fiber.lanes = lanes; return fiber; } export function createFiberFromOffscreen( pendingProps: OffscreenProps, mode: TypeOfMode, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, key: null | string, ) { const fiber = createFiber(OffscreenComponent, pendingProps, key, mode); @@ -761,14 +737,14 @@ export function createFiberFromOffscreen( fiber.type = REACT_OFFSCREEN_TYPE; } fiber.elementType = REACT_OFFSCREEN_TYPE; - fiber.expirationTime_opaque = expirationTime; + fiber.lanes = lanes; return fiber; } export function createFiberFromLegacyHidden( pendingProps: OffscreenProps, mode: TypeOfMode, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, key: null | string, ) { const fiber = createFiber(LegacyHiddenComponent, pendingProps, key, mode); @@ -779,17 +755,17 @@ export function createFiberFromLegacyHidden( fiber.type = REACT_LEGACY_HIDDEN_TYPE; } fiber.elementType = REACT_LEGACY_HIDDEN_TYPE; - fiber.expirationTime_opaque = expirationTime; + fiber.lanes = lanes; return fiber; } export function createFiberFromText( content: string, mode: TypeOfMode, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ): Fiber { const fiber = createFiber(HostText, content, null, mode); - fiber.expirationTime_opaque = expirationTime; + fiber.lanes = lanes; return fiber; } @@ -812,11 +788,11 @@ export function createFiberFromDehydratedFragment( export function createFiberFromPortal( portal: ReactPortal, mode: TypeOfMode, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ): Fiber { const pendingProps = portal.children !== null ? portal.children : []; const fiber = createFiber(HostPortal, pendingProps, portal.key, mode); - fiber.expirationTime_opaque = expirationTime; + fiber.lanes = lanes; fiber.stateNode = { containerInfo: portal.containerInfo, pendingChildren: null, // Used by persistent updates @@ -862,8 +838,8 @@ export function assignFiberPropertiesInDEV( target.nextEffect = source.nextEffect; target.firstEffect = source.firstEffect; target.lastEffect = source.lastEffect; - target.expirationTime_opaque = source.expirationTime_opaque; - target.childExpirationTime_opaque = source.childExpirationTime_opaque; + target.lanes = source.lanes; + target.childLanes = source.childLanes; target.alternate = source.alternate; if (enableProfilerTimer) { target.actualDuration = source.actualDuration; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 576016d2f694..5a183f1b852f 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -12,7 +12,7 @@ import type {BlockComponent} from 'react/src/ReactBlock'; import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy'; import type {Fiber} from './ReactInternalTypes'; import type {FiberRoot} from './ReactInternalTypes'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; +import type {Lanes, Lane} from './ReactFiberLane'; import type { SuspenseState, SuspenseListRenderState, @@ -107,14 +107,17 @@ import { initializeUpdateQueue, } from './ReactUpdateQueue.new'; import { - NoWork, - Never, - Sync, - DefaultUpdateTime, - isSameOrHigherPriority, - isSameExpirationTime, - bumpPriorityHigher, -} from './ReactFiberExpirationTime.new'; + NoLane, + NoLanes, + SyncLane, + OffscreenLane, + DefaultHydrationLane, + includesSomeLane, + laneToLanes, + removeLanes, + mergeLanes, + getBumpedLaneForHydration, +} from './ReactFiberLane'; import { ConcurrentMode, NoMode, @@ -191,9 +194,9 @@ import { retryDehydratedSuspenseBoundary, scheduleUpdateOnFiber, renderDidSuspendDelayIfPossible, - markUnprocessedUpdateTime, + markSkippedUpdateLanes, getWorkInProgressRoot, - pushRenderExpirationTime, + pushRenderLanes, } from './ReactFiberWorkLoop.new'; import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev'; @@ -228,7 +231,7 @@ export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { if (current === null) { // If this is a fresh new component that hasn't been rendered yet, we @@ -239,7 +242,7 @@ export function reconcileChildren( workInProgress, null, nextChildren, - renderExpirationTime, + renderLanes, ); } else { // If the current child is the same as the work in progress, it means that @@ -252,7 +255,7 @@ export function reconcileChildren( workInProgress, current.child, nextChildren, - renderExpirationTime, + renderLanes, ); } } @@ -261,7 +264,7 @@ function forceUnmountCurrentAndReconcile( current: Fiber, workInProgress: Fiber, nextChildren: any, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { // This function is fork of reconcileChildren. It's used in cases where we // want to reconcile without matching against the existing set. This has the @@ -275,7 +278,7 @@ function forceUnmountCurrentAndReconcile( workInProgress, current.child, null, - renderExpirationTime, + renderLanes, ); // In the second pass, we mount the new children. The trick here is that we // pass null in place of where we usually pass the current child set. This has @@ -285,7 +288,7 @@ function forceUnmountCurrentAndReconcile( workInProgress, null, nextChildren, - renderExpirationTime, + renderLanes, ); } @@ -294,7 +297,7 @@ function updateForwardRef( workInProgress: Fiber, Component: any, nextProps: any, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { // TODO: current can be non-null here even if the component // hasn't yet mounted. This happens after the first render suspends. @@ -321,7 +324,7 @@ function updateForwardRef( // The rest is a fork of updateFunctionComponent let nextChildren; - prepareToReadContext(workInProgress, renderExpirationTime); + prepareToReadContext(workInProgress, renderLanes); if (__DEV__) { ReactCurrentOwner.current = workInProgress; setIsRendering(true); @@ -331,7 +334,7 @@ function updateForwardRef( render, nextProps, ref, - renderExpirationTime, + renderLanes, ); if ( debugRenderPhaseSideEffectsForStrictMode && @@ -345,7 +348,7 @@ function updateForwardRef( render, nextProps, ref, - renderExpirationTime, + renderLanes, ); } finally { reenableLogs(); @@ -359,27 +362,18 @@ function updateForwardRef( render, nextProps, ref, - renderExpirationTime, + renderLanes, ); } if (current !== null && !didReceiveUpdate) { - bailoutHooks(current, workInProgress, renderExpirationTime); - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderExpirationTime, - ); + bailoutHooks(current, workInProgress, renderLanes); + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; } @@ -388,8 +382,8 @@ function updateMemoComponent( workInProgress: Fiber, Component: any, nextProps: any, - updateExpirationTime, - renderExpirationTime: ExpirationTimeOpaque, + updateLanes: Lanes, + renderLanes: Lanes, ): null | Fiber { if (current === null) { const type = Component.type; @@ -416,8 +410,8 @@ function updateMemoComponent( workInProgress, resolvedType, nextProps, - updateExpirationTime, - renderExpirationTime, + updateLanes, + renderLanes, ); } if (__DEV__) { @@ -439,7 +433,7 @@ function updateMemoComponent( nextProps, null, workInProgress.mode, - renderExpirationTime, + renderLanes, ); child.ref = workInProgress.ref; child.return = workInProgress; @@ -461,7 +455,7 @@ function updateMemoComponent( } } const currentChild = ((current.child: any): Fiber); // This is always exactly one child - if (!isSameOrHigherPriority(updateExpirationTime, renderExpirationTime)) { + if (!includesSomeLane(updateLanes, renderLanes)) { // This will be the props with resolved defaultProps, // unlike current.memoizedProps which will be the unresolved ones. const prevProps = currentChild.memoizedProps; @@ -469,11 +463,7 @@ function updateMemoComponent( let compare = Component.compare; compare = compare !== null ? compare : shallowEqual; if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) { - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderExpirationTime, - ); + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } } // React DevTools reads this flag. @@ -490,8 +480,8 @@ function updateSimpleMemoComponent( workInProgress: Fiber, Component: any, nextProps: any, - updateExpirationTime, - renderExpirationTime: ExpirationTimeOpaque, + updateLanes: Lanes, + renderLanes: Lanes, ): null | Fiber { // TODO: current can be non-null here even if the component // hasn't yet mounted. This happens when the inner render suspends. @@ -536,10 +526,10 @@ function updateSimpleMemoComponent( (__DEV__ ? workInProgress.type === current.type : true) ) { didReceiveUpdate = false; - if (!isSameOrHigherPriority(updateExpirationTime, renderExpirationTime)) { - // The pending update priority was cleared at the beginning of - // beginWork. We're about to bail out, but there might be additional - // updates at a lower priority. Usually, the priority level of the + if (!includesSomeLane(renderLanes, updateLanes)) { + // The pending lanes were cleared at the beginning of beginWork. We're + // about to bail out, but there might be other lanes that weren't + // included in the current render. Usually, the priority level of the // remaining updates is accumlated during the evaluation of the // component (i.e. when processing the update queue). But since since // we're bailing out early *without* evaluating the component, we need @@ -550,11 +540,11 @@ function updateSimpleMemoComponent( // contains hooks. // TODO: Move the reset at in beginWork out of the common path so that // this is no longer necessary. - workInProgress.expirationTime_opaque = current.expirationTime_opaque; + workInProgress.lanes = current.lanes; return bailoutOnAlreadyFinishedWork( current, workInProgress, - renderExpirationTime, + renderLanes, ); } } @@ -564,14 +554,14 @@ function updateSimpleMemoComponent( workInProgress, Component, nextProps, - renderExpirationTime, + renderLanes, ); } function updateOffscreenComponent( current: Fiber | null, workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { const nextProps: OffscreenProps = workInProgress.pendingProps; const nextChildren = nextProps.children; @@ -580,71 +570,57 @@ function updateOffscreenComponent( current !== null ? current.memoizedState : null; if (nextProps.mode === 'hidden') { - if ( - !isSameExpirationTime(renderExpirationTime, (Never: ExpirationTimeOpaque)) - ) { - let nextBaseTime; + if (!includesSomeLane(renderLanes, (OffscreenLane: Lane))) { + let nextBaseLanes; if (prevState !== null) { - const prevBaseTime = prevState.baseTime; - nextBaseTime = !isSameOrHigherPriority( - prevBaseTime, - renderExpirationTime, - ) - ? prevBaseTime - : renderExpirationTime; + const prevBaseLanes = prevState.baseLanes; + nextBaseLanes = mergeLanes(prevBaseLanes, renderLanes); } else { - nextBaseTime = renderExpirationTime; + nextBaseLanes = renderLanes; } // Schedule this fiber to re-render at offscreen priority. Then bailout. if (enableSchedulerTracing) { - markSpawnedWork((Never: ExpirationTimeOpaque)); + markSpawnedWork((OffscreenLane: Lane)); } - workInProgress.expirationTime_opaque = workInProgress.childExpirationTime_opaque = Never; + workInProgress.lanes = workInProgress.childLanes = laneToLanes( + OffscreenLane, + ); const nextState: OffscreenState = { - baseTime: nextBaseTime, + baseLanes: nextBaseLanes, }; workInProgress.memoizedState = nextState; // We're about to bail out, but we need to push this to the stack anyway // to avoid a push/pop misalignment. - pushRenderExpirationTime(workInProgress, nextBaseTime); + pushRenderLanes(workInProgress, nextBaseLanes); return null; } else { - // Rendering at offscreen, so we can clear the base time. + // Rendering at offscreen, so we can clear the base lanes. const nextState: OffscreenState = { - baseTime: NoWork, + baseLanes: NoLanes, }; workInProgress.memoizedState = nextState; - pushRenderExpirationTime(workInProgress, renderExpirationTime); + // Push the lanes that were skipped when we bailed out. + const subtreeRenderLanes = + prevState !== null ? prevState.baseLanes : renderLanes; + pushRenderLanes(workInProgress, subtreeRenderLanes); } } else { - let subtreeRenderTime; + let subtreeRenderLanes; if (prevState !== null) { - const baseTime = prevState.baseTime; - subtreeRenderTime = !isSameOrHigherPriority( - baseTime, - renderExpirationTime, - ) - ? baseTime - : renderExpirationTime; - + subtreeRenderLanes = mergeLanes(prevState.baseLanes, renderLanes); // Since we're not hidden anymore, reset the state workInProgress.memoizedState = null; } else { // We weren't previously hidden, and we still aren't, so there's nothing // special to do. Need to push to the stack regardless, though, to avoid // a push/pop misalignment. - subtreeRenderTime = renderExpirationTime; + subtreeRenderLanes = renderLanes; } - pushRenderExpirationTime(workInProgress, subtreeRenderTime); + pushRenderLanes(workInProgress, subtreeRenderLanes); } - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; } @@ -656,37 +632,27 @@ const updateLegacyHiddenComponent = updateOffscreenComponent; function updateFragment( current: Fiber | null, workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { const nextChildren = workInProgress.pendingProps; - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; } function updateMode( current: Fiber | null, workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { const nextChildren = workInProgress.pendingProps.children; - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; } function updateProfiler( current: Fiber | null, workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { if (enableProfilerTimer) { workInProgress.effectTag |= Update; @@ -699,12 +665,7 @@ function updateProfiler( } const nextProps = workInProgress.pendingProps; const nextChildren = nextProps.children; - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; } @@ -724,7 +685,7 @@ function updateFunctionComponent( workInProgress, Component, nextProps: any, - renderExpirationTime, + renderLanes, ) { if (__DEV__) { if (workInProgress.type !== workInProgress.elementType) { @@ -749,7 +710,7 @@ function updateFunctionComponent( } let nextChildren; - prepareToReadContext(workInProgress, renderExpirationTime); + prepareToReadContext(workInProgress, renderLanes); if (__DEV__) { ReactCurrentOwner.current = workInProgress; setIsRendering(true); @@ -759,7 +720,7 @@ function updateFunctionComponent( Component, nextProps, context, - renderExpirationTime, + renderLanes, ); if ( debugRenderPhaseSideEffectsForStrictMode && @@ -773,7 +734,7 @@ function updateFunctionComponent( Component, nextProps, context, - renderExpirationTime, + renderLanes, ); } finally { reenableLogs(); @@ -787,27 +748,18 @@ function updateFunctionComponent( Component, nextProps, context, - renderExpirationTime, + renderLanes, ); } if (current !== null && !didReceiveUpdate) { - bailoutHooks(current, workInProgress, renderExpirationTime); - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderExpirationTime, - ); + bailoutHooks(current, workInProgress, renderLanes); + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; } @@ -816,7 +768,7 @@ function updateBlock( workInProgress: Fiber, block: BlockComponent, nextProps: any, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { // TODO: current can be non-null here even if the component // hasn't yet mounted. This happens after the first render suspends. @@ -827,7 +779,7 @@ function updateBlock( // The rest is a fork of updateFunctionComponent let nextChildren; - prepareToReadContext(workInProgress, renderExpirationTime); + prepareToReadContext(workInProgress, renderLanes); if (__DEV__) { ReactCurrentOwner.current = workInProgress; setIsRendering(true); @@ -837,7 +789,7 @@ function updateBlock( render, nextProps, data, - renderExpirationTime, + renderLanes, ); if ( debugRenderPhaseSideEffectsForStrictMode && @@ -851,7 +803,7 @@ function updateBlock( render, nextProps, data, - renderExpirationTime, + renderLanes, ); } finally { reenableLogs(); @@ -865,27 +817,18 @@ function updateBlock( render, nextProps, data, - renderExpirationTime, + renderLanes, ); } if (current !== null && !didReceiveUpdate) { - bailoutHooks(current, workInProgress, renderExpirationTime); - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderExpirationTime, - ); + bailoutHooks(current, workInProgress, renderLanes); + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; } @@ -893,8 +836,8 @@ function updateClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, - nextProps, - renderExpirationTime: ExpirationTimeOpaque, + nextProps: any, + renderLanes: Lanes, ) { if (__DEV__) { if (workInProgress.type !== workInProgress.elementType) { @@ -922,7 +865,7 @@ function updateClassComponent( } else { hasContext = false; } - prepareToReadContext(workInProgress, renderExpirationTime); + prepareToReadContext(workInProgress, renderLanes); const instance = workInProgress.stateNode; let shouldUpdate; @@ -939,12 +882,7 @@ function updateClassComponent( } // In the initial pass we might need to construct the instance. constructClassInstance(workInProgress, Component, nextProps); - mountClassInstance( - workInProgress, - Component, - nextProps, - renderExpirationTime, - ); + mountClassInstance(workInProgress, Component, nextProps, renderLanes); shouldUpdate = true; } else if (current === null) { // In a resume, we'll already have an instance we can reuse. @@ -952,7 +890,7 @@ function updateClassComponent( workInProgress, Component, nextProps, - renderExpirationTime, + renderLanes, ); } else { shouldUpdate = updateClassInstance( @@ -960,7 +898,7 @@ function updateClassComponent( workInProgress, Component, nextProps, - renderExpirationTime, + renderLanes, ); } const nextUnitOfWork = finishClassComponent( @@ -969,7 +907,7 @@ function updateClassComponent( Component, shouldUpdate, hasContext, - renderExpirationTime, + renderLanes, ); if (__DEV__) { const inst = workInProgress.stateNode; @@ -993,7 +931,7 @@ function finishClassComponent( Component: any, shouldUpdate: boolean, hasContext: boolean, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { // Refs should update even if shouldComponentUpdate returns false markRef(current, workInProgress); @@ -1006,11 +944,7 @@ function finishClassComponent( invalidateContextProvider(workInProgress, Component, false); } - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderExpirationTime, - ); + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } const instance = workInProgress.stateNode; @@ -1064,15 +998,10 @@ function finishClassComponent( current, workInProgress, nextChildren, - renderExpirationTime, + renderLanes, ); } else { - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, nextChildren, renderLanes); } // Memoize state using the values we just used to render. @@ -1102,7 +1031,7 @@ function pushHostRootContext(workInProgress) { pushHostContainer(workInProgress, root.containerInfo); } -function updateHostRoot(current, workInProgress, renderExpirationTime) { +function updateHostRoot(current, workInProgress, renderLanes) { pushHostRootContext(workInProgress); const updateQueue = workInProgress.updateQueue; invariant( @@ -1115,20 +1044,14 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) { const prevState = workInProgress.memoizedState; const prevChildren = prevState !== null ? prevState.element : null; cloneUpdateQueue(current, workInProgress); - processUpdateQueue(workInProgress, nextProps, null, renderExpirationTime); + processUpdateQueue(workInProgress, nextProps, null, renderLanes); const nextState = workInProgress.memoizedState; // Caution: React DevTools currently depends on this property // being called "element". const nextChildren = nextState.element; if (nextChildren === prevChildren) { - // If the state is the same as before, that's a bailout because we had - // no work that expires at this time. resetHydrationState(); - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderExpirationTime, - ); + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } const root: FiberRoot = workInProgress.stateNode; if (root.hydrate && enterHydrationState(workInProgress)) { @@ -1141,7 +1064,7 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) { workInProgress, null, nextChildren, - renderExpirationTime, + renderLanes, ); workInProgress.child = child; @@ -1159,12 +1082,7 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) { } else { // Otherwise reset hydration state in case we aborted and resumed another // root. - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, nextChildren, renderLanes); resetHydrationState(); } return workInProgress.child; @@ -1173,7 +1091,7 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) { function updateHostComponent( current: Fiber | null, workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { pushHostContext(workInProgress); @@ -1221,12 +1139,7 @@ function updateHostComponent( nextChildren = wrappedChildren; } - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; } @@ -1243,8 +1156,8 @@ function mountLazyComponent( _current, workInProgress, elementType, - updateExpirationTime, - renderExpirationTime, + updateLanes, + renderLanes, ) { if (_current !== null) { // A lazy component only mounts if it suspended inside a non- @@ -1280,7 +1193,7 @@ function mountLazyComponent( workInProgress, Component, resolvedProps, - renderExpirationTime, + renderLanes, ); return child; } @@ -1295,7 +1208,7 @@ function mountLazyComponent( workInProgress, Component, resolvedProps, - renderExpirationTime, + renderLanes, ); return child; } @@ -1310,7 +1223,7 @@ function mountLazyComponent( workInProgress, Component, resolvedProps, - renderExpirationTime, + renderLanes, ); return child; } @@ -1333,8 +1246,8 @@ function mountLazyComponent( workInProgress, Component, resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too - updateExpirationTime, - renderExpirationTime, + updateLanes, + renderLanes, ); return child; } @@ -1346,7 +1259,7 @@ function mountLazyComponent( workInProgress, Component, props, - renderExpirationTime, + renderLanes, ); return child; } @@ -1380,7 +1293,7 @@ function mountIncompleteClassComponent( workInProgress, Component, nextProps, - renderExpirationTime, + renderLanes, ) { if (_current !== null) { // An incomplete component only mounts if it suspended inside a non- @@ -1408,15 +1321,10 @@ function mountIncompleteClassComponent( } else { hasContext = false; } - prepareToReadContext(workInProgress, renderExpirationTime); + prepareToReadContext(workInProgress, renderLanes); constructClassInstance(workInProgress, Component, nextProps); - mountClassInstance( - workInProgress, - Component, - nextProps, - renderExpirationTime, - ); + mountClassInstance(workInProgress, Component, nextProps, renderLanes); return finishClassComponent( null, @@ -1424,7 +1332,7 @@ function mountIncompleteClassComponent( Component, true, hasContext, - renderExpirationTime, + renderLanes, ); } @@ -1432,7 +1340,7 @@ function mountIndeterminateComponent( _current, workInProgress, Component, - renderExpirationTime, + renderLanes, ) { if (_current !== null) { // An indeterminate component only mounts if it suspended inside a non- @@ -1456,7 +1364,7 @@ function mountIndeterminateComponent( context = getMaskedContext(workInProgress, unmaskedContext); } - prepareToReadContext(workInProgress, renderExpirationTime); + prepareToReadContext(workInProgress, renderLanes); let value; if (__DEV__) { @@ -1489,7 +1397,7 @@ function mountIndeterminateComponent( Component, props, context, - renderExpirationTime, + renderLanes, ); setIsRendering(false); } else { @@ -1499,7 +1407,7 @@ function mountIndeterminateComponent( Component, props, context, - renderExpirationTime, + renderLanes, ); } // React DevTools reads this flag. @@ -1591,14 +1499,14 @@ function mountIndeterminateComponent( } adoptClassInstance(workInProgress, value); - mountClassInstance(workInProgress, Component, props, renderExpirationTime); + mountClassInstance(workInProgress, Component, props, renderLanes); return finishClassComponent( null, workInProgress, Component, true, hasContext, - renderExpirationTime, + renderLanes, ); } else { // Proceed under the assumption that this is a function component @@ -1624,14 +1532,14 @@ function mountIndeterminateComponent( Component, props, context, - renderExpirationTime, + renderLanes, ); } finally { reenableLogs(); } } } - reconcileChildren(null, workInProgress, value, renderExpirationTime); + reconcileChildren(null, workInProgress, value, renderLanes); if (__DEV__) { validateFunctionComponentInDev(workInProgress, Component); } @@ -1719,30 +1627,21 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) { const SUSPENDED_MARKER: SuspenseState = { dehydrated: null, - retryTime: NoWork, + retryLane: NoLane, }; -function mountSuspenseOffscreenState( - renderExpirationTime: ExpirationTimeOpaque, -): OffscreenState { +function mountSuspenseOffscreenState(renderLanes: Lanes): OffscreenState { return { - baseTime: renderExpirationTime, + baseLanes: renderLanes, }; } function updateSuspenseOffscreenState( prevOffscreenState: OffscreenState, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ): OffscreenState { - const prevBaseTime = prevOffscreenState.baseTime; return { - // Choose whichever time is inclusive of the other one. This represents - // the union of all the levels that suspended. - baseTime: - !isSameExpirationTime(prevBaseTime, (NoWork: ExpirationTimeOpaque)) && - !isSameOrHigherPriority(prevBaseTime, renderExpirationTime) - ? prevBaseTime - : renderExpirationTime, + baseLanes: mergeLanes(prevOffscreenState.baseLanes, renderLanes), }; } @@ -1750,7 +1649,7 @@ function shouldRemainOnFallback( suspenseContext: SuspenseContext, current: null | Fiber, workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { // If we're already showing a fallback, there are cases where we need to // remain on that fallback regardless of whether the content has resolved. @@ -1773,44 +1672,12 @@ function shouldRemainOnFallback( ); } -function getRemainingWorkInPrimaryTree( - current: Fiber, - workInProgress: Fiber, - renderExpirationTime, -) { - const currentChildExpirationTime = current.childExpirationTime_opaque; - if ( - !isSameOrHigherPriority(currentChildExpirationTime, renderExpirationTime) - ) { - // The highest priority remaining work is not part of this render. So the - // remaining work has not changed. - return currentChildExpirationTime; - } - - if ((workInProgress.mode & BlockingMode) !== NoMode) { - // The highest priority remaining work is part of this render. Since we only - // keep track of the highest level, we don't know if there's a lower - // priority level scheduled. As a compromise, we'll render at the lowest - // known level in the entire tree, since that will include everything. - // TODO: If expirationTime were a bitmask where each bit represents a - // separate task thread, this would be: currentChildBits & ~renderBits - const root = getWorkInProgressRoot(); - if (root !== null) { - const lastPendingTime = root.lastPendingTime_opaque; - if (!isSameOrHigherPriority(lastPendingTime, renderExpirationTime)) { - return lastPendingTime; - } - } - } - // In legacy mode, there's no work left. - return NoWork; +function getRemainingWorkInPrimaryTree(current: Fiber, renderLanes) { + // TODO: Should not remove render lanes that were pinged during this render + return removeLanes(current.childLanes, renderLanes); } -function updateSuspenseComponent( - current, - workInProgress, - renderExpirationTime, -) { +function updateSuspenseComponent(current, workInProgress, renderLanes) { const nextProps = workInProgress.pendingProps; // This is used by DevTools to force a boundary to suspend. @@ -1831,7 +1698,7 @@ function updateSuspenseComponent( suspenseContext, current, workInProgress, - renderExpirationTime, + renderLanes, ) ) { // Something in this boundary's subtree already suspended. Switch to @@ -1903,7 +1770,7 @@ function updateSuspenseComponent( return mountDehydratedSuspenseComponent( workInProgress, dehydrated, - renderExpirationTime, + renderLanes, ); } } @@ -1917,11 +1784,11 @@ function updateSuspenseComponent( workInProgress, nextPrimaryChildren, nextFallbackChildren, - renderExpirationTime, + renderLanes, ); const primaryChildFragment: Fiber = (workInProgress.child: any); primaryChildFragment.memoizedState = mountSuspenseOffscreenState( - renderExpirationTime, + renderLanes, ); workInProgress.memoizedState = SUSPENDED_MARKER; return fallbackFragment; @@ -1930,7 +1797,7 @@ function updateSuspenseComponent( return mountSuspensePrimaryChildren( workInProgress, nextPrimaryChildren, - renderExpirationTime, + renderLanes, ); } } else { @@ -1952,7 +1819,7 @@ function updateSuspenseComponent( workInProgress, dehydrated, prevState, - renderExpirationTime, + renderLanes, ); } else if ( (workInProgress.memoizedState: null | SuspenseState) !== null @@ -1974,11 +1841,11 @@ function updateSuspenseComponent( workInProgress, nextPrimaryChildren, nextFallbackChildren, - renderExpirationTime, + renderLanes, ); const primaryChildFragment: Fiber = (workInProgress.child: any); primaryChildFragment.memoizedState = mountSuspenseOffscreenState( - renderExpirationTime, + renderLanes, ); workInProgress.memoizedState = SUSPENDED_MARKER; return fallbackChildFragment; @@ -1994,22 +1861,18 @@ function updateSuspenseComponent( workInProgress, nextPrimaryChildren, nextFallbackChildren, - renderExpirationTime, + renderLanes, ); const primaryChildFragment: Fiber = (workInProgress.child: any); const prevOffscreenState: OffscreenState | null = (current.child: any) .memoizedState; primaryChildFragment.memoizedState = prevOffscreenState === null - ? mountSuspenseOffscreenState(renderExpirationTime) - : updateSuspenseOffscreenState( - prevOffscreenState, - renderExpirationTime, - ); - primaryChildFragment.childExpirationTime_opaque = getRemainingWorkInPrimaryTree( + ? mountSuspenseOffscreenState(renderLanes) + : updateSuspenseOffscreenState(prevOffscreenState, renderLanes); + primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree( current, - workInProgress, - renderExpirationTime, + renderLanes, ); workInProgress.memoizedState = SUSPENDED_MARKER; return fallbackChildFragment; @@ -2019,7 +1882,7 @@ function updateSuspenseComponent( current, workInProgress, nextPrimaryChildren, - renderExpirationTime, + renderLanes, ); workInProgress.memoizedState = null; return primaryChildFragment; @@ -2035,22 +1898,18 @@ function updateSuspenseComponent( workInProgress, nextPrimaryChildren, nextFallbackChildren, - renderExpirationTime, + renderLanes, ); const primaryChildFragment: Fiber = (workInProgress.child: any); const prevOffscreenState: OffscreenState | null = (current.child: any) .memoizedState; primaryChildFragment.memoizedState = prevOffscreenState === null - ? mountSuspenseOffscreenState(renderExpirationTime) - : updateSuspenseOffscreenState( - prevOffscreenState, - renderExpirationTime, - ); - primaryChildFragment.childExpirationTime_opaque = getRemainingWorkInPrimaryTree( + ? mountSuspenseOffscreenState(renderLanes) + : updateSuspenseOffscreenState(prevOffscreenState, renderLanes); + primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree( current, - workInProgress, - renderExpirationTime, + renderLanes, ); // Skip the primary children, and continue working on the // fallback children. @@ -2064,7 +1923,7 @@ function updateSuspenseComponent( current, workInProgress, nextPrimaryChildren, - renderExpirationTime, + renderLanes, ); workInProgress.memoizedState = null; return primaryChildFragment; @@ -2076,7 +1935,7 @@ function updateSuspenseComponent( function mountSuspensePrimaryChildren( workInProgress, primaryChildren, - renderExpirationTime, + renderLanes, ) { const mode = workInProgress.mode; const primaryChildProps: OffscreenProps = { @@ -2086,7 +1945,7 @@ function mountSuspensePrimaryChildren( const primaryChildFragment = createFiberFromOffscreen( primaryChildProps, mode, - renderExpirationTime, + renderLanes, null, ); primaryChildFragment.return = workInProgress; @@ -2098,7 +1957,7 @@ function mountSuspenseFallbackChildren( workInProgress, primaryChildren, fallbackChildren, - renderExpirationTime, + renderLanes, ) { const mode = workInProgress.mode; const progressedPrimaryFragment: Fiber | null = workInProgress.child; @@ -2114,7 +1973,7 @@ function mountSuspenseFallbackChildren( // In legacy mode, we commit the primary tree as if it successfully // completed, even though it's in an inconsistent state. primaryChildFragment = progressedPrimaryFragment; - primaryChildFragment.childExpirationTime_opaque = NoWork; + primaryChildFragment.childLanes = NoLanes; primaryChildFragment.pendingProps = primaryChildProps; if (enableProfilerTimer && workInProgress.mode & ProfileMode) { @@ -2131,20 +1990,20 @@ function mountSuspenseFallbackChildren( fallbackChildFragment = createFiberFromFragment( fallbackChildren, mode, - renderExpirationTime, + renderLanes, null, ); } else { primaryChildFragment = createFiberFromOffscreen( primaryChildProps, mode, - NoWork, + NoLanes, null, ); fallbackChildFragment = createFiberFromFragment( fallbackChildren, mode, - renderExpirationTime, + renderLanes, null, ); } @@ -2169,7 +2028,7 @@ function updateSuspensePrimaryChildren( current, workInProgress, primaryChildren, - renderExpirationTime, + renderLanes, ) { const currentPrimaryChildFragment: Fiber = (current.child: any); const currentFallbackChildFragment: Fiber | null = @@ -2183,7 +2042,7 @@ function updateSuspensePrimaryChildren( }, ); if ((workInProgress.mode & BlockingMode) === NoMode) { - primaryChildFragment.expirationTime_opaque = renderExpirationTime; + primaryChildFragment.lanes = renderLanes; } primaryChildFragment.return = workInProgress; primaryChildFragment.sibling = null; @@ -2203,7 +2062,7 @@ function updateSuspenseFallbackChildren( workInProgress, primaryChildren, fallbackChildren, - renderExpirationTime, + renderLanes, ) { const mode = workInProgress.mode; const currentPrimaryChildFragment: Fiber = (current.child: any); @@ -2221,7 +2080,7 @@ function updateSuspenseFallbackChildren( // completed, even though it's in an inconsistent state. const progressedPrimaryFragment: Fiber = (workInProgress.child: any); primaryChildFragment = progressedPrimaryFragment; - primaryChildFragment.childExpirationTime_opaque = NoWork; + primaryChildFragment.childLanes = NoLanes; primaryChildFragment.pendingProps = primaryChildProps; if (enableProfilerTimer && workInProgress.mode & ProfileMode) { @@ -2268,7 +2127,7 @@ function updateSuspenseFallbackChildren( fallbackChildFragment = createFiberFromFragment( fallbackChildren, mode, - renderExpirationTime, + renderLanes, null, ); // Needs a placement effect because the parent (the Suspense boundary) already @@ -2287,15 +2146,10 @@ function updateSuspenseFallbackChildren( function retrySuspenseComponentWithoutHydrating( current: Fiber, workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { // This will add the old fiber to the deletion list - reconcileChildFibers( - workInProgress, - current.child, - null, - renderExpirationTime, - ); + reconcileChildFibers(workInProgress, current.child, null, renderLanes); // We're now not suspended nor dehydrated. const nextProps = workInProgress.pendingProps; @@ -2303,7 +2157,7 @@ function retrySuspenseComponentWithoutHydrating( const primaryChildFragment = mountSuspensePrimaryChildren( workInProgress, primaryChildren, - renderExpirationTime, + renderLanes, ); // Needs a placement effect because the parent (the Suspense boundary) already // mounted but this is a new fiber. @@ -2318,19 +2172,19 @@ function mountSuspenseFallbackAfterRetryWithoutHydrating( workInProgress, primaryChildren, fallbackChildren, - renderExpirationTime, + renderLanes, ) { const mode = workInProgress.mode; const primaryChildFragment = createFiberFromOffscreen( primaryChildren, mode, - NoWork, + NoLanes, null, ); const fallbackChildFragment = createFiberFromFragment( fallbackChildren, mode, - renderExpirationTime, + renderLanes, null, ); // Needs a placement effect because the parent (the Suspense @@ -2345,12 +2199,7 @@ function mountSuspenseFallbackAfterRetryWithoutHydrating( if ((workInProgress.mode & BlockingMode) !== NoMode) { // We will have dropped the effect list which contains the // deletion. We need to reconcile to delete the current child. - reconcileChildFibers( - workInProgress, - current.child, - null, - renderExpirationTime, - ); + reconcileChildFibers(workInProgress, current.child, null, renderLanes); } return fallbackChildFragment; @@ -2359,7 +2208,7 @@ function mountSuspenseFallbackAfterRetryWithoutHydrating( function mountDehydratedSuspenseComponent( workInProgress: Fiber, suspenseInstance: SuspenseInstance, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ): null | Fiber { // During the first pass, we'll bail out and not drill into the children. // Instead, we'll leave the content in place and try to hydrate it later. @@ -2373,7 +2222,7 @@ function mountDehydratedSuspenseComponent( 'the server rendered components.', ); } - workInProgress.expirationTime_opaque = Sync; + workInProgress.lanes = laneToLanes(SyncLane); } else if (isSuspenseInstanceFallback(suspenseInstance)) { // This is a client-only boundary. Since we won't get any content from the server // for this, we need to schedule that at a higher priority based on when it would @@ -2387,17 +2236,16 @@ function mountDehydratedSuspenseComponent( // time. This will mean that Suspense timeouts are slightly shifted to later than // they should be. // Schedule a normal pri update to render this content. - const newExpirationTime = DefaultUpdateTime; if (enableSchedulerTracing) { - markSpawnedWork(newExpirationTime); + markSpawnedWork(DefaultHydrationLane); } - workInProgress.expirationTime_opaque = newExpirationTime; + workInProgress.lanes = laneToLanes(DefaultHydrationLane); } else { // We'll continue hydrating the rest at offscreen priority since we'll already // be showing the right content coming from the server, it is no rush. - workInProgress.expirationTime_opaque = Never; + workInProgress.lanes = laneToLanes(OffscreenLane); if (enableSchedulerTracing) { - markSpawnedWork((Never: ExpirationTimeOpaque)); + markSpawnedWork(OffscreenLane); } } return null; @@ -2408,7 +2256,7 @@ function updateDehydratedSuspenseComponent( workInProgress: Fiber, suspenseInstance: SuspenseInstance, suspenseState: SuspenseState, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ): null | Fiber { // We should never be hydrating at this point because it is the first pass, // but after we've already committed once. @@ -2418,7 +2266,7 @@ function updateDehydratedSuspenseComponent( return retrySuspenseComponentWithoutHydrating( current, workInProgress, - renderExpirationTime, + renderLanes, ); } @@ -2429,36 +2277,30 @@ function updateDehydratedSuspenseComponent( return retrySuspenseComponentWithoutHydrating( current, workInProgress, - renderExpirationTime, + renderLanes, ); } - // We use childExpirationTime to indicate that a child might depend on context, so if + // We use lanes to indicate that a child might depend on context, so if // any context has changed, we need to treat is as if the input might have changed. - const hasContextChanged = isSameOrHigherPriority( - current.childExpirationTime_opaque, - renderExpirationTime, - ); + const hasContextChanged = includesSomeLane(renderLanes, current.childLanes); if (didReceiveUpdate || hasContextChanged) { // This boundary has changed since the first render. This means that we are now unable to - // hydrate it. We might still be able to hydrate it using an earlier expiration time, if - // we are rendering at lower expiration than sync. - if ( - !isSameOrHigherPriority( - renderExpirationTime, - (Sync: ExpirationTimeOpaque), - ) - ) { + // hydrate it. We might still be able to hydrate it using a higher priority lane. + const root = getWorkInProgressRoot(); + if (root !== null) { + const attemptHydrationAtLane = getBumpedLaneForHydration( + root, + renderLanes, + ); if ( - isSameOrHigherPriority(renderExpirationTime, suspenseState.retryTime) + attemptHydrationAtLane !== NoLane && + attemptHydrationAtLane !== suspenseState.retryLane ) { - // This render is even higher pri than we've seen before, let's try again - // at even higher pri. - const attemptHydrationAtExpirationTime = bumpPriorityHigher( - renderExpirationTime, - ); - suspenseState.retryTime = attemptHydrationAtExpirationTime; - scheduleUpdateOnFiber(current, attemptHydrationAtExpirationTime); - // TODO: Early abort this render. + // Intentionally mutating since this render will get interrupted. This + // is one of the very rare times where we mutate the current tree + // during the render phase. + suspenseState.retryLane = attemptHydrationAtLane; + scheduleUpdateOnFiber(current, attemptHydrationAtLane); } else { // We have already tried to ping at a higher priority than we're rendering with // so if we got here, we must have failed to hydrate at those levels. We must @@ -2468,6 +2310,7 @@ function updateDehydratedSuspenseComponent( // an opportunity to hydrate before this pass commits. } } + // If we have scheduled higher pri work above, this will probably just abort the render // since we now have higher priority work, but in case it doesn't, we need to prepare to // render something, if we time out. Even if that requires us to delete everything and @@ -2477,7 +2320,7 @@ function updateDehydratedSuspenseComponent( return retrySuspenseComponentWithoutHydrating( current, workInProgress, - renderExpirationTime, + renderLanes, ); } else if (isSuspenseInstancePending(suspenseInstance)) { // This component is still pending more data from the server, so we can't hydrate its @@ -2509,7 +2352,7 @@ function updateDehydratedSuspenseComponent( const primaryChildFragment = mountSuspensePrimaryChildren( workInProgress, primaryChildren, - renderExpirationTime, + renderLanes, ); // Mark the children as hydrating. This is a fast path to know whether this // tree is part of a hydrating tree. This is used to determine if a child @@ -2522,32 +2365,19 @@ function updateDehydratedSuspenseComponent( } } -function scheduleWorkOnFiber( - fiber: Fiber, - renderExpirationTime: ExpirationTimeOpaque, -) { - if ( - !isSameOrHigherPriority(fiber.expirationTime_opaque, renderExpirationTime) - ) { - fiber.expirationTime_opaque = renderExpirationTime; - } +function scheduleWorkOnFiber(fiber: Fiber, renderLanes: Lanes) { + fiber.lanes = mergeLanes(fiber.lanes, renderLanes); const alternate = fiber.alternate; - if ( - alternate !== null && - !isSameOrHigherPriority( - alternate.expirationTime_opaque, - renderExpirationTime, - ) - ) { - alternate.expirationTime_opaque = renderExpirationTime; + if (alternate !== null) { + alternate.lanes = mergeLanes(alternate.lanes, renderLanes); } - scheduleWorkOnParentPath(fiber.return, renderExpirationTime); + scheduleWorkOnParentPath(fiber.return, renderLanes); } function propagateSuspenseContextChange( workInProgress: Fiber, firstChild: null | Fiber, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ): void { // Mark any Suspense boundaries with fallbacks as having work to do. // If they were previously forced into fallbacks, they may now be able @@ -2557,7 +2387,7 @@ function propagateSuspenseContextChange( if (node.tag === SuspenseComponent) { const state: SuspenseState | null = node.memoizedState; if (state !== null) { - scheduleWorkOnFiber(node, renderExpirationTime); + scheduleWorkOnFiber(node, renderLanes); } } else if (node.tag === SuspenseListComponent) { // If the tail is hidden there might not be an Suspense boundaries @@ -2565,7 +2395,7 @@ function propagateSuspenseContextChange( // list itself. // We don't have to traverse to the children of the list since // the list will propagate the change when it rerenders. - scheduleWorkOnFiber(node, renderExpirationTime); + scheduleWorkOnFiber(node, renderLanes); } else if (node.child !== null) { node.child.return = node; node = node.child; @@ -2797,7 +2627,7 @@ function initSuspenseListRenderState( function updateSuspenseListComponent( current: Fiber | null, workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { const nextProps = workInProgress.pendingProps; const revealOrder: SuspenseListRevealOrder = nextProps.revealOrder; @@ -2808,7 +2638,7 @@ function updateSuspenseListComponent( validateTailOptions(tailMode, revealOrder); validateSuspenseListChildren(newChildren, revealOrder); - reconcileChildren(current, workInProgress, newChildren, renderExpirationTime); + reconcileChildren(current, workInProgress, newChildren, renderLanes); let suspenseContext: SuspenseContext = suspenseStackCursor.current; @@ -2832,7 +2662,7 @@ function updateSuspenseListComponent( propagateSuspenseContextChange( workInProgress, workInProgress.child, - renderExpirationTime, + renderLanes, ); } suspenseContext = setDefaultShallowSuspenseContext(suspenseContext); @@ -2925,7 +2755,7 @@ function updateSuspenseListComponent( function updatePortalComponent( current: Fiber | null, workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); const nextChildren = workInProgress.pendingProps; @@ -2939,15 +2769,10 @@ function updatePortalComponent( workInProgress, null, nextChildren, - renderExpirationTime, + renderLanes, ); } else { - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, nextChildren, renderLanes); } return workInProgress.child; } @@ -2955,7 +2780,7 @@ function updatePortalComponent( function updateContextProvider( current: Fiber | null, workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { const providerType: ReactProviderType = workInProgress.type; const context: ReactContext = providerType._context; @@ -2987,23 +2812,18 @@ function updateContextProvider( return bailoutOnAlreadyFinishedWork( current, workInProgress, - renderExpirationTime, + renderLanes, ); } } else { // The context value changed. Search for matching consumers and schedule // them to update. - propagateContextChange( - workInProgress, - context, - changedBits, - renderExpirationTime, - ); + propagateContextChange(workInProgress, context, changedBits, renderLanes); } } const newChildren = newProps.children; - reconcileChildren(current, workInProgress, newChildren, renderExpirationTime); + reconcileChildren(current, workInProgress, newChildren, renderLanes); return workInProgress.child; } @@ -3012,7 +2832,7 @@ let hasWarnedAboutUsingContextAsConsumer = false; function updateContextConsumer( current: Fiber | null, workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { let context: ReactContext = workInProgress.type; // The logic below for Context differs depending on PROD or DEV mode. In @@ -3054,7 +2874,7 @@ function updateContextConsumer( } } - prepareToReadContext(workInProgress, renderExpirationTime); + prepareToReadContext(workInProgress, renderLanes); const newValue = readContext(context, newProps.unstable_observedBits); let newChildren; if (__DEV__) { @@ -3068,15 +2888,11 @@ function updateContextConsumer( // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; - reconcileChildren(current, workInProgress, newChildren, renderExpirationTime); + reconcileChildren(current, workInProgress, newChildren, renderLanes); return workInProgress.child; } -function updateFundamentalComponent( - current, - workInProgress, - renderExpirationTime, -) { +function updateFundamentalComponent(current, workInProgress, renderLanes) { const fundamentalImpl = workInProgress.type.impl; if (fundamentalImpl.reconcileChildren === false) { return null; @@ -3084,25 +2900,15 @@ function updateFundamentalComponent( const nextProps = workInProgress.pendingProps; const nextChildren = nextProps.children; - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; } -function updateScopeComponent(current, workInProgress, renderExpirationTime) { +function updateScopeComponent(current, workInProgress, renderLanes) { const nextProps = workInProgress.pendingProps; const nextChildren = nextProps.children; - reconcileChildren( - current, - workInProgress, - nextChildren, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; } @@ -3113,7 +2919,7 @@ export function markWorkInProgressReceivedUpdate() { function bailoutOnAlreadyFinishedWork( current: Fiber | null, workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ): Fiber | null { if (current !== null) { // Reuse previous dependencies @@ -3125,16 +2931,10 @@ function bailoutOnAlreadyFinishedWork( stopProfilerTimerIfRunning(workInProgress); } - const updateExpirationTime = workInProgress.expirationTime_opaque; - if ( - !isSameExpirationTime(updateExpirationTime, (NoWork: ExpirationTimeOpaque)) - ) { - markUnprocessedUpdateTime(updateExpirationTime); - } + markSkippedUpdateLanes(workInProgress.lanes); // Check if the children have any pending work. - const childExpirationTime = workInProgress.childExpirationTime_opaque; - if (!isSameOrHigherPriority(childExpirationTime, renderExpirationTime)) { + if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { // The children don't have any work either. We can skip them. // TODO: Once we add back resuming, we should check if the children are // a work-in-progress set. If so, we need to transfer their effects. @@ -3213,9 +3013,9 @@ function remountFiber( function beginWork( current: Fiber | null, workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ): Fiber | null { - const updateExpirationTime = workInProgress.expirationTime_opaque; + const updateLanes = workInProgress.lanes; if (__DEV__) { if (workInProgress._debugNeedsRemount && current !== null) { @@ -3229,7 +3029,7 @@ function beginWork( workInProgress.pendingProps, workInProgress._debugOwner || null, workInProgress.mode, - workInProgress.expirationTime_opaque, + workInProgress.lanes, ), ); } @@ -3248,9 +3048,7 @@ function beginWork( // If props or context changed, mark the fiber as having performed work. // This may be unset if the props are determined to be equal later (memo). didReceiveUpdate = true; - } else if ( - !isSameOrHigherPriority(updateExpirationTime, renderExpirationTime) - ) { + } else if (!includesSomeLane(renderLanes, updateLanes)) { didReceiveUpdate = false; // This fiber does not have any pending work. Bailout without entering // the begin phase. There's still some bookkeeping we that needs to be done @@ -3284,9 +3082,9 @@ function beginWork( case Profiler: if (enableProfilerTimer) { // Profiler should only call onRender when one of its descendants actually rendered. - const hasChildWork = isSameOrHigherPriority( - workInProgress.childExpirationTime_opaque, - renderExpirationTime, + const hasChildWork = includesSomeLane( + renderLanes, + workInProgress.childLanes, ); if (hasChildWork) { workInProgress.effectTag |= Update; @@ -3321,20 +3119,14 @@ function beginWork( // go straight to the fallback. Check the priority of the primary // child fragment. const primaryChildFragment: Fiber = (workInProgress.child: any); - const primaryChildExpirationTime = - primaryChildFragment.childExpirationTime_opaque; - if ( - isSameOrHigherPriority( - primaryChildExpirationTime, - renderExpirationTime, - ) - ) { + const primaryChildLanes = primaryChildFragment.childLanes; + if (includesSomeLane(renderLanes, primaryChildLanes)) { // The primary children have pending work. Use the normal path // to attempt to render the primary children again. return updateSuspenseComponent( current, workInProgress, - renderExpirationTime, + renderLanes, ); } else { // The primary child fragment does not have pending work marked @@ -3348,7 +3140,7 @@ function beginWork( const child = bailoutOnAlreadyFinishedWork( current, workInProgress, - renderExpirationTime, + renderLanes, ); if (child !== null) { // The fallback children have pending work. Skip over the @@ -3370,9 +3162,9 @@ function beginWork( const didSuspendBefore = (current.effectTag & DidCapture) !== NoEffect; - const hasChildWork = isSameOrHigherPriority( - workInProgress.childExpirationTime_opaque, - renderExpirationTime, + const hasChildWork = includesSomeLane( + renderLanes, + workInProgress.childLanes, ); if (didSuspendBefore) { @@ -3385,7 +3177,7 @@ function beginWork( return updateSuspenseListComponent( current, workInProgress, - renderExpirationTime, + renderLanes, ); } // If none of the children had any work, that means that none of @@ -3426,19 +3218,11 @@ function beginWork( // TODO: Probably should refactor `beginWork` to split the bailout // path from the normal path. I'm tempted to do a labeled break here // but I won't :) - workInProgress.expirationTime_opaque = NoWork; - return updateOffscreenComponent( - current, - workInProgress, - renderExpirationTime, - ); + workInProgress.lanes = NoLanes; + return updateOffscreenComponent(current, workInProgress, renderLanes); } } - return bailoutOnAlreadyFinishedWork( - current, - workInProgress, - renderExpirationTime, - ); + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } else { // An update was scheduled on this fiber, but there are no new props // nor legacy context. Set this to false. If an update queue or context @@ -3455,7 +3239,7 @@ function beginWork( // the update queue. However, there's an exception: SimpleMemoComponent // sometimes bails out later in the begin phase. This indicates that we should // move this assignment out of the common path and into each branch. - workInProgress.expirationTime_opaque = NoWork; + workInProgress.lanes = NoLanes; switch (workInProgress.tag) { case IndeterminateComponent: { @@ -3463,7 +3247,7 @@ function beginWork( current, workInProgress, workInProgress.type, - renderExpirationTime, + renderLanes, ); } case LazyComponent: { @@ -3472,8 +3256,8 @@ function beginWork( current, workInProgress, elementType, - updateExpirationTime, - renderExpirationTime, + updateLanes, + renderLanes, ); } case FunctionComponent: { @@ -3488,7 +3272,7 @@ function beginWork( workInProgress, Component, resolvedProps, - renderExpirationTime, + renderLanes, ); } case ClassComponent: { @@ -3503,27 +3287,19 @@ function beginWork( workInProgress, Component, resolvedProps, - renderExpirationTime, + renderLanes, ); } case HostRoot: - return updateHostRoot(current, workInProgress, renderExpirationTime); + return updateHostRoot(current, workInProgress, renderLanes); case HostComponent: - return updateHostComponent(current, workInProgress, renderExpirationTime); + return updateHostComponent(current, workInProgress, renderLanes); case HostText: return updateHostText(current, workInProgress); case SuspenseComponent: - return updateSuspenseComponent( - current, - workInProgress, - renderExpirationTime, - ); + return updateSuspenseComponent(current, workInProgress, renderLanes); case HostPortal: - return updatePortalComponent( - current, - workInProgress, - renderExpirationTime, - ); + return updatePortalComponent(current, workInProgress, renderLanes); case ForwardRef: { const type = workInProgress.type; const unresolvedProps = workInProgress.pendingProps; @@ -3536,27 +3312,19 @@ function beginWork( workInProgress, type, resolvedProps, - renderExpirationTime, + renderLanes, ); } case Fragment: - return updateFragment(current, workInProgress, renderExpirationTime); + return updateFragment(current, workInProgress, renderLanes); case Mode: - return updateMode(current, workInProgress, renderExpirationTime); + return updateMode(current, workInProgress, renderLanes); case Profiler: - return updateProfiler(current, workInProgress, renderExpirationTime); + return updateProfiler(current, workInProgress, renderLanes); case ContextProvider: - return updateContextProvider( - current, - workInProgress, - renderExpirationTime, - ); + return updateContextProvider(current, workInProgress, renderLanes); case ContextConsumer: - return updateContextConsumer( - current, - workInProgress, - renderExpirationTime, - ); + return updateContextConsumer(current, workInProgress, renderLanes); case MemoComponent: { const type = workInProgress.type; const unresolvedProps = workInProgress.pendingProps; @@ -3581,8 +3349,8 @@ function beginWork( workInProgress, type, resolvedProps, - updateExpirationTime, - renderExpirationTime, + updateLanes, + renderLanes, ); } case SimpleMemoComponent: { @@ -3591,8 +3359,8 @@ function beginWork( workInProgress, workInProgress.type, workInProgress.pendingProps, - updateExpirationTime, - renderExpirationTime, + updateLanes, + renderLanes, ); } case IncompleteClassComponent: { @@ -3607,33 +3375,21 @@ function beginWork( workInProgress, Component, resolvedProps, - renderExpirationTime, + renderLanes, ); } case SuspenseListComponent: { - return updateSuspenseListComponent( - current, - workInProgress, - renderExpirationTime, - ); + return updateSuspenseListComponent(current, workInProgress, renderLanes); } case FundamentalComponent: { if (enableFundamentalAPI) { - return updateFundamentalComponent( - current, - workInProgress, - renderExpirationTime, - ); + return updateFundamentalComponent(current, workInProgress, renderLanes); } break; } case ScopeComponent: { if (enableScopeAPI) { - return updateScopeComponent( - current, - workInProgress, - renderExpirationTime, - ); + return updateScopeComponent(current, workInProgress, renderLanes); } break; } @@ -3641,29 +3397,15 @@ function beginWork( if (enableBlocksAPI) { const block = workInProgress.type; const props = workInProgress.pendingProps; - return updateBlock( - current, - workInProgress, - block, - props, - renderExpirationTime, - ); + return updateBlock(current, workInProgress, block, props, renderLanes); } break; } case OffscreenComponent: { - return updateOffscreenComponent( - current, - workInProgress, - renderExpirationTime, - ); + return updateOffscreenComponent(current, workInProgress, renderLanes); } case LegacyHiddenComponent: { - return updateLegacyHiddenComponent( - current, - workInProgress, - renderExpirationTime, - ); + return updateLegacyHiddenComponent(current, workInProgress, renderLanes); } } invariant( diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.new.js b/packages/react-reconciler/src/ReactFiberClassComponent.new.js index b568278d452b..20e544b00a05 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.new.js @@ -8,7 +8,7 @@ */ import type {Fiber} from './ReactInternalTypes'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; +import type {Lanes} from './ReactFiberLane'; import type {UpdateQueue} from './ReactUpdateQueue.new'; import * as React from 'react'; @@ -40,7 +40,7 @@ import { initializeUpdateQueue, cloneUpdateQueue, } from './ReactUpdateQueue.new'; -import {NoWork, isSameExpirationTime} from './ReactFiberExpirationTime.new'; +import {NoLanes} from './ReactFiberLane'; import { cacheContext, getMaskedContext, @@ -51,7 +51,7 @@ import { import {readContext} from './ReactFiberNewContext.new'; import { requestEventTime, - requestUpdateExpirationTime, + requestUpdateLane, scheduleUpdateOnFiber, } from './ReactFiberWorkLoop.new'; import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig'; @@ -177,12 +177,7 @@ export function applyDerivedStateFromProps( // Once the update queue is empty, persist the derived state onto the // base state. - if ( - isSameExpirationTime( - workInProgress.expirationTime_opaque, - (NoWork: ExpirationTimeOpaque), - ) - ) { + if (workInProgress.lanes === NoLanes) { // Queue is always non-null for classes const updateQueue: UpdateQueue = (workInProgress.updateQueue: any); updateQueue.baseState = memoizedState; @@ -195,9 +190,9 @@ const classComponentUpdater = { const fiber = getInstance(inst); const eventTime = requestEventTime(); const suspenseConfig = requestCurrentSuspenseConfig(); - const expirationTime = requestUpdateExpirationTime(fiber, suspenseConfig); + const lane = requestUpdateLane(fiber, suspenseConfig); - const update = createUpdate(eventTime, expirationTime, suspenseConfig); + const update = createUpdate(eventTime, lane, suspenseConfig); update.payload = payload; if (callback !== undefined && callback !== null) { if (__DEV__) { @@ -207,15 +202,15 @@ const classComponentUpdater = { } enqueueUpdate(fiber, update); - scheduleUpdateOnFiber(fiber, expirationTime); + scheduleUpdateOnFiber(fiber, lane); }, enqueueReplaceState(inst, payload, callback) { const fiber = getInstance(inst); const eventTime = requestEventTime(); const suspenseConfig = requestCurrentSuspenseConfig(); - const expirationTime = requestUpdateExpirationTime(fiber, suspenseConfig); + const lane = requestUpdateLane(fiber, suspenseConfig); - const update = createUpdate(eventTime, expirationTime, suspenseConfig); + const update = createUpdate(eventTime, lane, suspenseConfig); update.tag = ReplaceState; update.payload = payload; @@ -227,15 +222,15 @@ const classComponentUpdater = { } enqueueUpdate(fiber, update); - scheduleUpdateOnFiber(fiber, expirationTime); + scheduleUpdateOnFiber(fiber, lane); }, enqueueForceUpdate(inst, callback) { const fiber = getInstance(inst); const eventTime = requestEventTime(); const suspenseConfig = requestCurrentSuspenseConfig(); - const expirationTime = requestUpdateExpirationTime(fiber, suspenseConfig); + const lane = requestUpdateLane(fiber, suspenseConfig); - const update = createUpdate(eventTime, expirationTime, suspenseConfig); + const update = createUpdate(eventTime, lane, suspenseConfig); update.tag = ForceUpdate; if (callback !== undefined && callback !== null) { @@ -246,7 +241,7 @@ const classComponentUpdater = { } enqueueUpdate(fiber, update); - scheduleUpdateOnFiber(fiber, expirationTime); + scheduleUpdateOnFiber(fiber, lane); }, }; @@ -771,7 +766,7 @@ function mountClassInstance( workInProgress: Fiber, ctor: any, newProps: any, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ): void { if (__DEV__) { checkClassInstance(workInProgress, ctor, newProps); @@ -823,7 +818,7 @@ function mountClassInstance( } } - processUpdateQueue(workInProgress, newProps, instance, renderExpirationTime); + processUpdateQueue(workInProgress, newProps, instance, renderLanes); instance.state = workInProgress.memoizedState; const getDerivedStateFromProps = ctor.getDerivedStateFromProps; @@ -848,12 +843,7 @@ function mountClassInstance( callComponentWillMount(workInProgress, instance); // If we had additional state updates during this life-cycle, let's // process them now. - processUpdateQueue( - workInProgress, - newProps, - instance, - renderExpirationTime, - ); + processUpdateQueue(workInProgress, newProps, instance, renderLanes); instance.state = workInProgress.memoizedState; } @@ -866,7 +856,7 @@ function resumeMountClassInstance( workInProgress: Fiber, ctor: any, newProps: any, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ): boolean { const instance = workInProgress.stateNode; @@ -917,7 +907,7 @@ function resumeMountClassInstance( const oldState = workInProgress.memoizedState; let newState = (instance.state = oldState); - processUpdateQueue(workInProgress, newProps, instance, renderExpirationTime); + processUpdateQueue(workInProgress, newProps, instance, renderLanes); newState = workInProgress.memoizedState; if ( oldProps === newProps && @@ -1001,7 +991,7 @@ function updateClassInstance( workInProgress: Fiber, ctor: any, newProps: any, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ): boolean { const instance = workInProgress.stateNode; @@ -1058,7 +1048,7 @@ function updateClassInstance( const oldState = workInProgress.memoizedState; let newState = (instance.state = oldState); - processUpdateQueue(workInProgress, newProps, instance, renderExpirationTime); + processUpdateQueue(workInProgress, newProps, instance, renderLanes); newState = workInProgress.memoizedState; if ( diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 8e71d3a99bef..91e74031dded 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -17,7 +17,7 @@ import type { } from './ReactFiberHostConfig'; import type {Fiber} from './ReactInternalTypes'; import type {FiberRoot} from './ReactInternalTypes'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; +import type {Lanes} from './ReactFiberLane'; import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; import type {UpdateQueue} from './ReactUpdateQueue.new'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new'; @@ -516,7 +516,7 @@ function commitLifeCycles( finishedRoot: FiberRoot, current: Fiber | null, finishedWork: Fiber, - committedExpirationTime: ExpirationTimeOpaque, + committedLanes: Lanes, ): void { switch (finishedWork.tag) { case FunctionComponent: diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index e2280673232b..e420bb6916c7 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -8,7 +8,7 @@ */ import type {Fiber} from './ReactInternalTypes'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; +import type {Lanes} from './ReactFiberLane'; import type { ReactFundamentalComponentInstance, ReactScopeInstance, @@ -133,10 +133,10 @@ import { renderDidSuspend, renderDidSuspendDelayIfPossible, renderHasNotSuspendedYet, - popRenderExpirationTime, + popRenderLanes, } from './ReactFiberWorkLoop.new'; import {createFundamentalStateInstance} from './ReactFiberFundamental.new'; -import {Never, isSameOrHigherPriority} from './ReactFiberExpirationTime.new'; +import {OffscreenLane} from './ReactFiberLane'; import {resetChildFibers} from './ReactChildFiber.new'; import {updateDeprecatedEventListeners} from './ReactFiberDeprecatedEvents.new'; import {createScopeInstance} from './ReactFiberScope.new'; @@ -644,7 +644,7 @@ function cutOffTailIfNeeded( function completeWork( current: Fiber | null, workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ): Fiber | null { const newProps = workInProgress.pendingProps; @@ -857,7 +857,7 @@ function completeWork( ); prepareToHydrateHostSuspenseInstance(workInProgress); if (enableSchedulerTracing) { - markSpawnedWork((Never: ExpirationTimeOpaque)); + markSpawnedWork(OffscreenLane); } return null; } else { @@ -882,7 +882,7 @@ function completeWork( if ((workInProgress.effectTag & DidCapture) !== NoEffect) { // Something suspended. Re-render with the fallback children. - workInProgress.expirationTime_opaque = renderExpirationTime; + workInProgress.lanes = renderLanes; // Do not reset the effect list. return workInProgress; } @@ -1051,7 +1051,7 @@ function completeWork( } workInProgress.lastEffect = renderState.lastEffect; // Reset the child fibers to their original state. - resetChildFibers(workInProgress, renderExpirationTime); + resetChildFibers(workInProgress, renderLanes); // Set up the Suspense Context to force suspense and immediately // rerender the children. @@ -1111,10 +1111,7 @@ function completeWork( // the expiration. now() * 2 - renderState.renderingStartTime > renderState.tailExpiration && - !isSameOrHigherPriority( - (Never: ExpirationTimeOpaque), - renderExpirationTime, - ) + renderLanes !== OffscreenLane ) { // We have now passed our CPU deadline and we'll just give up further // attempts to render the main content and only render fallbacks. @@ -1129,9 +1126,9 @@ function completeWork( // them, then they really have the same priority as this render. // So we'll pick it back up the very next render pass once we've had // an opportunity to yield for paint. - workInProgress.expirationTime_opaque = renderExpirationTime; + workInProgress.lanes = renderLanes; if (enableSchedulerTracing) { - markSpawnedWork(renderExpirationTime); + markSpawnedWork(renderLanes); } } } @@ -1297,7 +1294,7 @@ function completeWork( break; case OffscreenComponent: case LegacyHiddenComponent: { - popRenderExpirationTime(workInProgress); + popRenderLanes(workInProgress); if (current !== null) { const nextState: OffscreenState | null = workInProgress.memoizedState; const prevState: OffscreenState | null = current.memoizedState; diff --git a/packages/react-reconciler/src/ReactFiberDeprecatedEvents.new.js b/packages/react-reconciler/src/ReactFiberDeprecatedEvents.new.js index f77179fdb105..26e055718cf8 100644 --- a/packages/react-reconciler/src/ReactFiberDeprecatedEvents.new.js +++ b/packages/react-reconciler/src/ReactFiberDeprecatedEvents.new.js @@ -19,7 +19,7 @@ import { DEPRECATED_mountResponderInstance, DEPRECATED_unmountResponderInstance, } from './ReactFiberHostConfig'; -import {NoWork} from './ReactFiberExpirationTime.new'; +import {NoLanes} from './ReactFiberLane'; import {REACT_RESPONDER_TYPE} from 'shared/ReactSymbols'; @@ -154,7 +154,7 @@ export function updateDeprecatedEventListeners( if (listeners != null) { if (dependencies === null) { dependencies = fiber.dependencies_new = { - expirationTime: NoWork, + lanes: NoLanes, firstContext: null, responders: new Map(), }; diff --git a/packages/react-reconciler/src/ReactFiberExpirationTime.new.js b/packages/react-reconciler/src/ReactFiberExpirationTime.new.js deleted file mode 100644 index d8d3cfd69d5a..000000000000 --- a/packages/react-reconciler/src/ReactFiberExpirationTime.new.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {ReactPriorityLevel} from './ReactInternalTypes'; - -import {MAX_SIGNED_31_BIT_INT} from './MaxInts'; - -import { - ImmediatePriority, - UserBlockingPriority, - NormalPriority, - IdlePriority, -} from './SchedulerWithReactIntegration.new'; - -export opaque type ExpirationTimeOpaque = number; - -export const NoWork: ExpirationTimeOpaque = 0; -// TODO: Think of a better name for Never. The key difference with Idle is that -// Never work can be committed in an inconsistent state without tearing the UI. -// The main example is offscreen content, like a hidden subtree. So one possible -// name is Offscreen. However, it also includes dehydrated Suspense boundaries, -// which are inconsistent in the sense that they haven't finished yet, but -// aren't visibly inconsistent because the server rendered HTML matches what the -// hydrated tree would look like. -export const Never: ExpirationTimeOpaque = 1; -// Idle is slightly higher priority than Never. It must completely finish in -// order to be consistent. -export const Idle: ExpirationTimeOpaque = 2; -// Continuous Hydration is slightly higher than Idle and is used to increase -// priority of hover targets. -export const ContinuousHydration: ExpirationTimeOpaque = 3; -export const LongTransition: ExpirationTimeOpaque = 49999; -export const ShortTransition: ExpirationTimeOpaque = 99999; -export const DefaultUpdateTime: ExpirationTimeOpaque = 1073741296; -export const UserBlockingUpdateTime: ExpirationTimeOpaque = 1073741761; -export const Sync: ExpirationTimeOpaque = MAX_SIGNED_31_BIT_INT; -export const Batched: ExpirationTimeOpaque = Sync - 1; - -// Accounts for -1 trick to bump updates into a different batch -const ADJUSTMENT_OFFSET = 5; - -export function inferPriorityFromExpirationTime( - expirationTime: ExpirationTimeOpaque, -): ReactPriorityLevel { - if (expirationTime >= Batched - ADJUSTMENT_OFFSET) { - return ImmediatePriority; - } - if (expirationTime >= UserBlockingUpdateTime - ADJUSTMENT_OFFSET) { - return UserBlockingPriority; - } - if (expirationTime >= LongTransition - ADJUSTMENT_OFFSET) { - return NormalPriority; - } - - // TODO: Handle LowPriority. Maybe should give it NormalPriority since Idle is - // very agressively deprioritized. - - // Assume anything lower has idle priority - return IdlePriority; -} - -export function isSameOrHigherPriority( - a: ExpirationTimeOpaque, - b: ExpirationTimeOpaque, -) { - return a >= b; -} - -export function isSameExpirationTime( - a: ExpirationTimeOpaque, - b: ExpirationTimeOpaque, -) { - return a === b; -} - -export function bumpPriorityHigher( - a: ExpirationTimeOpaque, -): ExpirationTimeOpaque { - return a + 1; -} - -export function bumpPriorityLower( - a: ExpirationTimeOpaque, -): ExpirationTimeOpaque { - return a - 1; -} diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 818a7e529bff..64c17405dc3f 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -16,7 +16,7 @@ import type { ReactEventResponderListener, } from 'shared/ReactTypes'; import type {Fiber, Dispatcher} from './ReactInternalTypes'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; +import type {Lanes, Lane} from './ReactFiberLane'; import type {HookEffectTag} from './ReactHookEffectTags'; import type {SuspenseConfig} from './ReactFiberSuspenseConfig'; import type {ReactPriorityLevel} from './ReactInternalTypes'; @@ -25,14 +25,16 @@ import type {OpaqueIDType} from './ReactFiberHostConfig'; import ReactSharedInternals from 'shared/ReactSharedInternals'; -import {markRootExpiredAtTime} from './ReactFiberRoot.new'; -import { - NoWork, - Sync, - isSameOrHigherPriority, - isSameExpirationTime, -} from './ReactFiberExpirationTime.new'; import {NoMode, BlockingMode} from './ReactTypeOfMode'; +import { + NoLane, + NoLanes, + isSubsetOfLanes, + mergeLanes, + removeLanes, + markRootExpired, + markRootMutableRead, +} from './ReactFiberLane'; import {readContext} from './ReactFiberNewContext.new'; import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents.new'; import { @@ -47,13 +49,13 @@ import { import { getWorkInProgressRoot, scheduleUpdateOnFiber, - requestUpdateExpirationTime, + requestUpdateLane, requestEventTime, warnIfNotCurrentlyActingEffectsInDEV, warnIfNotCurrentlyActingUpdatesInDev, warnIfNotScopedWithMatchingAct, markRenderEventTimeAndConfig, - markUnprocessedUpdateTime, + markSkippedUpdateLanes, } from './ReactFiberWorkLoop.new'; import invariant from 'shared/invariant'; @@ -74,10 +76,8 @@ import { makeOpaqueHydratingObject, } from './ReactFiberHostConfig'; import { - getLastPendingExpirationTime, getWorkInProgressVersion, markSourceAsDirty, - setPendingExpirationTime, setWorkInProgressVersion, warnAboutMultipleRenderersDEV, } from './ReactMutableSource.new'; @@ -89,7 +89,7 @@ type Update = {| // TODO: Temporary field. Will remove this by storing a map of // transition -> start time on the root. eventTime: number, - expirationTime: ExpirationTimeOpaque, + lane: Lane, suspenseConfig: null | SuspenseConfig, action: A, eagerReducer: ((S, A) => S) | null, @@ -156,7 +156,7 @@ type BasicStateAction = (S => S) | S; type Dispatch = A => void; // These are set right before calling the component. -let renderExpirationTime: ExpirationTimeOpaque = NoWork; +let renderLanes: Lanes = NoLanes; // The work-in-progress fiber. I've named it differently to distinguish it from // the work-in-progress hook. let currentlyRenderingFiber: Fiber = (null: any); @@ -347,9 +347,9 @@ export function renderWithHooks( Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, - nextRenderExpirationTime: ExpirationTimeOpaque, + nextRenderLanes: Lanes, ): any { - renderExpirationTime = nextRenderExpirationTime; + renderLanes = nextRenderLanes; currentlyRenderingFiber = workInProgress; if (__DEV__) { @@ -365,7 +365,7 @@ export function renderWithHooks( workInProgress.memoizedState = null; workInProgress.updateQueue = null; - workInProgress.expirationTime_opaque = NoWork; + workInProgress.lanes = NoLanes; // The following should have already been reset // currentHook = null; @@ -454,7 +454,7 @@ export function renderWithHooks( const didRenderTooFewHooks = currentHook !== null && currentHook.next !== null; - renderExpirationTime = NoWork; + renderLanes = NoLanes; currentlyRenderingFiber = (null: any); currentHook = null; @@ -480,13 +480,11 @@ export function renderWithHooks( export function bailoutHooks( current: Fiber, workInProgress: Fiber, - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, ) { workInProgress.updateQueue = current.updateQueue; workInProgress.effectTag &= ~(PassiveEffect | UpdateEffect); - if (isSameOrHigherPriority(expirationTime, current.expirationTime_opaque)) { - current.expirationTime_opaque = NoWork; - } + current.lanes = removeLanes(current.lanes, lanes); } export function resetHooksAfterThrow(): void { @@ -514,7 +512,7 @@ export function resetHooksAfterThrow(): void { didScheduleRenderPhaseUpdate = false; } - renderExpirationTime = NoWork; + renderLanes = NoLanes; currentlyRenderingFiber = (null: any); currentHook = null; @@ -708,15 +706,15 @@ function updateReducer( let update = first; do { const suspenseConfig = update.suspenseConfig; - const updateExpirationTime = update.expirationTime; + const updateLane = update.lane; const updateEventTime = update.eventTime; - if (!isSameOrHigherPriority(updateExpirationTime, renderExpirationTime)) { + if (!isSubsetOfLanes(renderLanes, updateLane)) { // Priority is insufficient. Skip this update. If this is the first // skipped update, the previous update/state is the new base // update/state. const clone: Update = { eventTime: updateEventTime, - expirationTime: updateExpirationTime, + lane: updateLane, suspenseConfig: suspenseConfig, action: update.action, eagerReducer: update.eagerReducer, @@ -730,22 +728,23 @@ function updateReducer( newBaseQueueLast = newBaseQueueLast.next = clone; } // Update the remaining priority in the queue. - if ( - !isSameOrHigherPriority( - currentlyRenderingFiber.expirationTime_opaque, - updateExpirationTime, - ) - ) { - currentlyRenderingFiber.expirationTime_opaque = updateExpirationTime; - markUnprocessedUpdateTime(updateExpirationTime); - } + // TODO: Don't need to accumulate this. Instead, we can remove + // renderLanes from the original lanes. + currentlyRenderingFiber.lanes = mergeLanes( + currentlyRenderingFiber.lanes, + updateLane, + ); + markSkippedUpdateLanes(updateLane); } else { // This update does have sufficient priority. if (newBaseQueueLast !== null) { const clone: Update = { eventTime: updateEventTime, - expirationTime: Sync, // This update is going to be committed so we never want uncommit it. + // This update is going to be committed so we never want uncommit + // it. Using NoLane works because 0 is a subset of all bitmasks, so + // this will never be skipped by the check above. + lane: NoLane, suspenseConfig: update.suspenseConfig, action: update.action, eagerReducer: update.eagerReducer, @@ -882,26 +881,28 @@ function readFromUnsubcribedMutableSource( // we can use it alone to determine if we can safely read from the source. const currentRenderVersion = getWorkInProgressVersion(source); if (currentRenderVersion !== null) { + // It's safe to read if the store hasn't been mutated since the last time + // we read something. isSafeToReadFromSource = currentRenderVersion === version; } else { - // If there's no version, then we should fallback to checking the update time. - const pendingExpirationTime = getLastPendingExpirationTime(root); - - if ( - isSameExpirationTime( - pendingExpirationTime, - (NoWork: ExpirationTimeOpaque), - ) - ) { - isSafeToReadFromSource = true; - } else { - // If the source has pending updates, we can use the current render's expiration - // time to determine if it's safe to read again from the source. - isSafeToReadFromSource = isSameOrHigherPriority( - pendingExpirationTime, - renderExpirationTime, - ); - } + // If there's no version, then this is the first time we've read from the + // source during the current render pass, so we need to do a bit more work. + // What we need to determine is if there are any hooks that already + // subscribed to the source, and if so, whether there are any pending + // mutations that haven't been synchronized yet. + // + // If there are no pending mutations, then `root.mutableReadLanes` will be + // empty, and we know we can safely read. + // + // If there *are* pending mutations, we may still be able to safely read + // if the currently rendering lanes are inclusive of the pending mutation + // lanes, since that guarantees that the value we're about to read from + // the source is consistent with the values that we read during the most + // recent mutation. + isSafeToReadFromSource = isSubsetOfLanes( + renderLanes, + root.mutableReadLanes, + ); if (isSafeToReadFromSource) { // If it's safe to read from this source during the current render, @@ -994,19 +995,16 @@ function useMutableSource( setSnapshot(maybeNewSnapshot); const suspenseConfig = requestCurrentSuspenseConfig(); - const expirationTime = requestUpdateExpirationTime( - fiber, - suspenseConfig, - ); - setPendingExpirationTime(root, expirationTime); + const lane = requestUpdateLane(fiber, suspenseConfig); + markRootMutableRead(root, lane); // If the source mutated between render and now, // there may be state updates already scheduled from the old getSnapshot. // Those updates should not commit without this value. // There is no mechanism currently to associate these updates though, // so for now we fall back to synchronously flushing all pending updates. - // TODO: Improve this later. - markRootExpiredAtTime(root, getLastPendingExpirationTime(root)); + // TODO: This should entangle the lanes instead of expiring everything. + markRootExpired(root, root.mutableReadLanes); } } }, [getSnapshot, source, subscribe]); @@ -1022,12 +1020,9 @@ function useMutableSource( // Record a pending mutable source update with the same expiration time. const suspenseConfig = requestCurrentSuspenseConfig(); - const expirationTime = requestUpdateExpirationTime( - fiber, - suspenseConfig, - ); + const lane = requestUpdateLane(fiber, suspenseConfig); - setPendingExpirationTime(root, expirationTime); + markRootMutableRead(root, lane); } catch (error) { // A selector might throw after a source mutation. // e.g. it might try to read from a part of the store that no longer exists. @@ -1646,11 +1641,11 @@ function dispatchAction( const eventTime = requestEventTime(); const suspenseConfig = requestCurrentSuspenseConfig(); - const expirationTime = requestUpdateExpirationTime(fiber, suspenseConfig); + const lane = requestUpdateLane(fiber, suspenseConfig); const update: Update = { eventTime, - expirationTime, + lane, suspenseConfig, action, eagerReducer: null, @@ -1678,18 +1673,10 @@ function dispatchAction( // queue -> linked list of updates. After this render pass, we'll restart // and apply the stashed updates on top of the work-in-progress hook. didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; - update.expirationTime = renderExpirationTime; } else { if ( - isSameExpirationTime( - fiber.expirationTime_opaque, - (NoWork: ExpirationTimeOpaque), - ) && - (alternate === null || - isSameExpirationTime( - alternate.expirationTime_opaque, - (NoWork: ExpirationTimeOpaque), - )) + fiber.lanes === NoLanes && + (alternate === null || alternate.lanes === NoLanes) ) { // The queue is currently empty, which means we can eagerly compute the // next state before entering the render phase. If the new state is the @@ -1733,7 +1720,7 @@ function dispatchAction( warnIfNotCurrentlyActingUpdatesInDev(fiber); } } - scheduleUpdateOnFiber(fiber, expirationTime); + scheduleUpdateOnFiber(fiber, lane); } } diff --git a/packages/react-reconciler/src/ReactFiberHotReloading.new.js b/packages/react-reconciler/src/ReactFiberHotReloading.new.js index 2c834382eea9..21819bf9edbc 100644 --- a/packages/react-reconciler/src/ReactFiberHotReloading.new.js +++ b/packages/react-reconciler/src/ReactFiberHotReloading.new.js @@ -12,7 +12,6 @@ import type {Fiber} from './ReactInternalTypes'; import type {FiberRoot} from './ReactInternalTypes'; import type {Instance} from './ReactFiberHostConfig'; import type {ReactNodeList} from 'shared/ReactTypes'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; import { flushSync, @@ -21,7 +20,7 @@ import { } from './ReactFiberWorkLoop.new'; import {updateContainer, syncUpdates} from './ReactFiberReconciler.new'; import {emptyContextObject} from './ReactFiberContext.new'; -import {Sync} from './ReactFiberExpirationTime.new'; +import {SyncLane} from './ReactFiberLane'; import { ClassComponent, FunctionComponent, @@ -320,7 +319,7 @@ function scheduleFibersWithFamiliesRecursively( fiber._debugNeedsRemount = true; } if (needsRemount || needsRender) { - scheduleUpdateOnFiber(fiber, (Sync: ExpirationTimeOpaque)); + scheduleUpdateOnFiber(fiber, SyncLane); } if (child !== null && !needsRemount) { scheduleFibersWithFamiliesRecursively( diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js index f1ed2e73cccd..3f4c83da829b 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js @@ -55,7 +55,7 @@ import { didNotFindHydratableSuspenseInstance, } from './ReactFiberHostConfig'; import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags'; -import {Never} from './ReactFiberExpirationTime.new'; +import {OffscreenLane} from './ReactFiberLane'; // The deepest Fiber on the stack involved in a hydration context. // This may have been an insertion or a hydration. @@ -231,7 +231,7 @@ function tryHydrate(fiber, nextInstance) { if (suspenseInstance !== null) { const suspenseState: SuspenseState = { dehydrated: suspenseInstance, - retryTime: Never, + retryLane: OffscreenLane, }; fiber.memoizedState = suspenseState; // Store the dehydrated fragment as a child fiber. diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index 4e389fe737b8..65d35d764011 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -7,16 +7,682 @@ * @flow */ -export opaque type LanePriority = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; - -export const SyncLanePriority: LanePriority = 10; -export const SyncBatchedLanePriority: LanePriority = 9; -export const InputDiscreteLanePriority: LanePriority = 8; -export const InputContinuousLanePriority: LanePriority = 7; -export const DefaultLanePriority: LanePriority = 6; -export const TransitionShortLanePriority: LanePriority = 5; -export const TransitionLongLanePriority: LanePriority = 4; -export const HydrationContinuousLanePriority: LanePriority = 3; -export const IdleLanePriority: LanePriority = 2; -export const OffscreenLanePriority: LanePriority = 1; -export const NoLanePriority: LanePriority = 0; +import type {FiberRoot, ReactPriorityLevel} from './ReactInternalTypes'; + +export opaque type LanePriority = + | 0 + | 1 + | 2 + | 3 + | 4 + | 5 + | 6 + | 7 + | 8 + | 9 + | 10 + | 11 + | 12 + | 13 + | 14 + | 15 + | 16; +export opaque type Lanes = number; +export opaque type Lane = number; + +import invariant from 'shared/invariant'; + +import { + ImmediatePriority as ImmediateSchedulerPriority, + UserBlockingPriority as UserBlockingSchedulerPriority, + NormalPriority as NormalSchedulerPriority, + LowPriority as LowSchedulerPriority, + IdlePriority as IdleSchedulerPriority, + NoPriority as NoSchedulerPriority, +} from './SchedulerWithReactIntegration.new'; + +export const SyncLanePriority: LanePriority = 16; +const SyncBatchedLanePriority: LanePriority = 15; + +const InputDiscreteHydrationLanePriority: LanePriority = 14; +export const InputDiscreteLanePriority: LanePriority = 13; + +const InputContinuousHydrationLanePriority: LanePriority = 12; +const InputContinuousLanePriority: LanePriority = 11; + +const DefaultHydrationLanePriority: LanePriority = 10; +const DefaultLanePriority: LanePriority = 9; + +const TransitionShortHydrationLanePriority: LanePriority = 8; +export const TransitionShortLanePriority: LanePriority = 7; + +const TransitionLongHydrationLanePriority: LanePriority = 6; +export const TransitionLongLanePriority: LanePriority = 5; + +const SelectiveHydrationLanePriority: LanePriority = 4; + +const IdleHydrationLanePriority: LanePriority = 3; +const IdleLanePriority: LanePriority = 2; + +const OffscreenLanePriority: LanePriority = 1; + +const NoLanePriority: LanePriority = 0; + +const TotalLanes = 31; + +export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000; +export const NoLane: Lane = /* */ 0b0000000000000000000000000000000; + +export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001; +const SyncUpdateRangeEnd = 1; +export const SyncBatchedLane: Lane = /* */ 0b0000000000000000000000000000010; +const SyncBatchedUpdateRangeEnd = 2; + +export const InputDiscreteHydrationLane: Lane = /* */ 0b0000000000000000000000000000100; +const InputDiscreteLanes: Lanes = /* */ 0b0000000000000000000000000011100; +const InputDiscreteUpdateRangeStart = 3; +const InputDiscreteUpdateRangeEnd = 5; + +const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000100000; +const InputContinuousLanes: Lanes = /* */ 0b0000000000000000000000011100000; +const InputContinuousUpdateRangeStart = 6; +const InputContinuousUpdateRangeEnd = 8; + +export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000100000000; +const DefaultLanes: Lanes = /* */ 0b0000000000000000011111100000000; +const DefaultUpdateRangeStart = 9; +const DefaultUpdateRangeEnd = 14; + +const TransitionShortHydrationLane: Lane = /* */ 0b0000000000000000100000000000000; +const TransitionShortLanes: Lanes = /* */ 0b0000000000011111100000000000000; +const TransitionShortUpdateRangeStart = 15; +const TransitionShortUpdateRangeEnd = 20; + +const TransitionLongHydrationLane: Lane = /* */ 0b0000000000100000000000000000000; +const TransitionLongLanes: Lanes = /* */ 0b0000011111100000000000000000000; +const TransitionLongUpdateRangeStart = 21; +const TransitionLongUpdateRangeEnd = 26; + +export const SelectiveHydrationLane: Lane = /* */ 0b0000110000000000000000000000000; +const SelectiveHydrationRangeEnd = 27; + +// Includes all non-Idle updates +const UpdateRangeEnd = 27; +const NonIdleLanes = /* */ 0b0000111111111111111111111111111; + +export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000; +const IdleLanes: Lanes = /* */ 0b0111000000000000000000000000000; +const IdleUpdateRangeStart = 28; +const IdleUpdateRangeEnd = 30; + +export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000; + +// "Registers" used to "return" multiple values +// Used by getHighestPriorityLanes and getNextLanes: +let return_highestLanePriority: LanePriority = DefaultLanePriority; +let return_updateRangeEnd: number = -1; + +function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes { + if ((SyncLane & lanes) !== NoLanes) { + return_highestLanePriority = SyncLanePriority; + return_updateRangeEnd = SyncUpdateRangeEnd; + return SyncLane; + } + if ((SyncBatchedLane & lanes) !== NoLanes) { + return_highestLanePriority = SyncBatchedLanePriority; + return_updateRangeEnd = SyncBatchedUpdateRangeEnd; + return SyncBatchedLane; + } + const inputDiscreteLanes = InputDiscreteLanes & lanes; + if (inputDiscreteLanes !== NoLanes) { + if (inputDiscreteLanes & InputDiscreteHydrationLane) { + return_highestLanePriority = InputDiscreteHydrationLanePriority; + return_updateRangeEnd = InputDiscreteUpdateRangeStart; + return InputDiscreteHydrationLane; + } else { + return_highestLanePriority = InputDiscreteLanePriority; + return_updateRangeEnd = InputDiscreteUpdateRangeEnd; + return inputDiscreteLanes; + } + } + const inputContinuousLanes = InputContinuousLanes & lanes; + if (inputContinuousLanes !== NoLanes) { + if (inputContinuousLanes & InputContinuousHydrationLane) { + return_highestLanePriority = InputContinuousHydrationLanePriority; + return_updateRangeEnd = InputContinuousUpdateRangeStart; + return InputContinuousHydrationLane; + } else { + return_highestLanePriority = InputContinuousLanePriority; + return_updateRangeEnd = InputContinuousUpdateRangeEnd; + return inputContinuousLanes; + } + } + const defaultLanes = DefaultLanes & lanes; + if (defaultLanes !== NoLanes) { + if (defaultLanes & DefaultHydrationLane) { + return_highestLanePriority = DefaultHydrationLanePriority; + return_updateRangeEnd = DefaultUpdateRangeStart; + return DefaultHydrationLane; + } else { + return_highestLanePriority = DefaultLanePriority; + return_updateRangeEnd = DefaultUpdateRangeEnd; + return defaultLanes; + } + } + const transitionShortLanes = TransitionShortLanes & lanes; + if (transitionShortLanes !== NoLanes) { + if (transitionShortLanes & TransitionShortHydrationLane) { + return_highestLanePriority = TransitionShortHydrationLanePriority; + return_updateRangeEnd = TransitionShortUpdateRangeStart; + return TransitionShortHydrationLane; + } else { + return_highestLanePriority = TransitionShortLanePriority; + return_updateRangeEnd = TransitionShortUpdateRangeEnd; + return transitionShortLanes; + } + } + const transitionLongLanes = TransitionLongLanes & lanes; + if (transitionLongLanes !== NoLanes) { + if (transitionLongLanes & TransitionLongHydrationLane) { + return_highestLanePriority = TransitionLongHydrationLanePriority; + return_updateRangeEnd = TransitionLongUpdateRangeStart; + return TransitionLongHydrationLane; + } else { + return_highestLanePriority = TransitionLongLanePriority; + return_updateRangeEnd = TransitionLongUpdateRangeEnd; + return transitionLongLanes; + } + } + if (lanes & SelectiveHydrationLane) { + return_highestLanePriority = SelectiveHydrationLanePriority; + return_updateRangeEnd = SelectiveHydrationRangeEnd; + return SelectiveHydrationLane; + } + const idleLanes = IdleLanes & lanes; + if (idleLanes !== NoLanes) { + if (idleLanes & IdleHydrationLane) { + return_highestLanePriority = IdleHydrationLanePriority; + return_updateRangeEnd = IdleUpdateRangeStart; + return IdleHydrationLane; + } else { + return_updateRangeEnd = IdleUpdateRangeEnd; + return idleLanes; + } + } + if ((OffscreenLane & lanes) !== NoLanes) { + return_highestLanePriority = OffscreenLanePriority; + return_updateRangeEnd = TotalLanes; + return OffscreenLane; + } + if (__DEV__) { + console.error('Should have found matching lanes. This is a bug in React.'); + } + // This shouldn't be reachable, but as a fallback, return the entire bitmask. + return_highestLanePriority = DefaultLanePriority; + return_updateRangeEnd = DefaultUpdateRangeEnd; + return lanes; +} + +export function schedulerPriorityToLanePriority( + schedulerPriorityLevel: ReactPriorityLevel, +): LanePriority { + switch (schedulerPriorityLevel) { + case ImmediateSchedulerPriority: + return SyncLanePriority; + case UserBlockingSchedulerPriority: + return InputContinuousLanePriority; + case NormalSchedulerPriority: + case LowSchedulerPriority: + // TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration. + return DefaultLanePriority; + case IdleSchedulerPriority: + return IdleLanePriority; + default: + return NoLanePriority; + } +} + +export function lanePriorityToSchedulerPriority( + lanePriority: LanePriority, +): ReactPriorityLevel { + switch (lanePriority) { + case SyncLanePriority: + case SyncBatchedLanePriority: + return ImmediateSchedulerPriority; + case InputDiscreteHydrationLanePriority: + case InputDiscreteLanePriority: + case InputContinuousHydrationLanePriority: + case InputContinuousLanePriority: + return UserBlockingSchedulerPriority; + case DefaultHydrationLanePriority: + case DefaultLanePriority: + case TransitionShortHydrationLanePriority: + case TransitionShortLanePriority: + case TransitionLongHydrationLanePriority: + case TransitionLongLanePriority: + case SelectiveHydrationLanePriority: + return NormalSchedulerPriority; + case IdleHydrationLanePriority: + case IdleLanePriority: + case OffscreenLanePriority: + return IdleSchedulerPriority; + case NoLanePriority: + return NoSchedulerPriority; + default: + invariant( + false, + 'Invalid update priority: %s. This is a bug in React.', + lanePriority, + ); + } +} + +export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes { + // Early bailout if there's no pending work left. + const pendingLanes = root.pendingLanes; + if (pendingLanes === NoLanes) { + return_highestLanePriority = NoLanePriority; + return NoLanes; + } + + let nextLanes = NoLanes; + let nextLanePriority = NoLanePriority; + let equalOrHigherPriorityLanes = NoLanes; + + const expiredLanes = root.expiredLanes; + const suspendedLanes = root.suspendedLanes; + const pingedLanes = root.pingedLanes; + + // Check if any work has expired. + if (expiredLanes !== NoLanes) { + nextLanes = expiredLanes; + nextLanePriority = return_highestLanePriority = SyncLanePriority; + equalOrHigherPriorityLanes = (getLowestPriorityLane(nextLanes) << 1) - 1; + } else { + // Do not work on any idle work until all the non-idle work has finished, + // even if the work is suspended. + const nonIdlePendingLanes = pendingLanes & NonIdleLanes; + if (nonIdlePendingLanes !== NoLanes) { + const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes; + if (nonIdleUnblockedLanes !== NoLanes) { + nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes); + nextLanePriority = return_highestLanePriority; + equalOrHigherPriorityLanes = (1 << return_updateRangeEnd) - 1; + } else { + const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes; + if (nonIdlePingedLanes !== NoLanes) { + nextLanes = getHighestPriorityLanes(nonIdlePingedLanes); + nextLanePriority = return_highestLanePriority; + equalOrHigherPriorityLanes = (1 << return_updateRangeEnd) - 1; + } + } + } else { + // The only remaining work is Idle. + const unblockedLanes = pendingLanes & ~suspendedLanes; + if (unblockedLanes !== NoLanes) { + nextLanes = getHighestPriorityLanes(unblockedLanes); + nextLanePriority = return_highestLanePriority; + equalOrHigherPriorityLanes = (1 << return_updateRangeEnd) - 1; + } else { + if (pingedLanes !== NoLanes) { + nextLanes = getHighestPriorityLanes(pingedLanes); + nextLanePriority = return_highestLanePriority; + equalOrHigherPriorityLanes = (1 << return_updateRangeEnd) - 1; + } + } + } + } + + if (nextLanes === NoLanes) { + // This should only be reachable if we're suspended + // TODO: Consider warning in this path if a fallback timer is not scheduled. + return NoLanes; + } + + // If there are higher priority lanes, we'll include them even if they + // are suspended. + nextLanes = pendingLanes & equalOrHigherPriorityLanes; + + // If we're already in the middle of a render, switching lanes will interrupt + // it and we'll lose our progress. We should only do this if the new lanes are + // higher priority. + if ( + wipLanes !== NoLanes && + wipLanes !== nextLanes && + // If we already suspended with a delay, then interrupting is fine. Don't + // bother waiting until the root is complete. + (wipLanes & suspendedLanes) === NoLanes + ) { + getHighestPriorityLanes(wipLanes); + const wipLanePriority = return_highestLanePriority; + if (nextLanePriority <= wipLanePriority) { + return wipLanes; + } else { + return_highestLanePriority = nextLanePriority; + } + } + + return nextLanes; +} + +// This returns the highest priority pending lanes regardless of whether they +// are suspended. +export function getHighestPriorityPendingLanes(root: FiberRoot) { + return getHighestPriorityLanes(root.pendingLanes); +} + +export function getLanesToRetrySynchronouslyOnError(root: FiberRoot): Lanes { + const everythingButOffscreen = root.pendingLanes & ~OffscreenLane; + if (everythingButOffscreen !== NoLanes) { + return everythingButOffscreen; + } + if (everythingButOffscreen & OffscreenLane) { + return OffscreenLane; + } + return NoLanes; +} + +export function returnNextLanesPriority() { + return return_highestLanePriority; +} +export function hasUpdatePriority(lanes: Lanes) { + return (lanes & NonIdleLanes) !== NoLanes; +} + +// To ensure consistency across multiple updates in the same event, this should +// be a pure function, so that it always returns the same lane for given inputs. +export function findUpdateLane( + lanePriority: LanePriority, + wipLanes: Lanes, +): Lane { + switch (lanePriority) { + case NoLanePriority: + break; + case SyncLanePriority: + return SyncLane; + case SyncBatchedLanePriority: + return SyncBatchedLane; + case InputDiscreteLanePriority: { + let lane = findLane( + InputDiscreteUpdateRangeStart, + UpdateRangeEnd, + wipLanes, + ); + if (lane === NoLane) { + lane = InputDiscreteHydrationLane; + } + return lane; + } + case InputContinuousLanePriority: { + let lane = findLane( + InputContinuousUpdateRangeStart, + UpdateRangeEnd, + wipLanes, + ); + if (lane === NoLane) { + lane = InputContinuousHydrationLane; + } + return lane; + } + case DefaultLanePriority: { + let lane = findLane(DefaultUpdateRangeStart, UpdateRangeEnd, wipLanes); + if (lane === NoLane) { + lane = DefaultHydrationLane; + } + return lane; + } + case TransitionShortLanePriority: + case TransitionLongLanePriority: + // Should be handled by findTransitionLane instead + break; + case IdleLanePriority: + let lane = findLane(IdleUpdateRangeStart, IdleUpdateRangeEnd, IdleLanes); + if (lane === NoLane) { + lane = IdleHydrationLane; + } + return lane; + default: + // The remaining priorities are not valid for updates + break; + } + invariant( + false, + 'Invalid update priority: %s. This is a bug in React.', + lanePriority, + ); +} + +// To ensure consistency across multiple updates in the same event, this should +// be pure function, so that it always returns the same lane for given inputs. +export function findTransitionLane( + lanePriority: LanePriority, + wipLanes: Lanes, + pendingLanes: Lanes, +): Lane { + if (lanePriority === TransitionShortLanePriority) { + let lane = findLane( + TransitionShortUpdateRangeStart, + TransitionShortUpdateRangeEnd, + wipLanes | pendingLanes, + ); + if (lane === NoLane) { + lane = findLane( + TransitionShortUpdateRangeStart, + TransitionShortUpdateRangeEnd, + wipLanes, + ); + if (lane === NoLane) { + lane = TransitionShortHydrationLane; + } + } + return lane; + } + if (lanePriority === TransitionLongLanePriority) { + let lane = findLane( + TransitionLongUpdateRangeStart, + TransitionLongUpdateRangeEnd, + wipLanes | pendingLanes, + ); + if (lane === NoLane) { + lane = findLane( + TransitionLongUpdateRangeStart, + TransitionLongUpdateRangeEnd, + wipLanes, + ); + if (lane === NoLane) { + lane = TransitionLongHydrationLane; + } + } + return lane; + } + invariant( + false, + 'Invalid transition priority: %s. This is a bug in React.', + lanePriority, + ); +} + +function findLane(start, end, skipLanes) { + // This finds the first bit between the `start` and `end` positions that isn't + // in `skipLanes`. + // TODO: This will always favor the rightmost bits. That's usually fine + // because any bit that's pending will be part of `skipLanes`, so we'll do our + // best to avoid accidental entanglement. However, lanes that are pending + // inside an Offscreen tree aren't considered "pending" at the root level. So + // they aren't included in `skipLanes`. So we should try not to favor any + // particular part of the range, perhaps by incrementing an offset for each + // distinct event. Must be the same within a single event, though. + const bitsInRange = ((1 << (end - start)) - 1) << start; + const possibleBits = bitsInRange & ~skipLanes; + const leastSignificantBit = possibleBits & -possibleBits; + return leastSignificantBit; +} + +function getLowestPriorityLane(lanes: Lanes): Lane { + // This finds the most significant non-zero bit. + const index = 31 - clz32(lanes); + return index < 0 ? NoLanes : 1 << index; +} + +export function pickArbitraryLane(lanes: Lanes): Lane { + return getLowestPriorityLane(lanes); +} + +export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) { + return (a & b) !== NoLanes; +} + +export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane) { + return (set & subset) === subset; +} + +export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes { + return a | b; +} + +export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes { + return set & ~subset; +} + +// Seems redundant, but it changes the type from a single lane (used for +// updates) to a group of lanes (used for flushing work). +export function laneToLanes(lane: Lane): Lanes { + return lane; +} + +export function higherPriorityLane(a: Lane, b: Lane) { + // This works because the bit ranges decrease in priority as you go left. + return a !== NoLane && a < b ? a : b; +} + +export function markRootUpdated(root: FiberRoot, updateLane: Lane) { + root.pendingLanes |= updateLane; + + // TODO: Theoretically, any update to any lane can unblock any other lane. But + // it's not practical to try every single possible combination. We need a + // heuristic to decide which lanes to attempt to render, and in which batches. + // For now, we use the same heuristic as in the old ExpirationTimes model: + // retry any lane at equal or lower priority, but don't try updates at higher + // priority without also including the lower priority updates. This works well + // when considering updates across different priority levels, but isn't + // sufficient for updates within the same priority, since we want to treat + // those updates as parallel. + + // Unsuspend any update at equal or lower priority. + const higherPriorityLanes = updateLane - 1; // Turns 0b1000 into 0b0111 + root.suspendedLanes &= higherPriorityLanes; + root.pingedLanes &= higherPriorityLanes; +} + +export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) { + root.suspendedLanes |= suspendedLanes; + root.pingedLanes &= ~suspendedLanes; +} + +export function markRootPinged(root: FiberRoot, pingedLanes: Lanes) { + root.pingedLanes |= root.suspendedLanes & pingedLanes; +} + +export function markRootExpired(root: FiberRoot, expiredLanes: Lanes) { + root.expiredLanes |= expiredLanes & root.pendingLanes; +} + +export function markDiscreteUpdatesExpired(root: FiberRoot) { + root.expiredLanes |= InputDiscreteLanes & root.pendingLanes; +} + +export function hasDiscreteLanes(lanes: Lanes) { + return (lanes & InputDiscreteLanes) !== NoLanes; +} + +export function markRootMutableRead(root: FiberRoot, updateLane: Lane) { + root.mutableReadLanes |= updateLane & root.pendingLanes; +} + +export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) { + root.pendingLanes = remainingLanes; + + // Let's try everything again + root.suspendedLanes = 0; + root.pingedLanes = 0; + + root.expiredLanes &= remainingLanes; + root.mutableReadLanes &= remainingLanes; +} + +export function getBumpedLaneForHydration( + root: FiberRoot, + renderLanes: Lanes, +): Lane { + getHighestPriorityLanes(renderLanes); + const highestLanePriority = return_highestLanePriority; + + let lane; + switch (highestLanePriority) { + case SyncLanePriority: + case SyncBatchedLanePriority: + lane = NoLane; + break; + case InputDiscreteHydrationLanePriority: + case InputDiscreteLanePriority: + lane = InputDiscreteHydrationLane; + break; + case InputContinuousHydrationLanePriority: + case InputContinuousLanePriority: + lane = InputContinuousHydrationLane; + break; + case DefaultHydrationLanePriority: + case DefaultLanePriority: + lane = DefaultHydrationLane; + break; + case TransitionShortHydrationLanePriority: + case TransitionShortLanePriority: + lane = TransitionShortHydrationLane; + break; + case TransitionLongHydrationLanePriority: + case TransitionLongLanePriority: + lane = TransitionLongHydrationLane; + break; + case SelectiveHydrationLanePriority: + lane = SelectiveHydrationLane; + break; + case IdleHydrationLanePriority: + case IdleLanePriority: + lane = IdleHydrationLane; + break; + case OffscreenLanePriority: + case NoLanePriority: + lane = NoLane; + break; + default: + invariant(false, 'Invalid lane: %s. This is a bug in React.', lane); + } + + // Check if the lane we chose is suspended. If so, that indicates that we + // already attempted and failed to hydrate at that level. Also check if we're + // already rendering that lane, which is rare but could happen. + if ((lane & (root.suspendedLanes | renderLanes)) !== NoLane) { + // Give up trying to hydrate and fall back to client render. + return NoLane; + } + + return lane; +} + +const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback; + +// Taken from: +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32 +const log = Math.log; +const LN2 = Math.LN2; +function clz32Fallback(x) { + // Let n be ToUint32(x). + // Let p be the number of leading zero bits in + // the 32-bit binary representation of n. + // Return p. + const asUint = x >>> 0; + if (asUint === 0) { + return 32; + } + return (31 - ((log(asUint) / LN2) | 0)) | 0; // the "| 0" acts like math.floor +} diff --git a/packages/react-reconciler/src/ReactFiberNewContext.new.js b/packages/react-reconciler/src/ReactFiberNewContext.new.js index 3c03586bb547..ab603a08e5f3 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.new.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.new.js @@ -10,7 +10,7 @@ import type {ReactContext} from 'shared/ReactTypes'; import type {Fiber, ContextDependency} from './ReactInternalTypes'; import type {StackCursor} from './ReactFiberStack.new'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; +import type {Lanes} from './ReactFiberLane'; import {isPrimaryRenderer} from './ReactFiberHostConfig'; import {createCursor, push, pop} from './ReactFiberStack.new'; @@ -20,12 +20,17 @@ import { ClassComponent, DehydratedFragment, } from './ReactWorkTags'; -import {isSameOrHigherPriority} from './ReactFiberExpirationTime.new'; +import { + NoLanes, + isSubsetOfLanes, + includesSomeLane, + mergeLanes, + pickArbitraryLane, +} from './ReactFiberLane'; import invariant from 'shared/invariant'; import is from 'shared/objectIs'; import {createUpdate, enqueueUpdate, ForceUpdate} from './ReactUpdateQueue.new'; -import {NoWork} from './ReactFiberExpirationTime.new'; import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork.new'; import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags'; @@ -148,37 +153,22 @@ export function calculateChangedBits( export function scheduleWorkOnParentPath( parent: Fiber | null, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ) { - // Update the child expiration time of all the ancestors, including - // the alternates. + // Update the child lanes of all the ancestors, including the alternates. let node = parent; while (node !== null) { const alternate = node.alternate; - if ( - !isSameOrHigherPriority( - node.childExpirationTime_opaque, - renderExpirationTime, - ) - ) { - node.childExpirationTime_opaque = renderExpirationTime; - if ( - alternate !== null && - !isSameOrHigherPriority( - alternate.childExpirationTime_opaque, - renderExpirationTime, - ) - ) { - alternate.childExpirationTime_opaque = renderExpirationTime; + if (!isSubsetOfLanes(node.childLanes, renderLanes)) { + node.childLanes = mergeLanes(node.childLanes, renderLanes); + if (alternate !== null) { + alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes); } } else if ( alternate !== null && - !isSameOrHigherPriority( - alternate.childExpirationTime_opaque, - renderExpirationTime, - ) + !isSubsetOfLanes(alternate.childLanes, renderLanes) ) { - alternate.childExpirationTime_opaque = renderExpirationTime; + alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes); } else { // Neither alternate was updated, which means the rest of the // ancestor path already has sufficient priority. @@ -192,7 +182,7 @@ export function propagateContextChange( workInProgress: Fiber, context: ReactContext, changedBits: number, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ): void { let fiber = workInProgress.child; if (fiber !== null) { @@ -218,7 +208,11 @@ export function propagateContextChange( if (fiber.tag === ClassComponent) { // Schedule a force update on the work-in-progress. - const update = createUpdate(-1, renderExpirationTime, null); + const update = createUpdate( + -1, + pickArbitraryLane(renderLanes), + null, + ); update.tag = ForceUpdate; // TODO: Because we don't have a work-in-progress, this will add the // update to the current fiber, too, which means it will persist even if @@ -226,34 +220,15 @@ export function propagateContextChange( // worth fixing. enqueueUpdate(fiber, update); } - - if ( - !isSameOrHigherPriority( - fiber.expirationTime_opaque, - renderExpirationTime, - ) - ) { - fiber.expirationTime_opaque = renderExpirationTime; - } + fiber.lanes = mergeLanes(fiber.lanes, renderLanes); const alternate = fiber.alternate; - if ( - alternate !== null && - !isSameOrHigherPriority( - alternate.expirationTime_opaque, - renderExpirationTime, - ) - ) { - alternate.expirationTime_opaque = renderExpirationTime; + if (alternate !== null) { + alternate.lanes = mergeLanes(alternate.lanes, renderLanes); } + scheduleWorkOnParentPath(fiber.return, renderLanes); - scheduleWorkOnParentPath(fiber.return, renderExpirationTime); - - // Mark the expiration time on the list, too. - if ( - !isSameOrHigherPriority(list.expirationTime, renderExpirationTime) - ) { - list.expirationTime = renderExpirationTime; - } + // Mark the updated lanes on the list, too. + list.lanes = mergeLanes(list.lanes, renderLanes); // Since we already found a match, we can stop traversing the // dependency list. @@ -276,29 +251,16 @@ export function propagateContextChange( parentSuspense !== null, 'We just came from a parent so we must have had a parent. This is a bug in React.', ); - if ( - !isSameOrHigherPriority( - parentSuspense.expirationTime_opaque, - renderExpirationTime, - ) - ) { - parentSuspense.expirationTime_opaque = renderExpirationTime; - } + parentSuspense.lanes = mergeLanes(parentSuspense.lanes, renderLanes); const alternate = parentSuspense.alternate; - if ( - alternate !== null && - !isSameOrHigherPriority( - alternate.expirationTime_opaque, - renderExpirationTime, - ) - ) { - alternate.expirationTime_opaque = renderExpirationTime; + if (alternate !== null) { + alternate.lanes = mergeLanes(alternate.lanes, renderLanes); } // This is intentionally passing this fiber as the parent // because we want to schedule this fiber as having work - // on its children. We'll use the childExpirationTime on + // on its children. We'll use the childLanes on // this fiber to indicate that a context has changed. - scheduleWorkOnParentPath(parentSuspense, renderExpirationTime); + scheduleWorkOnParentPath(parentSuspense, renderLanes); nextFiber = fiber.sibling; } else { // Traverse down. @@ -334,7 +296,7 @@ export function propagateContextChange( export function prepareToReadContext( workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ): void { currentlyRenderingFiber = workInProgress; lastContextDependency = null; @@ -344,12 +306,7 @@ export function prepareToReadContext( if (dependencies !== null) { const firstContext = dependencies.firstContext; if (firstContext !== null) { - if ( - isSameOrHigherPriority( - dependencies.expirationTime, - renderExpirationTime, - ) - ) { + if (includesSomeLane(dependencies.lanes, renderLanes)) { // Context list has a pending update. Mark that this fiber performed work. markWorkInProgressReceivedUpdate(); } @@ -411,7 +368,7 @@ export function readContext( // This is the first dependency for this component. Create a new list. lastContextDependency = contextItem; currentlyRenderingFiber.dependencies_new = { - expirationTime: NoWork, + lanes: NoLanes, firstContext: contextItem, responders: null, }; diff --git a/packages/react-reconciler/src/ReactFiberOffscreenComponent.js b/packages/react-reconciler/src/ReactFiberOffscreenComponent.js index 0808c4c19317..58f236050177 100644 --- a/packages/react-reconciler/src/ReactFiberOffscreenComponent.js +++ b/packages/react-reconciler/src/ReactFiberOffscreenComponent.js @@ -8,7 +8,7 @@ */ import type {ReactNodeList} from 'shared/ReactTypes'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; +import type {Lanes} from './ReactFiberLane'; export type OffscreenProps = {| // TODO: Pick an API before exposing the Offscreen type. I've chosen an enum @@ -24,8 +24,8 @@ export type OffscreenProps = {| // We use the existence of the state object as an indicator that the component // is hidden. export type OffscreenState = {| - // TODO: This doesn't do anything, yet. It's always NoWork. But eventually it + // TODO: This doesn't do anything, yet. It's always NoLanes. But eventually it // will represent the pending work that must be included in the render in // order to unhide the component. - baseTime: ExpirationTimeOpaque, + baseLanes: Lanes, |}; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index 0cb5fada23b7..4b694f766512 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -19,7 +19,7 @@ import type { import type {RendererInspectionConfig} from './ReactFiberHostConfig'; import {FundamentalComponent} from './ReactWorkTags'; import type {ReactNodeList} from 'shared/ReactTypes'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; +import type {Lane} from './ReactFiberLane'; import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; import { @@ -47,7 +47,7 @@ import {createFiberRoot} from './ReactFiberRoot.new'; import {injectInternals, onScheduleRoot} from './ReactFiberDevToolsHook.new'; import { requestEventTime, - requestUpdateExpirationTime, + requestUpdateLane, scheduleUpdateOnFiber, flushRoot, batchedEventUpdates, @@ -74,11 +74,12 @@ import { } from './ReactCurrentFiber'; import {StrictMode} from './ReactTypeOfMode'; import { - Sync, - ContinuousHydration, - UserBlockingUpdateTime, - isSameOrHigherPriority, -} from './ReactFiberExpirationTime.new'; + SyncLane, + InputDiscreteHydrationLane, + SelectiveHydrationLane, + getHighestPriorityPendingLanes, + higherPriorityLane, +} from './ReactFiberLane'; import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig'; import { scheduleRefresh, @@ -235,7 +236,7 @@ export function updateContainer( container: OpaqueRoot, parentComponent: ?React$Component, callback: ?Function, -): ExpirationTimeOpaque { +): Lane { if (__DEV__) { onScheduleRoot(container, element); } @@ -249,7 +250,7 @@ export function updateContainer( } } const suspenseConfig = requestCurrentSuspenseConfig(); - const expirationTime = requestUpdateExpirationTime(current, suspenseConfig); + const lane = requestUpdateLane(current, suspenseConfig); const context = getContextForSubtree(parentComponent); if (container.context === null) { @@ -275,7 +276,7 @@ export function updateContainer( } } - const update = createUpdate(eventTime, expirationTime, suspenseConfig); + const update = createUpdate(eventTime, lane, suspenseConfig); // Caution: React DevTools currently depends on this property // being called "element". update.payload = {element}; @@ -295,9 +296,9 @@ export function updateContainer( } enqueueUpdate(current, update); - scheduleUpdateOnFiber(current, expirationTime); + scheduleUpdateOnFiber(current, lane); - return expirationTime; + return lane; } export { @@ -336,40 +337,37 @@ export function attemptSynchronousHydration(fiber: Fiber): void { const root: FiberRoot = fiber.stateNode; if (root.hydrate) { // Flush the first scheduled "update". - flushRoot(root, root.firstPendingTime_opaque); + const lanes = getHighestPriorityPendingLanes(root); + flushRoot(root, lanes); } break; case SuspenseComponent: - flushSync(() => - scheduleUpdateOnFiber(fiber, (Sync: ExpirationTimeOpaque)), - ); + flushSync(() => scheduleUpdateOnFiber(fiber, SyncLane)); // If we're still blocked after this, we need to increase // the priority of any promises resolving within this // boundary so that they next attempt also has higher pri. - const retryExpTime = UserBlockingUpdateTime; - markRetryTimeIfNotHydrated(fiber, retryExpTime); + const retryLane = InputDiscreteHydrationLane; + markRetryLaneIfNotHydrated(fiber, retryLane); break; } } -function markRetryTimeImpl(fiber: Fiber, retryTime: ExpirationTimeOpaque) { +function markRetryLaneImpl(fiber: Fiber, retryLane: Lane) { const suspenseState: null | SuspenseState = fiber.memoizedState; if (suspenseState !== null && suspenseState.dehydrated !== null) { - if (!isSameOrHigherPriority(suspenseState.retryTime, retryTime)) { - suspenseState.retryTime = retryTime; - } + suspenseState.retryLane = higherPriorityLane( + suspenseState.retryLane, + retryLane, + ); } } // Increases the priority of thennables when they resolve within this boundary. -function markRetryTimeIfNotHydrated( - fiber: Fiber, - retryTime: ExpirationTimeOpaque, -) { - markRetryTimeImpl(fiber, retryTime); +function markRetryLaneIfNotHydrated(fiber: Fiber, retryLane: Lane) { + markRetryLaneImpl(fiber, retryLane); const alternate = fiber.alternate; if (alternate) { - markRetryTimeImpl(alternate, retryTime); + markRetryLaneImpl(alternate, retryLane); } } @@ -381,9 +379,9 @@ export function attemptUserBlockingHydration(fiber: Fiber): void { // Suspense. return; } - const expTime = UserBlockingUpdateTime; - scheduleUpdateOnFiber(fiber, expTime); - markRetryTimeIfNotHydrated(fiber, expTime); + const lane = InputDiscreteHydrationLane; + scheduleUpdateOnFiber(fiber, lane); + markRetryLaneIfNotHydrated(fiber, lane); } export function attemptContinuousHydration(fiber: Fiber): void { @@ -394,11 +392,9 @@ export function attemptContinuousHydration(fiber: Fiber): void { // Suspense. return; } - scheduleUpdateOnFiber(fiber, (ContinuousHydration: ExpirationTimeOpaque)); - markRetryTimeIfNotHydrated( - fiber, - (ContinuousHydration: ExpirationTimeOpaque), - ); + const lane = SelectiveHydrationLane; + scheduleUpdateOnFiber(fiber, lane); + markRetryLaneIfNotHydrated(fiber, lane); } export function attemptHydrationAtCurrentPriority(fiber: Fiber): void { @@ -407,9 +403,9 @@ export function attemptHydrationAtCurrentPriority(fiber: Fiber): void { // their priority other than synchronously flush it. return; } - const expTime = requestUpdateExpirationTime(fiber, null); - scheduleUpdateOnFiber(fiber, expTime); - markRetryTimeIfNotHydrated(fiber, expTime); + const lane = requestUpdateLane(fiber, null); + scheduleUpdateOnFiber(fiber, lane); + markRetryLaneIfNotHydrated(fiber, lane); } export {findHostInstance}; @@ -491,7 +487,7 @@ if (__DEV__) { // Shallow cloning props works as a workaround for now to bypass the bailout check. fiber.memoizedProps = {...fiber.memoizedProps}; - scheduleUpdateOnFiber(fiber, (Sync: ExpirationTimeOpaque)); + scheduleUpdateOnFiber(fiber, SyncLane); } }; @@ -501,11 +497,11 @@ if (__DEV__) { if (fiber.alternate) { fiber.alternate.pendingProps = fiber.pendingProps; } - scheduleUpdateOnFiber(fiber, (Sync: ExpirationTimeOpaque)); + scheduleUpdateOnFiber(fiber, SyncLane); }; scheduleUpdate = (fiber: Fiber) => { - scheduleUpdateOnFiber(fiber, (Sync: ExpirationTimeOpaque)); + scheduleUpdateOnFiber(fiber, SyncLane); }; setSuspenseHandler = (newShouldSuspendImpl: Fiber => boolean) => { diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js index ae0305623548..b3fc36d51724 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.new.js +++ b/packages/react-reconciler/src/ReactFiberRoot.new.js @@ -8,25 +8,17 @@ */ import type {FiberRoot, SuspenseHydrationCallbacks} from './ReactInternalTypes'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; import type {RootTag} from './ReactRootTags'; import {noTimeout} from './ReactFiberHostConfig'; import {createHostRootFiber} from './ReactFiber.new'; -import { - NoWork, - isSameOrHigherPriority, - isSameExpirationTime, - bumpPriorityHigher, - bumpPriorityLower, -} from './ReactFiberExpirationTime.new'; +import {NoLanes} from './ReactFiberLane'; import { enableSchedulerTracing, enableSuspenseCallback, } from 'shared/ReactFeatureFlags'; import {unstable_getThreadID} from 'scheduler/tracing'; import {initializeUpdateQueue} from './ReactUpdateQueue.new'; -import {clearPendingUpdates as clearPendingMutableSourceUpdates} from './ReactMutableSource.new'; function FiberRootNode(containerInfo, tag, hydrate) { this.tag = tag; @@ -35,23 +27,22 @@ function FiberRootNode(containerInfo, tag, hydrate) { this.current = null; this.pingCache = null; this.finishedWork = null; - this.finishedExpirationTime_opaque = NoWork; this.timeoutHandle = noTimeout; this.context = null; this.pendingContext = null; this.hydrate = hydrate; this.callbackNode = null; - this.callbackId = NoWork; + this.callbackId = NoLanes; this.callbackIsSync = false; this.expiresAt = -1; - this.firstPendingTime_opaque = NoWork; - this.lastPendingTime_opaque = NoWork; - this.firstSuspendedTime_opaque = NoWork; - this.lastSuspendedTime_opaque = NoWork; - this.nextKnownPendingLevel_opaque = NoWork; - this.lastPingedTime_opaque = NoWork; - this.lastExpiredTime_opaque = NoWork; - this.mutableSourceLastPendingUpdateTime_opaque = NoWork; + + this.pendingLanes = NoLanes; + this.suspendedLanes = NoLanes; + this.pingedLanes = NoLanes; + this.expiredLanes = NoLanes; + this.mutableReadLanes = NoLanes; + + this.finishedLanes = NoLanes; if (enableSchedulerTracing) { this.interactionThreadID = unstable_getThreadID(); @@ -84,154 +75,3 @@ export function createFiberRoot( return root; } - -export function isRootSuspendedAtTime( - root: FiberRoot, - expirationTime: ExpirationTimeOpaque, -): boolean { - const firstSuspendedTime = root.firstSuspendedTime_opaque; - const lastSuspendedTime = root.lastSuspendedTime_opaque; - return ( - !isSameExpirationTime(firstSuspendedTime, (NoWork: ExpirationTimeOpaque)) && - isSameOrHigherPriority(firstSuspendedTime, expirationTime) && - isSameOrHigherPriority(expirationTime, lastSuspendedTime) - ); -} - -export function markRootSuspendedAtTime( - root: FiberRoot, - expirationTime: ExpirationTimeOpaque, -): void { - const firstSuspendedTime = root.firstSuspendedTime_opaque; - const lastSuspendedTime = root.lastSuspendedTime_opaque; - if (!isSameOrHigherPriority(firstSuspendedTime, expirationTime)) { - root.firstSuspendedTime_opaque = expirationTime; - } - if ( - !isSameOrHigherPriority(expirationTime, lastSuspendedTime) || - isSameExpirationTime(firstSuspendedTime, (NoWork: ExpirationTimeOpaque)) - ) { - root.lastSuspendedTime_opaque = expirationTime; - } - - if (isSameOrHigherPriority(root.lastPingedTime_opaque, expirationTime)) { - root.lastPingedTime_opaque = NoWork; - } - - if (isSameOrHigherPriority(root.lastExpiredTime_opaque, expirationTime)) { - root.lastExpiredTime_opaque = NoWork; - } -} - -export function markRootUpdatedAtTime( - root: FiberRoot, - expirationTime: ExpirationTimeOpaque, -): void { - // Update the range of pending times - const firstPendingTime = root.firstPendingTime_opaque; - if (!isSameOrHigherPriority(firstPendingTime, expirationTime)) { - root.firstPendingTime_opaque = expirationTime; - } - const lastPendingTime = root.lastPendingTime_opaque; - if ( - isSameExpirationTime(lastPendingTime, (NoWork: ExpirationTimeOpaque)) || - !isSameOrHigherPriority(expirationTime, lastPendingTime) - ) { - root.lastPendingTime_opaque = expirationTime; - } - - // Update the range of suspended times. Treat everything lower priority or - // equal to this update as unsuspended. - const firstSuspendedTime = root.firstSuspendedTime_opaque; - if ( - !isSameExpirationTime(firstSuspendedTime, (NoWork: ExpirationTimeOpaque)) - ) { - if (isSameOrHigherPriority(expirationTime, firstSuspendedTime)) { - // The entire suspended range is now unsuspended. - root.firstSuspendedTime_opaque = root.lastSuspendedTime_opaque = root.nextKnownPendingLevel_opaque = NoWork; - } else if ( - isSameOrHigherPriority(expirationTime, root.lastSuspendedTime_opaque) - ) { - root.lastSuspendedTime_opaque = bumpPriorityHigher(expirationTime); - } - - // This is a pending level. Check if it's higher priority than the next - // known pending level. - if ( - !isSameOrHigherPriority(root.nextKnownPendingLevel_opaque, expirationTime) - ) { - root.nextKnownPendingLevel_opaque = expirationTime; - } - } -} - -export function markRootFinishedAtTime( - root: FiberRoot, - finishedExpirationTime: ExpirationTimeOpaque, - remainingExpirationTime: ExpirationTimeOpaque, -): void { - // Update the range of pending times - root.firstPendingTime_opaque = remainingExpirationTime; - if ( - !isSameOrHigherPriority( - remainingExpirationTime, - root.lastPendingTime_opaque, - ) - ) { - // This usually means we've finished all the work, but it can also happen - // when something gets downprioritized during render, like a hidden tree. - root.lastPendingTime_opaque = remainingExpirationTime; - } - - // Update the range of suspended times. Treat everything higher priority or - // equal to this update as unsuspended. - if ( - isSameOrHigherPriority( - root.lastSuspendedTime_opaque, - finishedExpirationTime, - ) - ) { - // The entire suspended range is now unsuspended. - root.firstSuspendedTime_opaque = root.lastSuspendedTime_opaque = root.nextKnownPendingLevel_opaque = NoWork; - } else if ( - isSameOrHigherPriority( - root.firstSuspendedTime_opaque, - finishedExpirationTime, - ) - ) { - // Part of the suspended range is now unsuspended. Narrow the range to - // include everything between the unsuspended time (non-inclusive) and the - // last suspended time. - root.firstSuspendedTime_opaque = bumpPriorityLower(finishedExpirationTime); - } - - if ( - isSameOrHigherPriority(root.lastPingedTime_opaque, finishedExpirationTime) - ) { - // Clear the pinged time - root.lastPingedTime_opaque = NoWork; - } - - if ( - isSameOrHigherPriority(root.lastExpiredTime_opaque, finishedExpirationTime) - ) { - // Clear the expired time - root.lastExpiredTime_opaque = NoWork; - } - - // Clear any pending updates that were just processed. - clearPendingMutableSourceUpdates(root, finishedExpirationTime); -} - -export function markRootExpiredAtTime( - root: FiberRoot, - expirationTime: ExpirationTimeOpaque, -): void { - const lastExpiredTime = root.lastExpiredTime_opaque; - if ( - isSameExpirationTime(lastExpiredTime, (NoWork: ExpirationTimeOpaque)) || - !isSameOrHigherPriority(expirationTime, lastExpiredTime) - ) { - root.lastExpiredTime_opaque = expirationTime; - } -} diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js index a45f1710138f..e151ab0be008 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js @@ -9,7 +9,7 @@ import type {Fiber} from './ReactInternalTypes'; import type {SuspenseInstance} from './ReactFiberHostConfig'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; +import type {Lane} from './ReactFiberLane'; import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags'; import {NoEffect, DidCapture} from './ReactSideEffectTags'; import { @@ -29,11 +29,10 @@ export type SuspenseState = {| // here to indicate that it is dehydrated (flag) and for quick access // to check things like isSuspenseInstancePending. dehydrated: null | SuspenseInstance, - // Represents the earliest expiration time we should attempt to hydrate - // a dehydrated boundary at. - // Never is the default for dehydrated boundaries. - // NoWork is the default for normal boundaries, which turns into "normal" pri. - retryTime: ExpirationTimeOpaque, + // Represents the lane we should attempt to hydrate a dehydrated boundary at. + // OffscreenLane is the default for dehydrated boundaries. + // NoLane is the default for normal boundaries, which turns into "normal" pri. + retryLane: Lane, |}; export type SuspenseListTailMode = 'collapsed' | 'hidden' | void; diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js index a63deaaef113..190ca9d4730d 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.new.js +++ b/packages/react-reconciler/src/ReactFiberThrow.new.js @@ -9,7 +9,7 @@ import type {Fiber} from './ReactInternalTypes'; import type {FiberRoot} from './ReactInternalTypes'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; +import type {Lane, Lanes} from './ReactFiberLane'; import type {CapturedValue} from './ReactCapturedValue'; import type {Update} from './ReactUpdateQueue.new'; import type {Wakeable} from 'shared/ReactTypes'; @@ -54,16 +54,21 @@ import { } from './ReactFiberWorkLoop.new'; import {logCapturedError} from './ReactFiberErrorLogger'; -import {Sync, isSameExpirationTime} from './ReactFiberExpirationTime.new'; +import { + SyncLane, + includesSomeLane, + mergeLanes, + pickArbitraryLane, +} from './ReactFiberLane'; const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; function createRootErrorUpdate( fiber: Fiber, errorInfo: CapturedValue, - expirationTime: ExpirationTimeOpaque, + lane: Lane, ): Update { - const update = createUpdate(-1, expirationTime, null); + const update = createUpdate(-1, lane, null); // Unmount the root by rendering null. update.tag = CaptureUpdate; // Caution: React DevTools currently depends on this property @@ -80,9 +85,9 @@ function createRootErrorUpdate( function createClassErrorUpdate( fiber: Fiber, errorInfo: CapturedValue, - expirationTime: ExpirationTimeOpaque, + lane: Lane, ): Update { - const update = createUpdate(-1, expirationTime, null); + const update = createUpdate(-1, lane, null); update.tag = CaptureUpdate; const getDerivedStateFromError = fiber.type.getDerivedStateFromError; if (typeof getDerivedStateFromError === 'function') { @@ -120,12 +125,7 @@ function createClassErrorUpdate( // If componentDidCatch is the only error boundary method defined, // then it needs to call setState to recover from errors. // If no state update is scheduled then the boundary will swallow the error. - if ( - !isSameExpirationTime( - fiber.expirationTime_opaque, - (Sync: ExpirationTimeOpaque), - ) - ) { + if (!includesSomeLane(fiber.lanes, (SyncLane: Lane))) { console.error( '%s: Error boundaries should implement getDerivedStateFromError(). ' + 'In that method, return a state update to display an error message or fallback UI.', @@ -143,14 +143,10 @@ function createClassErrorUpdate( return update; } -function attachPingListener( - root: FiberRoot, - renderExpirationTime: ExpirationTimeOpaque, - wakeable: Wakeable, -) { - // Attach a listener to the promise to "ping" the root and retry. But - // only if one does not already exist for the current render expiration - // time (which acts like a "thread ID" here). +function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) { + // Attach a listener to the promise to "ping" the root and retry. But only if + // one does not already exist for the lanes we're currently rendering (which + // acts like a "thread ID" here). let pingCache = root.pingCache; let threadIDs; if (pingCache === null) { @@ -164,15 +160,10 @@ function attachPingListener( pingCache.set(wakeable, threadIDs); } } - if (!threadIDs.has(renderExpirationTime)) { + if (!threadIDs.has(lanes)) { // Memoize using the thread ID to prevent redundant listeners. - threadIDs.add(renderExpirationTime); - const ping = pingSuspendedRoot.bind( - null, - root, - wakeable, - renderExpirationTime, - ); + threadIDs.add(lanes); + const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes); wakeable.then(ping, ping); } } @@ -182,7 +173,7 @@ function throwException( returnFiber: Fiber, sourceFiber: Fiber, value: mixed, - renderExpirationTime: ExpirationTimeOpaque, + rootRenderLanes: Lanes, ) { // The source fiber did not complete. sourceFiber.effectTag |= Incomplete; @@ -204,7 +195,7 @@ function throwException( if (currentSource) { sourceFiber.updateQueue = currentSource.updateQueue; sourceFiber.memoizedState = currentSource.memoizedState; - sourceFiber.expirationTime_opaque = currentSource.expirationTime_opaque; + sourceFiber.lanes = currentSource.lanes; } else { sourceFiber.updateQueue = null; sourceFiber.memoizedState = null; @@ -263,11 +254,7 @@ function throwException( // When we try rendering again, we should not reuse the current fiber, // since it's known to be in an inconsistent state. Use a force update to // prevent a bail out. - const update = createUpdate( - -1, - (Sync: ExpirationTimeOpaque), - null, - ); + const update = createUpdate(-1, SyncLane, null); update.tag = ForceUpdate; enqueueUpdate(sourceFiber, update); } @@ -275,7 +262,7 @@ function throwException( // The source fiber did not complete. Mark it with Sync priority to // indicate that it still has pending work. - sourceFiber.expirationTime_opaque = Sync; + sourceFiber.lanes = mergeLanes(sourceFiber.lanes, SyncLane); // Exit without suspending. return; @@ -323,10 +310,10 @@ function throwException( // We want to ensure that a "busy" state doesn't get force committed. We want to // ensure that new initial loading states can commit as soon as possible. - attachPingListener(root, renderExpirationTime, wakeable); + attachPingListener(root, wakeable, rootRenderLanes); workInProgress.effectTag |= ShouldCapture; - workInProgress.expirationTime_opaque = renderExpirationTime; + workInProgress.lanes = rootRenderLanes; return; } @@ -349,6 +336,7 @@ function throwException( // over and traverse parent path again, this time treating the exception // as an error. renderDidError(); + value = createCapturedValue(value, sourceFiber); let workInProgress = returnFiber; do { @@ -356,12 +344,9 @@ function throwException( case HostRoot: { const errorInfo = value; workInProgress.effectTag |= ShouldCapture; - workInProgress.expirationTime_opaque = renderExpirationTime; - const update = createRootErrorUpdate( - workInProgress, - errorInfo, - renderExpirationTime, - ); + const lane = pickArbitraryLane(rootRenderLanes); + workInProgress.lanes = mergeLanes(workInProgress.lanes, lane); + const update = createRootErrorUpdate(workInProgress, errorInfo, lane); enqueueCapturedUpdate(workInProgress, update); return; } @@ -378,12 +363,13 @@ function throwException( !isAlreadyFailedLegacyErrorBoundary(instance))) ) { workInProgress.effectTag |= ShouldCapture; - workInProgress.expirationTime_opaque = renderExpirationTime; + const lane = pickArbitraryLane(rootRenderLanes); + workInProgress.lanes = mergeLanes(workInProgress.lanes, lane); // Schedule the error boundary to re-render using updated state const update = createClassErrorUpdate( workInProgress, errorInfo, - renderExpirationTime, + lane, ); enqueueCapturedUpdate(workInProgress, update); return; diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js index 64bed3225461..735888f3215d 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js @@ -8,7 +8,7 @@ */ import type {Fiber} from './ReactInternalTypes'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; +import type {Lanes} from './ReactFiberLane'; import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new'; @@ -35,14 +35,11 @@ import { popTopLevelContextObject as popTopLevelLegacyContextObject, } from './ReactFiberContext.new'; import {popProvider} from './ReactFiberNewContext.new'; -import {popRenderExpirationTime} from './ReactFiberWorkLoop.new'; +import {popRenderLanes} from './ReactFiberWorkLoop.new'; import invariant from 'shared/invariant'; -function unwindWork( - workInProgress: Fiber, - renderExpirationTime: ExpirationTimeOpaque, -) { +function unwindWork(workInProgress: Fiber, renderLanes: Lanes) { switch (workInProgress.tag) { case ClassComponent: { const Component = workInProgress.type; @@ -110,7 +107,7 @@ function unwindWork( return null; case OffscreenComponent: case LegacyHiddenComponent: - popRenderExpirationTime(workInProgress); + popRenderLanes(workInProgress); return null; default: return null; @@ -150,7 +147,7 @@ function unwindInterruptedWork(interruptedWork: Fiber) { break; case OffscreenComponent: case LegacyHiddenComponent: - popRenderExpirationTime(interruptedWork); + popRenderLanes(interruptedWork); break; default: break; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 544d4f72b14a..0e9b9caaf08a 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -9,7 +9,7 @@ import type {Thenable, Wakeable} from 'shared/ReactTypes'; import type {Fiber, FiberRoot} from './ReactInternalTypes'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; +import type {Lanes, Lane} from './ReactFiberLane'; import type {ReactPriorityLevel} from './ReactInternalTypes'; import type {Interaction} from 'scheduler/src/Tracing'; import type {SuspenseConfig} from './ReactFiberSuspenseConfig'; @@ -43,8 +43,6 @@ import { ImmediatePriority as ImmediateSchedulerPriority, UserBlockingPriority as UserBlockingSchedulerPriority, NormalPriority as NormalSchedulerPriority, - LowPriority as LowSchedulerPriority, - IdlePriority as IdleSchedulerPriority, flushSyncCallbackQueue, scheduleSyncCallback, } from './SchedulerWithReactIntegration.new'; @@ -69,13 +67,6 @@ import { createWorkInProgress, assignFiberPropertiesInDEV, } from './ReactFiber.new'; -import { - isRootSuspendedAtTime, - markRootSuspendedAtTime, - markRootFinishedAtTime, - markRootUpdatedAtTime, - markRootExpiredAtTime, -} from './ReactFiberRoot.new'; import { NoMode, StrictMode, @@ -93,6 +84,8 @@ import { MemoComponent, SimpleMemoComponent, Block, + OffscreenComponent, + LegacyHiddenComponent, } from './ReactWorkTags'; import {LegacyRoot} from './ReactRootTags'; import { @@ -113,33 +106,35 @@ import { Hydrating, HydratingAndUpdate, } from './ReactSideEffectTags'; -import { - NoWork, - Sync, - UserBlockingUpdateTime, - DefaultUpdateTime, - Never, - inferPriorityFromExpirationTime, - Batched, - Idle, - ContinuousHydration, - ShortTransition, - LongTransition, - isSameOrHigherPriority, - isSameExpirationTime, - bumpPriorityLower, -} from './ReactFiberExpirationTime.new'; import { SyncLanePriority, - SyncBatchedLanePriority, InputDiscreteLanePriority, - InputContinuousLanePriority, - DefaultLanePriority, TransitionShortLanePriority, TransitionLongLanePriority, - HydrationContinuousLanePriority, - IdleLanePriority, - OffscreenLanePriority, + NoLanes, + NoLane, + SyncLane, + SyncBatchedLane, + OffscreenLane, + findUpdateLane, + findTransitionLane, + includesSomeLane, + isSubsetOfLanes, + mergeLanes, + removeLanes, + hasDiscreteLanes, + hasUpdatePriority, + getNextLanes, + returnNextLanesPriority, + getLanesToRetrySynchronouslyOnError, + markRootUpdated, + markRootSuspended as markRootSuspended_dontCallThisOneDirectly, + markRootPinged, + markRootExpired, + markDiscreteUpdatesExpired, + markRootFinished, + schedulerPriorityToLanePriority, + lanePriorityToSchedulerPriority, } from './ReactFiberLane'; import {beginWork as originalBeginWork} from './ReactFiberBeginWork.new'; import {completeWork} from './ReactFiberCompleteWork.new'; @@ -235,33 +230,43 @@ let executionContext: ExecutionContext = NoContext; let workInProgressRoot: FiberRoot | null = null; // The fiber we're working on let workInProgress: Fiber | null = null; -// The expiration time we're rendering -let renderExpirationTime: ExpirationTimeOpaque = NoWork; - -// Stack that allows components to channge renderExpirationTime for its subtree -const renderExpirationTimeCursor: StackCursor = createCursor( - NoWork, -); +// The lanes we're rendering +let workInProgressRootRenderLanes: Lanes = NoLanes; + +// Stack that allows components to change the render lanes for its subtree +// This is a superset of the lanes we started working on at the root. The only +// case where it's different from `workInProgressRootRenderLanes` is when we +// enter a subtree that is hidden and needs to be unhidden: Suspense and +// Offscreen component. +// +// Most things in the work loop should deal with workInProgressRootRenderLanes. +// Most things in begin/complete phases should deal with subtreeRenderLanes. +let subtreeRenderLanes: Lanes = NoLanes; +const subtreeRenderLanesCursor: StackCursor = createCursor(NoLanes); // Whether to root completed, errored, suspended, etc. let workInProgressRootExitStatus: RootExitStatus = RootIncomplete; // A fatal error, if one is thrown let workInProgressRootFatalError: mixed = null; // Most recent event time among processed updates during this render. -// This is conceptually a time stamp but expressed in terms of an ExpirationTimeOpaque -// because we deal mostly with expiration times in the hot path, so this avoids -// the conversion happening in the hot path. let workInProgressRootLatestProcessedEventTime: number = -1; let workInProgressRootLatestSuspenseTimeout: number = -1; let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null; +// "Included" lanes refer to lanes that were worked on during this render. It's +// slightly different than `renderLanes` because `renderLanes` can change as you +// enter and exit an Offscreen tree. This value is the combination of all render +// lanes for the entire render phase. +let workInProgressRootIncludedLanes: Lanes = NoLanes; // The work left over by components that were visited during this render. Only // includes unprocessed updates, not work in bailed out children. -let workInProgressRootNextUnprocessedUpdateTime: ExpirationTimeOpaque = NoWork; +let workInProgressRootSkippedLanes: Lanes = NoLanes; +// Lanes that were updated (in an interleaved event) during this render. +let workInProgressRootUpdatedLanes: Lanes = NoLanes; +// Lanes that were pinged (in an interleaved event) during this render. +let workInProgressRootPingedLanes: Lanes = NoLanes; + +let mostRecentlyUpdatedRoot: FiberRoot | null = null; -// If we're pinged while rendering we don't always restart immediately. -// This flag determines if it might be worthwhile to restart if an opportunity -// happens latere. -let workInProgressRootHasPendingPing: boolean = false; // The most recent time we committed a fallback. This lets us ensure a train // model where we don't commit new loading states in too quick succession. let globalMostRecentFallbackTime: number = 0; @@ -276,15 +281,12 @@ let legacyErrorBoundariesThatAlreadyFailed: Set | null = null; let rootDoesHavePassiveEffects: boolean = false; let rootWithPendingPassiveEffects: FiberRoot | null = null; let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoSchedulerPriority; -let pendingPassiveEffectsExpirationTime: ExpirationTimeOpaque = NoWork; +let pendingPassiveEffectsLanes: Lanes = NoLanes; let pendingPassiveHookEffectsMount: Array = []; let pendingPassiveHookEffectsUnmount: Array = []; let pendingPassiveProfilerEffects: Array = []; -let rootsWithPendingDiscreteUpdates: Map< - FiberRoot, - ExpirationTimeOpaque, -> | null = null; +let rootsWithPendingDiscreteUpdates: Set | null = null; // Use these to prevent an infinite loop of nested updates const NESTED_UPDATE_LIMIT = 50; @@ -294,16 +296,19 @@ let rootWithNestedUpdates: FiberRoot | null = null; const NESTED_PASSIVE_UPDATE_LIMIT = 50; let nestedPassiveUpdateCount: number = 0; -// Marks the need to reschedule pending interactions at these expiration times +// Marks the need to reschedule pending interactions at these lanes // during the commit phase. This enables them to be traced across components // that spawn new work during render. E.g. hidden boundaries, suspended SSR // hydration or SuspenseList. -let spawnedWorkDuringRender: null | Array = null; +// TODO: Can use a bitmask instead of an array +let spawnedWorkDuringRender: null | Array = null; // If two updates are scheduled within the same event, we should treat their // event times as simultaneous, even if the actual clock time has advanced // between the first and second call. let currentEventTime: number = -1; +let currentEventWipLanes: Lanes = NoLanes; +let currentEventPendingLanes: Lanes = NoLanes; // Dev only flag that tracks if passive effects are currently being flushed. // We warn about state updates for unmounted components differently in this case. @@ -335,121 +340,86 @@ export function getCurrentTime() { return now(); } -export function requestUpdateExpirationTime( +export function requestUpdateLane( fiber: Fiber, suspenseConfig: SuspenseConfig | null, -): ExpirationTimeOpaque { +): Lane { // Special cases const mode = fiber.mode; if ((mode & BlockingMode) === NoMode) { - return Sync; + return (SyncLane: Lane); } else if ((mode & ConcurrentMode) === NoMode) { return getCurrentPriorityLevel() === ImmediateSchedulerPriority - ? (Sync: ExpirationTimeOpaque) - : Batched; - } else if ((executionContext & RenderContext) !== NoContext) { - // Use whatever time we're already rendering - // TODO: Treat render phase updates as if they came from an - // interleaved event. - return renderExpirationTime; - } - - let updateLanePriority; + ? (SyncLane: Lane) + : (SyncBatchedLane: Lane); + } + + // The algorithm for assigning an update to a lane should be stable for all + // updates at the same priority within the same event. To do this, the inputs + // to the algorithm must be the same. For example, we use the `renderLanes` + // to avoid choosing a lane that is already in the middle of rendering. + // + // However, the "included" lanes could be mutated in between updates in the + // same event, like if you perform an update inside `flushSync`. Or any other + // codepath that might call `prepareFreshStack`. + // + // The trick we use is to cache the first of each of these inputs within an + // event. Then reset the cached values once we can be sure the event is over. + // Our heuristic for that is whenever we enter a concurrent work loop. + // + // We'll do the same for `currentEventPendingLanes` below. + if (currentEventWipLanes === NoLanes) { + currentEventWipLanes = workInProgressRootIncludedLanes; + } + + let lane; if (suspenseConfig !== null) { - // If there's a SuspenseConfig, choose an expiration time that's lower - // priority than a normal concurrent update (regardless of the current - // Scheduler priority.) Timeouts larger than 10 seconds move one level - // lower than that. + // Use the size of the timeout as a heuristic to prioritize shorter + // transitions over longer ones. // TODO: This will coerce numbers larger than 31 bits to 0. const timeoutMs = suspenseConfig.timeoutMs; - updateLanePriority = + const transitionLanePriority = timeoutMs === undefined || (timeoutMs | 0) < 10000 ? TransitionShortLanePriority : TransitionLongLanePriority; + + if (currentEventPendingLanes !== NoLanes) { + currentEventPendingLanes = + mostRecentlyUpdatedRoot !== null + ? mostRecentlyUpdatedRoot.pendingLanes + : NoLanes; + } + + lane = findTransitionLane( + transitionLanePriority, + currentEventWipLanes, + currentEventPendingLanes, + ); } else { // TODO: If we're not inside `runWithPriority`, this returns the priority // of the currently running task. That's probably not what we want. - const priorityLevel = getCurrentPriorityLevel(); - switch (priorityLevel) { - case ImmediateSchedulerPriority: - updateLanePriority = SyncLanePriority; - break; - case UserBlockingSchedulerPriority: - updateLanePriority = InputContinuousLanePriority; - break; - case NormalSchedulerPriority: - case LowSchedulerPriority: - // TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration. - updateLanePriority = DefaultLanePriority; - break; - case IdleSchedulerPriority: - updateLanePriority = IdleLanePriority; - break; - default: - invariant(false, 'Expected a valid priority level'); - } - } - - // TODO: In the new system, what we'll do here is claim one of the bits of - // the root's `pendingLanes` field, based on its priority. We'll combine this - // function with `scheduleUpdateOnFiber` and `markRootUpdatedAtTime`. - let expirationTime; - switch (updateLanePriority) { - case SyncLanePriority: - return Sync; - case SyncBatchedLanePriority: - return Batched; - case InputDiscreteLanePriority: - case InputContinuousLanePriority: - expirationTime = UserBlockingUpdateTime; - break; - case DefaultLanePriority: - expirationTime = DefaultUpdateTime; - break; - case TransitionShortLanePriority: - expirationTime = ShortTransition; - break; - case TransitionLongLanePriority: - expirationTime = LongTransition; - break; - case HydrationContinuousLanePriority: - expirationTime = ContinuousHydration; - break; - case IdleLanePriority: - expirationTime = Idle; - break; - case OffscreenLanePriority: - expirationTime = Never; - break; - default: - invariant(false, 'Expected a valid priority level'); - } + const schedulerPriority = getCurrentPriorityLevel(); - // If we're in the middle of rendering a tree, do not update at the same - // expiration time that is already rendering. - // TODO: We shouldn't have to do this if the update is on a different root. - // TODO: In the new system, we'll find a different bit that's not the one - // we're currently rendering. - if ( - workInProgressRoot !== null && - isSameExpirationTime(expirationTime, renderExpirationTime) - ) { - // This is a trick to move this update into a separate batch - // TODO: This probably causes problems with ContinuousHydration and Idle - expirationTime = bumpPriorityLower(expirationTime); + if ( + // TODO: Temporary. We're removing the concept of discrete updates. + (executionContext & DiscreteEventContext) !== NoContext && + schedulerPriority === UserBlockingSchedulerPriority + ) { + lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes); + } else { + const lanePriority = schedulerPriorityToLanePriority(schedulerPriority); + lane = findUpdateLane(lanePriority, currentEventWipLanes); + } } - return expirationTime; + return lane; } -export function scheduleUpdateOnFiber( - fiber: Fiber, - expirationTime: ExpirationTimeOpaque, -) { +export function scheduleUpdateOnFiber(fiber: Fiber, lane: Lane) { checkForNestedUpdates(); warnAboutRenderPhaseUpdatesInDEV(fiber); - const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime); + const root = markUpdateLaneFromFiberToRoot(fiber, lane); if (root === null) { warnAboutUpdateOnUnmountedFiberInDEV(fiber); return null; @@ -459,7 +429,7 @@ export function scheduleUpdateOnFiber( // priority as an argument to that function and this one. const priorityLevel = getCurrentPriorityLevel(); - if (isSameExpirationTime(expirationTime, (Sync: ExpirationTimeOpaque))) { + if (lane === SyncLane) { if ( // Check if we're inside unbatchedUpdates (executionContext & LegacyUnbatchedContext) !== NoContext && @@ -467,7 +437,7 @@ export function scheduleUpdateOnFiber( (executionContext & (RenderContext | CommitContext)) === NoContext ) { // Register pending interactions on the root to avoid losing traced interaction data. - schedulePendingInteractions(root, expirationTime); + schedulePendingInteractions(root, lane); // This is a legacy edge case. The initial mount of a ReactDOM.render-ed // root inside of batchedUpdates should be synchronous, but layout updates @@ -475,7 +445,7 @@ export function scheduleUpdateOnFiber( performSyncWorkOnRoot(root); } else { ensureRootIsScheduled(root); - schedulePendingInteractions(root, expirationTime); + schedulePendingInteractions(root, lane); if (executionContext === NoContext) { // Flush the synchronous work now, unless we're already working or inside // a batch. This is intentionally inside scheduleUpdateOnFiber instead of @@ -497,38 +467,37 @@ export function scheduleUpdateOnFiber( // This is the result of a discrete event. Track the lowest priority // discrete update per root so we can flush them early, if needed. if (rootsWithPendingDiscreteUpdates === null) { - rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]); + rootsWithPendingDiscreteUpdates = new Set([root]); } else { - const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root); - if ( - lastDiscreteTime === undefined || - !isSameOrHigherPriority(expirationTime, lastDiscreteTime) - ) { - rootsWithPendingDiscreteUpdates.set(root, expirationTime); - } + rootsWithPendingDiscreteUpdates.add(root); } } // Schedule other updates after in case the callback is sync. ensureRootIsScheduled(root); - schedulePendingInteractions(root, expirationTime); + schedulePendingInteractions(root, lane); } + + // We use this when assigning a lane for a transition inside + // `requestUpdateLane`. We assume it's the same as the root being updated, + // since in the common case of a single root app it probably is. If it's not + // the same root, then it's not a huge deal, we just might batch more stuff + // together more than necessary. + mostRecentlyUpdatedRoot = root; } // This is split into a separate function so we can mark a fiber with pending // work without treating it as a typical update that originates from an event; // e.g. retrying a Suspense boundary isn't an update, but it does schedule work // on a fiber. -function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { - // Update the source fiber's expiration time - if (!isSameOrHigherPriority(fiber.expirationTime_opaque, expirationTime)) { - fiber.expirationTime_opaque = expirationTime; - } +function markUpdateLaneFromFiberToRoot( + fiber: Fiber, + lane: Lane, +): FiberRoot | null { + // Update the source fiber's lanes + fiber.lanes = mergeLanes(fiber.lanes, lane); let alternate = fiber.alternate; - if ( - alternate !== null && - !isSameOrHigherPriority(alternate.expirationTime_opaque, expirationTime) - ) { - alternate.expirationTime_opaque = expirationTime; + if (alternate !== null) { + alternate.lanes = mergeLanes(alternate.lanes, lane); } // Walk the parent path to the root and update the child expiration time. let node = fiber.return; @@ -538,27 +507,9 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { } else { while (node !== null) { alternate = node.alternate; - if ( - !isSameOrHigherPriority(node.childExpirationTime_opaque, expirationTime) - ) { - node.childExpirationTime_opaque = expirationTime; - if ( - alternate !== null && - !isSameOrHigherPriority( - alternate.childExpirationTime_opaque, - expirationTime, - ) - ) { - alternate.childExpirationTime_opaque = expirationTime; - } - } else if ( - alternate !== null && - !isSameOrHigherPriority( - alternate.childExpirationTime_opaque, - expirationTime, - ) - ) { - alternate.childExpirationTime_opaque = expirationTime; + node.childLanes = mergeLanes(node.childLanes, lane); + if (alternate !== null) { + alternate.childLanes = mergeLanes(alternate.childLanes, lane); } if (node.return === null && node.tag === HostRoot) { root = node.stateNode; @@ -569,76 +520,30 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) { } if (root !== null) { + // Mark that the root has a pending update. + markRootUpdated(root, lane); if (workInProgressRoot === root) { // Received an update to a tree that's in the middle of rendering. Mark - // that's unprocessed work on this root. - markUnprocessedUpdateTime(expirationTime); - + // that there is unprocessed work on this root. + workInProgressRootUpdatedLanes = mergeLanes( + workInProgressRootUpdatedLanes, + lane, + ); if (workInProgressRootExitStatus === RootSuspendedWithDelay) { // The root already suspended with a delay, which means this render // definitely won't finish. Since we have a new update, let's mark it as // suspended now, right before marking the incoming update. This has the // effect of interrupting the current render and switching to the update. - // TODO: This happens to work when receiving an update during the render - // phase, because of the trick inside requestUpdateExpirationTime to - // subtract 1 from `renderExpirationTime` to move it into a - // separate bucket. But we should probably model it with an exception, - // using the same mechanism we use to force hydration of a subtree. - // TODO: This does not account for low pri updates that were already - // scheduled before the root started rendering. Need to track the next - // pending expiration time (perhaps by backtracking the return path) and - // then trigger a restart in the `renderDidSuspendDelayIfPossible` path. - markRootSuspendedAtTime(root, renderExpirationTime); + // TODO: Make sure this doesn't override pings that happen while we've + // already started rendering. + markRootSuspended(root, workInProgressRootRenderLanes); } } - // Mark that the root has a pending update. - markRootUpdatedAtTime(root, expirationTime); } return root; } -function getNextRootExpirationTimeToWorkOn( - root: FiberRoot, -): ExpirationTimeOpaque { - // Determines the next expiration time that the root should render, taking - // into account levels that may be suspended, or levels that may have - // received a ping. - - const lastExpiredTime = root.lastExpiredTime_opaque; - if (!isSameExpirationTime(lastExpiredTime, (NoWork: ExpirationTimeOpaque))) { - return lastExpiredTime; - } - - // "Pending" refers to any update that hasn't committed yet, including if it - // suspended. The "suspended" range is therefore a subset. - const firstPendingTime = root.firstPendingTime_opaque; - if (!isRootSuspendedAtTime(root, firstPendingTime)) { - // The highest priority pending time is not suspended. Let's work on that. - return firstPendingTime; - } - - // If the first pending time is suspended, check if there's a lower priority - // pending level that we know about. Or check if we received a ping. Work - // on whichever is higher priority. - const lastPingedTime = root.lastPingedTime_opaque; - const nextKnownPendingLevel = root.nextKnownPendingLevel_opaque; - const nextLevel = !isSameOrHigherPriority( - nextKnownPendingLevel, - lastPingedTime, - ) - ? lastPingedTime - : nextKnownPendingLevel; - if ( - isSameOrHigherPriority((Idle: ExpirationTimeOpaque), nextLevel) && - !isSameExpirationTime(firstPendingTime, nextLevel) - ) { - // Don't work on Idle/Never priority unless everything else is committed. - return NoWork; - } - return nextLevel; -} - // Use this function to schedule a task for a root. There's only one task per // root; if a task was already scheduled, we'll check to make sure the // expiration time of the existing task is the same as the expiration time of @@ -647,30 +552,31 @@ function getNextRootExpirationTimeToWorkOn( function ensureRootIsScheduled(root: FiberRoot) { const existingCallbackNode = root.callbackNode; - const newCallbackId = getNextRootExpirationTimeToWorkOn(root); - if (newCallbackId === (NoWork: ExpirationTimeOpaque)) { + const newCallbackId = getNextLanes( + root, + root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes, + ); + // This returns the priority level computed during the `getNextLanes` call. + const newCallbackPriorityLevel = returnNextLanesPriority(); + + if (newCallbackId === NoLanes) { // Special case: There's nothing to work on. if (existingCallbackNode !== null) { cancelCallback(existingCallbackNode); root.expiresAt = -1; root.callbackNode = null; root.callbackIsSync = false; - root.callbackId = NoWork; + root.callbackId = NoLanes; } return; } - const newTaskIsSync = - newCallbackId === (Sync: ExpirationTimeOpaque) || - !isSameExpirationTime( - root.lastExpiredTime_opaque, - (NoWork: ExpirationTimeOpaque), - ); + const newTaskIsSync = newCallbackPriorityLevel === SyncLanePriority; // Check if there's an existing task. We may be able to reuse it. const existingTaskId = root.callbackId; const existingCallbackIsSync = root.callbackIsSync; - if (existingTaskId !== (NoWork: ExpirationTimeOpaque)) { + if (existingTaskId !== NoLanes) { if (newCallbackId === existingTaskId) { // This task is already scheduled. Let's check its priority. if ( @@ -689,48 +595,19 @@ function ensureRootIsScheduled(root: FiberRoot) { // Schedule a new callback. let newCallbackNode; if (newTaskIsSync) { - // Special case: Sync React callbacks are scheduled on a special internal queue + // Special case: Sync React callbacks are scheduled on a special + // internal queue newCallbackNode = scheduleSyncCallback( performSyncWorkOnRoot.bind(null, root), ); } else { - // TODO: Use LanePriority instead of SchedulerPriority - const priorityLevel = inferPriorityFromExpirationTime(newCallbackId); - if ( - priorityLevel === NormalSchedulerPriority || - priorityLevel === UserBlockingSchedulerPriority - ) { - const existingExpirationTime = root.expiresAt; - const currentTimeMs = now(); - - // Compute an expiration time based on the priority level. - const expiration = - priorityLevel === UserBlockingSchedulerPriority ? 250 : 5000; - - let msUntilExpiration; - if (existingExpirationTime === -1) { - // This is the first concurrent update on the root. Use the expiration - // time we just computed. - msUntilExpiration = expiration; - root.expiresAt = msUntilExpiration + currentTimeMs; - } else { - // There's already an expiration time. Use the smaller of the current - // expiration and the one we just computed. - msUntilExpiration = existingExpirationTime - currentTimeMs; - if (expiration < msUntilExpiration) { - root.expiresAt = expiration; - } - } - newCallbackNode = scheduleCallback( - priorityLevel, - performConcurrentWorkOnRoot.bind(null, root), - ); - } else { - newCallbackNode = scheduleCallback( - priorityLevel, - performConcurrentWorkOnRoot.bind(null, root), - ); - } + const schedulerPriorityLevel = lanePriorityToSchedulerPriority( + newCallbackPriorityLevel, + ); + newCallbackNode = scheduleCallback( + schedulerPriorityLevel, + performConcurrentWorkOnRoot.bind(null, root), + ); } root.callbackId = newCallbackId; @@ -744,18 +621,40 @@ function performConcurrentWorkOnRoot(root, didTimeout) { // Since we know we're in a React event, we can clear the current // event time. The next update will compute a new event time. currentEventTime = -1; + currentEventWipLanes = NoLanes; + currentEventPendingLanes = NoLanes; // Determine the next expiration time to work on, using the fields stored // on the root. - let expirationTime = getNextRootExpirationTimeToWorkOn(root); - if (isSameExpirationTime(expirationTime, (NoWork: ExpirationTimeOpaque))) { + let lanes = getNextLanes( + root, + root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes, + ); + if (lanes === NoLanes) { return null; } + // Check if any work has expired. + const rootExpiresAt = root.expiresAt; + if (rootExpiresAt !== -1 && rootExpiresAt < now()) { + // Something expired. Flush synchronously until there's no expired + // work left. + // TODO: Should flush only the lanes that have expired, and maybe any lanes + // that are higher priority than that. + markRootExpired(root, lanes); + // This will schedule a synchronous callback. + ensureRootIsScheduled(root); + return null; + } + // Similar branch, but for Scheduler. + // TODO: This is only here to account for a Scheduler bug where `shouldYield` + // sometimes returns `true` even if `didTimeout` is true, which leads to + // an infinite loop. Once the bug in Scheduler is fixed, we can remove this, + // since we track expiration times ourselves. if (didTimeout) { - // The render task took too long to complete. Mark the root as expired to + // The Scheduler task took too long to complete. Mark the root as expired to // prevent yielding to other tasks until this one finishes. - markRootExpiredAtTime(root, expirationTime); + markRootExpired(root, lanes); // This will schedule a synchronous callback. ensureRootIsScheduled(root); return null; @@ -769,28 +668,37 @@ function performConcurrentWorkOnRoot(root, didTimeout) { flushPassiveEffects(); - let exitStatus = renderRootConcurrent(root, expirationTime); + let exitStatus = renderRootConcurrent(root, lanes); - if (exitStatus !== RootIncomplete) { + if ( + includesSomeLane( + workInProgressRootIncludedLanes, + workInProgressRootUpdatedLanes, + ) + ) { + // The render included lanes that were updated during the render phase. + // For example, when unhiding a hidden tree, we include all the lanes + // that were previously skipped when the tree was hidden. That set of + // lanes is a superset of the lanes we started rendering with. + // + // So we'll throw out the current work and restart. + prepareFreshStack(root, NoLanes); + } else if (exitStatus !== RootIncomplete) { if (exitStatus === RootErrored) { - // If something threw an error, try rendering one more time. We'll - // render synchronously to block concurrent data mutations, and we'll - // render at Idle (or lower) so that all pending updates are included. - // If it still fails after the second attempt, we'll give up and commit - // the resulting tree. - expirationTime = !isSameOrHigherPriority( - (Idle: ExpirationTimeOpaque), - expirationTime, - ) - ? (Idle: ExpirationTimeOpaque) - : expirationTime; - exitStatus = renderRootSync(root, expirationTime); + // If something threw an error, try rendering one more time. We'll render + // synchronously to block concurrent data mutations, and we'll includes + // all pending updates are included. If it still fails after the second + // attempt, we'll give up and commit the resulting tree. + lanes = getLanesToRetrySynchronouslyOnError(root); + if (lanes !== NoLanes) { + exitStatus = renderRootSync(root, lanes); + } } if (exitStatus === RootFatalErrored) { const fatalError = workInProgressRootFatalError; - prepareFreshStack(root, expirationTime); - markRootSuspendedAtTime(root, expirationTime); + prepareFreshStack(root, NoLanes); + markRootSuspended(root, lanes); ensureRootIsScheduled(root); throw fatalError; } @@ -799,11 +707,8 @@ function performConcurrentWorkOnRoot(root, didTimeout) { // or, if something suspended, wait to commit it after a timeout. const finishedWork: Fiber = (root.current.alternate: any); root.finishedWork = finishedWork; - root.finishedExpirationTime_opaque = expirationTime; - root.nextKnownPendingLevel_opaque = getRemainingExpirationTime( - finishedWork, - ); - finishConcurrentRender(root, finishedWork, exitStatus, expirationTime); + root.finishedLanes = lanes; + finishConcurrentRender(root, finishedWork, exitStatus, lanes); } ensureRootIsScheduled(root); @@ -815,12 +720,7 @@ function performConcurrentWorkOnRoot(root, didTimeout) { return null; } -function finishConcurrentRender( - root, - finishedWork, - exitStatus, - expirationTime, -) { +function finishConcurrentRender(root, finishedWork, exitStatus, lanes) { switch (exitStatus) { case RootIncomplete: case RootFatalErrored: { @@ -836,8 +736,7 @@ function finishConcurrentRender( break; } case RootSuspended: { - markRootSuspendedAtTime(root, expirationTime); - const lastSuspendedTime = root.lastSuspendedTime_opaque; + markRootSuspended(root, lanes); // We have an acceptable loading state. We need to figure out if we // should immediately commit it or wait a bit. @@ -862,42 +761,18 @@ function finishConcurrentRender( globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now(); // Don't bother with a very short suspense time. if (msUntilTimeout > 10) { - if (workInProgressRootHasPendingPing) { - const lastPingedTime = root.lastPingedTime_opaque; - if ( - isSameExpirationTime( - lastPingedTime, - (NoWork: ExpirationTimeOpaque), - ) || - isSameOrHigherPriority(lastPingedTime, expirationTime) - ) { - // This render was pinged but we didn't get to restart - // earlier so try restarting now instead. - root.lastPingedTime_opaque = expirationTime; - prepareFreshStack(root, expirationTime); - break; - } - } - - const nextTime = getNextRootExpirationTimeToWorkOn(root); - if ( - !isSameExpirationTime(nextTime, (NoWork: ExpirationTimeOpaque)) && - !isSameExpirationTime(nextTime, expirationTime) - ) { + const nextLanes = getNextLanes(root, NoLanes); + if (nextLanes !== NoLanes) { // There's additional work on this root. break; } - if ( - !isSameExpirationTime( - lastSuspendedTime, - (NoWork: ExpirationTimeOpaque), - ) && - !isSameExpirationTime(lastSuspendedTime, expirationTime) - ) { + const suspendedLanes = root.suspendedLanes; + if (!isSubsetOfLanes(suspendedLanes, lanes)) { // We should prefer to render the fallback of at the last // suspended level. Ping the last suspended level to try // rendering it again. - root.lastPingedTime_opaque = lastSuspendedTime; + // FIXME: What if the suspended lanes are Idle? Should not restart. + markRootPinged(root, suspendedLanes); break; } @@ -916,8 +791,7 @@ function finishConcurrentRender( break; } case RootSuspendedWithDelay: { - markRootSuspendedAtTime(root, expirationTime); - const lastSuspendedTime = root.lastSuspendedTime_opaque; + markRootSuspended(root, lanes); if ( // do not delay if we're inside an act() scope @@ -925,42 +799,18 @@ function finishConcurrentRender( ) { // We're suspended in a state that should be avoided. We'll try to // avoid committing it for as long as the timeouts let us. - if (workInProgressRootHasPendingPing) { - const lastPingedTime = root.lastPingedTime_opaque; - if ( - isSameExpirationTime( - lastPingedTime, - (NoWork: ExpirationTimeOpaque), - ) || - isSameOrHigherPriority(lastPingedTime, expirationTime) - ) { - // This render was pinged but we didn't get to restart earlier - // so try restarting now instead. - root.lastPingedTime_opaque = expirationTime; - prepareFreshStack(root, expirationTime); - break; - } - } - - const nextTime = getNextRootExpirationTimeToWorkOn(root); - if ( - !isSameExpirationTime(nextTime, (NoWork: ExpirationTimeOpaque)) && - !isSameExpirationTime(nextTime, expirationTime) - ) { + const nextLanes = getNextLanes(root, NoLanes); + if (nextLanes !== NoLanes) { // There's additional work on this root. break; } - if ( - !isSameExpirationTime( - lastSuspendedTime, - (NoWork: ExpirationTimeOpaque), - ) && - !isSameExpirationTime(lastSuspendedTime, expirationTime) - ) { + const suspendedLanes = root.suspendedLanes; + if (!isSubsetOfLanes(suspendedLanes, lanes)) { // We should prefer to render the fallback of at the last // suspended level. Ping the last suspended level to try // rendering it again. - root.lastPingedTime_opaque = lastSuspendedTime; + // FIXME: What if the suspended lanes are Idle? Should not restart. + markRootPinged(root, suspendedLanes); break; } @@ -1015,7 +865,7 @@ function finishConcurrentRender( workInProgressRootCanSuspendUsingConfig, ); if (msUntilTimeout > 10) { - markRootSuspendedAtTime(root, expirationTime); + markRootSuspended(root, lanes); root.timeoutHandle = scheduleTimeout( commitRoot.bind(null, root), msUntilTimeout, @@ -1032,6 +882,16 @@ function finishConcurrentRender( } } +function markRootSuspended(root, suspendedLanes) { + // When suspending, we should always exclude lanes that were pinged or (more + // rarely, since we try to avoid it) updated during the render phase. + // TODO: Lol maybe there's a better way to factor this besides this + // obnoxiously named function :) + suspendedLanes = removeLanes(suspendedLanes, workInProgressRootPingedLanes); + suspendedLanes = removeLanes(suspendedLanes, workInProgressRootUpdatedLanes); + markRootSuspended_dontCallThisOneDirectly(root, suspendedLanes); +} + // This is the entry point for synchronous tasks that don't go // through Scheduler function performSyncWorkOnRoot(root) { @@ -1042,50 +902,53 @@ function performSyncWorkOnRoot(root) { flushPassiveEffects(); - const lastExpiredTime = root.lastExpiredTime_opaque; - - let expirationTime; - if (!isSameExpirationTime(lastExpiredTime, (NoWork: ExpirationTimeOpaque))) { - // There's expired work on this root. Check if we have a partial tree - // that we can reuse. + let lanes; + let exitStatus; + if ( + root === workInProgressRoot && + includesSomeLane(root.expiredLanes, workInProgressRootRenderLanes) + ) { + // There's a partial tree, and at least one of its lanes has expired. Finish + // rendering it before rendering the rest of the expired work. + lanes = workInProgressRootRenderLanes; + exitStatus = renderRootSync(root, lanes); if ( - root === workInProgressRoot && - isSameOrHigherPriority(renderExpirationTime, lastExpiredTime) + includesSomeLane( + workInProgressRootIncludedLanes, + workInProgressRootUpdatedLanes, + ) ) { - // There's a partial tree with equal or greater than priority than the - // expired level. Finish rendering it before rendering the rest of the - // expired work. - expirationTime = renderExpirationTime; - } else { - // Start a fresh tree. - expirationTime = lastExpiredTime; + // The render included lanes that were updated during the render phase. + // For example, when unhiding a hidden tree, we include all the lanes + // that were previously skipped when the tree was hidden. That set of + // lanes is a superset of the lanes we started rendering with. + // + // Note that this only happens when part of the tree is rendered + // concurrently. If the whole tree is rendered synchronously, then there + // are no interleaved events. + lanes = getNextLanes(root, lanes); + exitStatus = renderRootSync(root, lanes); } } else { - // There's no expired work. This must be a new, synchronous render. - expirationTime = Sync; + lanes = getNextLanes(root, NoLanes); + exitStatus = renderRootSync(root, lanes); } - let exitStatus = renderRootSync(root, expirationTime); - if (root.tag !== LegacyRoot && exitStatus === RootErrored) { - // If something threw an error, try rendering one more time. We'll - // render synchronously to block concurrent data mutations, and we'll - // render at Idle (or lower) so that all pending updates are included. - // If it still fails after the second attempt, we'll give up and commit - // the resulting tree. - expirationTime = !isSameOrHigherPriority( - (Idle: ExpirationTimeOpaque), - expirationTime, - ) - ? (Idle: ExpirationTimeOpaque) - : expirationTime; - exitStatus = renderRootSync(root, expirationTime); + // If something threw an error, try rendering one more time. We'll render + // synchronously to block concurrent data mutations, and we'll includes + // all pending updates are included. If it still fails after the second + // attempt, we'll give up and commit the resulting tree. + lanes = getLanesToRetrySynchronouslyOnError(root); + if (lanes !== NoLanes) { + exitStatus = renderRootSync(root, lanes); + } } if (exitStatus === RootFatalErrored) { const fatalError = workInProgressRootFatalError; - prepareFreshStack(root, expirationTime); - markRootSuspendedAtTime(root, expirationTime); + prepareFreshStack(root, NoLanes); + markRootSuspended(root, lanes); ensureRootIsScheduled(root); throw fatalError; } @@ -1094,8 +957,7 @@ function performSyncWorkOnRoot(root) { // will commit it even if something suspended. const finishedWork: Fiber = (root.current.alternate: any); root.finishedWork = finishedWork; - root.finishedExpirationTime_opaque = expirationTime; - root.nextKnownPendingLevel_opaque = getRemainingExpirationTime(finishedWork); + root.finishedLanes = lanes; commitRoot(root); // Before exiting, make sure there's a callback scheduled for the next @@ -1105,11 +967,8 @@ function performSyncWorkOnRoot(root) { return null; } -export function flushRoot( - root: FiberRoot, - expirationTime: ExpirationTimeOpaque, -) { - markRootExpiredAtTime(root, expirationTime); +export function flushRoot(root: FiberRoot, lanes: Lanes) { + markRootExpired(root, lanes); ensureRootIsScheduled(root); if ((executionContext & (RenderContext | CommitContext)) === NoContext) { flushSyncCallbackQueue(); @@ -1164,8 +1023,8 @@ function flushPendingDiscreteUpdates() { // immediately flush them. const roots = rootsWithPendingDiscreteUpdates; rootsWithPendingDiscreteUpdates = null; - roots.forEach((expirationTime, root) => { - markRootExpiredAtTime(root, expirationTime); + roots.forEach(root => { + markDiscreteUpdatesExpired(root); ensureRootIsScheduled(root); }); } @@ -1278,22 +1137,23 @@ export function flushControlled(fn: () => mixed): void { } } -export function pushRenderExpirationTime( - fiber: Fiber, - subtreeRenderTime: ExpirationTimeOpaque, -) { - pushToStack(renderExpirationTimeCursor, renderExpirationTime, fiber); - renderExpirationTime = subtreeRenderTime; +export function pushRenderLanes(fiber: Fiber, lanes: Lanes) { + pushToStack(subtreeRenderLanesCursor, subtreeRenderLanes, fiber); + subtreeRenderLanes = mergeLanes(subtreeRenderLanes, lanes); + workInProgressRootIncludedLanes = mergeLanes( + workInProgressRootIncludedLanes, + lanes, + ); } -export function popRenderExpirationTime(fiber: Fiber) { - renderExpirationTime = renderExpirationTimeCursor.current; - popFromStack(renderExpirationTimeCursor, fiber); +export function popRenderLanes(fiber: Fiber) { + subtreeRenderLanes = subtreeRenderLanesCursor.current; + popFromStack(subtreeRenderLanesCursor, fiber); } -function prepareFreshStack(root, expirationTime) { +function prepareFreshStack(root: FiberRoot, lanes: Lanes) { root.finishedWork = null; - root.finishedExpirationTime_opaque = NoWork; + root.finishedLanes = NoLanes; const timeoutHandle = root.timeoutHandle; if (timeoutHandle !== noTimeout) { @@ -1304,27 +1164,6 @@ function prepareFreshStack(root, expirationTime) { cancelTimeout(timeoutHandle); } - // Check if there's a suspended level at lower priority. - const lastSuspendedTime = root.lastSuspendedTime_opaque; - if ( - !isSameExpirationTime(lastSuspendedTime, (NoWork: ExpirationTimeOpaque)) && - !isSameOrHigherPriority(lastSuspendedTime, expirationTime) - ) { - const lastPingedTime = root.lastPingedTime_opaque; - // Make sure the suspended level is marked as pinged so that we return back - // to it later, in case the render we're about to start gets aborted. - // Generally we only reach this path via a ping, but we shouldn't assume - // that will always be the case. - // Note: This is defensive coding to prevent a pending commit from - // being dropped without being rescheduled. It shouldn't be necessary. - if ( - isSameExpirationTime(lastPingedTime, (NoWork: ExpirationTimeOpaque)) || - !isSameOrHigherPriority(lastSuspendedTime, lastPingedTime) - ) { - root.lastPingedTime_opaque = lastSuspendedTime; - } - } - if (workInProgress !== null) { let interruptedWork = workInProgress.return; while (interruptedWork !== null) { @@ -1334,14 +1173,15 @@ function prepareFreshStack(root, expirationTime) { } workInProgressRoot = root; workInProgress = createWorkInProgress(root.current, null); - renderExpirationTime = expirationTime; + workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes; workInProgressRootExitStatus = RootIncomplete; workInProgressRootFatalError = null; workInProgressRootLatestProcessedEventTime = -1; workInProgressRootLatestSuspenseTimeout = -1; workInProgressRootCanSuspendUsingConfig = null; - workInProgressRootNextUnprocessedUpdateTime = NoWork; - workInProgressRootHasPendingPing = false; + workInProgressRootSkippedLanes = NoLanes; + workInProgressRootUpdatedLanes = NoLanes; + workInProgressRootPingedLanes = NoLanes; if (enableSchedulerTracing) { spawnedWorkDuringRender = null; @@ -1393,7 +1233,7 @@ function handleError(root, thrownValue): void { erroredWork.return, erroredWork, thrownValue, - renderExpirationTime, + workInProgressRootRenderLanes, ); completeUnitOfWork(erroredWork); } catch (yetAnotherThrownValue) { @@ -1479,17 +1319,11 @@ export function markRenderEventTimeAndConfig( } } -export function markUnprocessedUpdateTime( - expirationTime: ExpirationTimeOpaque, -): void { - if ( - !isSameOrHigherPriority( - workInProgressRootNextUnprocessedUpdateTime, - expirationTime, - ) - ) { - workInProgressRootNextUnprocessedUpdateTime = expirationTime; - } +export function markSkippedUpdateLanes(lane: Lane | Lanes): void { + workInProgressRootSkippedLanes = mergeLanes( + lane, + workInProgressRootSkippedLanes, + ); } export function renderDidSuspend(): void { @@ -1506,23 +1340,21 @@ export function renderDidSuspendDelayIfPossible(): void { workInProgressRootExitStatus = RootSuspendedWithDelay; } - // Check if there's a lower priority update somewhere else in the tree. + // Check if there are updates that we skipped tree that might have unblocked + // this render. if ( - !isSameExpirationTime( - workInProgressRootNextUnprocessedUpdateTime, - (NoWork: ExpirationTimeOpaque), - ) && - workInProgressRoot !== null + workInProgressRoot !== null && + (hasUpdatePriority(workInProgressRootSkippedLanes) || + hasUpdatePriority(workInProgressRootUpdatedLanes)) ) { - // Mark the current render as suspended, and then mark that there's a - // pending update. - // TODO: This should immediately interrupt the current render, instead - // of waiting until the next time we yield. - markRootSuspendedAtTime(workInProgressRoot, renderExpirationTime); - markRootUpdatedAtTime( - workInProgressRoot, - workInProgressRootNextUnprocessedUpdateTime, - ); + // Mark the current render as suspended so that we switch to working on + // the updates that were skipped. Usually we only suspend at the end of + // the render phase. + // TODO: We should probably always mark the root as suspended immediately + // (inside this function), since by suspending at the end of the render + // phase introduces a potential mistake where we suspend lanes that were + // pinged or updated while we were rendering. + markRootSuspended(workInProgressRoot, workInProgressRootRenderLanes); } } @@ -1540,19 +1372,16 @@ export function renderHasNotSuspendedYet(): boolean { return workInProgressRootExitStatus === RootIncomplete; } -function renderRootSync(root, expirationTime) { +function renderRootSync(root: FiberRoot, lanes: Lanes) { const prevExecutionContext = executionContext; executionContext |= RenderContext; const prevDispatcher = pushDispatcher(root); - // If the root or expiration time have changed, throw out the existing stack + // If the root or lanes have changed, throw out the existing stack // and prepare a fresh one. Otherwise we'll continue where we left off. - if ( - root !== workInProgressRoot || - !isSameExpirationTime(expirationTime, renderExpirationTime) - ) { - prepareFreshStack(root, expirationTime); - startWorkOnPendingInteractions(root, expirationTime); + if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) { + prepareFreshStack(root, lanes); + startWorkOnPendingInteractions(root, lanes); } const prevInteractions = pushInteractions(root); @@ -1584,6 +1413,7 @@ function renderRootSync(root, expirationTime) { // Set this to null to indicate there's no in-progress render. workInProgressRoot = null; + workInProgressRootRenderLanes = NoLanes; return workInProgressRootExitStatus; } @@ -1597,19 +1427,16 @@ function workLoopSync() { } } -function renderRootConcurrent(root, expirationTime) { +function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { const prevExecutionContext = executionContext; executionContext |= RenderContext; const prevDispatcher = pushDispatcher(root); - // If the root or expiration time have changed, throw out the existing stack + // If the root or lanes have changed, throw out the existing stack // and prepare a fresh one. Otherwise we'll continue where we left off. - if ( - root !== workInProgressRoot || - !isSameExpirationTime(expirationTime, renderExpirationTime) - ) { - prepareFreshStack(root, expirationTime); - startWorkOnPendingInteractions(root, expirationTime); + if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) { + prepareFreshStack(root, lanes); + startWorkOnPendingInteractions(root, lanes); } const prevInteractions = pushInteractions(root); @@ -1638,6 +1465,7 @@ function renderRootConcurrent(root, expirationTime) { // Completed the tree. // Set this to null to indicate there's no in-progress render. workInProgressRoot = null; + workInProgressRootRenderLanes = NoLanes; // Return the final exit status. return workInProgressRootExitStatus; @@ -1662,10 +1490,10 @@ function performUnitOfWork(unitOfWork: Fiber): void { let next; if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) { startProfilerTimer(unitOfWork); - next = beginWork(current, unitOfWork, renderExpirationTime); + next = beginWork(current, unitOfWork, subtreeRenderLanes); stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); } else { - next = beginWork(current, unitOfWork, renderExpirationTime); + next = beginWork(current, unitOfWork, subtreeRenderLanes); } resetCurrentDebugFiberInDEV(); @@ -1699,15 +1527,15 @@ function completeUnitOfWork(unitOfWork: Fiber): void { !enableProfilerTimer || (completedWork.mode & ProfileMode) === NoMode ) { - next = completeWork(current, completedWork, renderExpirationTime); + next = completeWork(current, completedWork, subtreeRenderLanes); } else { startProfilerTimer(completedWork); - next = completeWork(current, completedWork, renderExpirationTime); + next = completeWork(current, completedWork, subtreeRenderLanes); // Update render duration assuming we didn't error. stopProfilerTimerIfRunningAndRecordDelta(completedWork, false); } resetCurrentDebugFiberInDEV(); - resetChildExpirationTime(completedWork); + resetChildLanes(completedWork); if (next !== null) { // Completing this fiber spawned new work. Work on that next. @@ -1757,7 +1585,7 @@ function completeUnitOfWork(unitOfWork: Fiber): void { // This fiber did not complete because something threw. Pop values off // the stack without entering the complete phase. If this is a boundary, // capture values if possible. - const next = unwindWork(completedWork, renderExpirationTime); + const next = unwindWork(completedWork, subtreeRenderLanes); // Because this fiber did not complete, don't reset its expiration time. @@ -1813,31 +1641,21 @@ function completeUnitOfWork(unitOfWork: Fiber): void { } } -function getRemainingExpirationTime(fiber: Fiber) { - const updateExpirationTime = fiber.expirationTime_opaque; - const childExpirationTime = fiber.childExpirationTime_opaque; - return !isSameOrHigherPriority(childExpirationTime, updateExpirationTime) - ? updateExpirationTime - : childExpirationTime; -} - -function resetChildExpirationTime(completedWork: Fiber) { +function resetChildLanes(completedWork: Fiber) { if ( - !isSameExpirationTime( - renderExpirationTime, - (Never: ExpirationTimeOpaque), - ) && - isSameExpirationTime( - completedWork.childExpirationTime_opaque, - (Never: ExpirationTimeOpaque), - ) + // TODO: Move this check out of the hot path by moving `resetChildLanes` + // to switch statement in `completeWork`. + (completedWork.tag === LegacyHiddenComponent || + completedWork.tag === OffscreenComponent) && + completedWork.memoizedState !== null && + !includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane)) ) { // The children of this component are hidden. Don't bubble their // expiration times. return; } - let newChildExpirationTime = NoWork; + let newChildLanes = NoLanes; // Bubble up the earliest expiration time. if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) { @@ -1859,24 +1677,10 @@ function resetChildExpirationTime(completedWork: Fiber) { let child = completedWork.child; while (child !== null) { - const childUpdateExpirationTime = child.expirationTime_opaque; - const childChildExpirationTime = child.childExpirationTime_opaque; - if ( - !isSameOrHigherPriority( - newChildExpirationTime, - childUpdateExpirationTime, - ) - ) { - newChildExpirationTime = childUpdateExpirationTime; - } - if ( - !isSameOrHigherPriority( - newChildExpirationTime, - childChildExpirationTime, - ) - ) { - newChildExpirationTime = childChildExpirationTime; - } + newChildLanes = mergeLanes( + newChildLanes, + mergeLanes(child.lanes, child.childLanes), + ); if (shouldBubbleActualDurations) { actualDuration += child.actualDuration; } @@ -1888,29 +1692,15 @@ function resetChildExpirationTime(completedWork: Fiber) { } else { let child = completedWork.child; while (child !== null) { - const childUpdateExpirationTime = child.expirationTime_opaque; - const childChildExpirationTime = child.childExpirationTime_opaque; - if ( - !isSameOrHigherPriority( - newChildExpirationTime, - childUpdateExpirationTime, - ) - ) { - newChildExpirationTime = childUpdateExpirationTime; - } - if ( - !isSameOrHigherPriority( - newChildExpirationTime, - childChildExpirationTime, - ) - ) { - newChildExpirationTime = childChildExpirationTime; - } + newChildLanes = mergeLanes( + newChildLanes, + mergeLanes(child.lanes, child.childLanes), + ); child = child.sibling; } } - completedWork.childExpirationTime_opaque = newChildExpirationTime; + completedWork.childLanes = newChildLanes; } function commitRoot(root) { @@ -1940,12 +1730,12 @@ function commitRootImpl(root, renderPriorityLevel) { ); const finishedWork = root.finishedWork; - const expirationTime = root.finishedExpirationTime_opaque; + const lanes = root.finishedLanes; if (finishedWork === null) { return null; } root.finishedWork = null; - root.finishedExpirationTime_opaque = NoWork; + root.finishedLanes = NoLanes; invariant( finishedWork !== root.current, @@ -1956,7 +1746,7 @@ function commitRootImpl(root, renderPriorityLevel) { // commitRoot never returns a continuation; it always finishes synchronously. // So we can clear these now to allow a new callback to be scheduled. root.callbackNode = null; - root.callbackId = NoWork; + root.callbackId = NoLanes; // TODO: Use LanePriority instead of SchedulerPriority if (renderPriorityLevel < ImmediateSchedulerPriority) { // If this was a concurrent render, we can reset the expiration time. @@ -1965,26 +1755,16 @@ function commitRootImpl(root, renderPriorityLevel) { // Update the first and last pending times on this root. The new first // pending time is whatever is left on the root fiber. - const remainingExpirationTimeBeforeCommit = getRemainingExpirationTime( - finishedWork, - ); - markRootFinishedAtTime( - root, - expirationTime, - remainingExpirationTimeBeforeCommit, - ); + let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes); + markRootFinished(root, remainingLanes); // Clear already finished discrete updates in case that a later call of // `flushDiscreteUpdates` starts a useless render pass which may cancels // a scheduled timeout. if (rootsWithPendingDiscreteUpdates !== null) { - const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root); if ( - lastDiscreteTime !== undefined && - !isSameOrHigherPriority( - remainingExpirationTimeBeforeCommit, - lastDiscreteTime, - ) + !hasDiscreteLanes(remainingLanes) && + rootsWithPendingDiscreteUpdates.has(root) ) { rootsWithPendingDiscreteUpdates.delete(root); } @@ -1994,7 +1774,7 @@ function commitRootImpl(root, renderPriorityLevel) { // We can reset these now that they are finished. workInProgressRoot = null; workInProgress = null; - renderExpirationTime = NoWork; + workInProgressRootRenderLanes = NoLanes; } else { // This indicates that the last root we worked on is not the same one that // we're committing now. This most commonly happens when a suspended root @@ -2112,13 +1892,7 @@ function commitRootImpl(root, renderPriorityLevel) { nextEffect = firstEffect; do { if (__DEV__) { - invokeGuardedCallback( - null, - commitLayoutEffects, - null, - root, - expirationTime, - ); + invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes); if (hasCaughtError()) { invariant(nextEffect !== null, 'Should be working on an effect.'); const error = clearCaughtError(); @@ -2127,7 +1901,7 @@ function commitRootImpl(root, renderPriorityLevel) { } } else { try { - commitLayoutEffects(root, expirationTime); + commitLayoutEffects(root, lanes); } catch (error) { invariant(nextEffect !== null, 'Should be working on an effect.'); captureCommitPhaseError(nextEffect, error); @@ -2164,7 +1938,7 @@ function commitRootImpl(root, renderPriorityLevel) { // schedule a callback until after flushing layout work. rootDoesHavePassiveEffects = false; rootWithPendingPassiveEffects = root; - pendingPassiveEffectsExpirationTime = expirationTime; + pendingPassiveEffectsLanes = lanes; pendingPassiveEffectsRenderPriority = renderPriorityLevel; } else { // We are done with the effect chain at this point so let's clear the @@ -2178,14 +1952,11 @@ function commitRootImpl(root, renderPriorityLevel) { } } + // Read this again, since an effect might have updated it + remainingLanes = root.pendingLanes; + // Check if there's remaining work on this root - const remainingExpirationTime = root.firstPendingTime_opaque; - if ( - !isSameExpirationTime( - remainingExpirationTime, - (NoWork: ExpirationTimeOpaque), - ) - ) { + if (remainingLanes !== NoLanes) { if (enableSchedulerTracing) { if (spawnedWorkDuringRender !== null) { const expirationTimes = spawnedWorkDuringRender; @@ -2198,7 +1969,7 @@ function commitRootImpl(root, renderPriorityLevel) { ); } } - schedulePendingInteractions(root, remainingExpirationTime); + schedulePendingInteractions(root, remainingLanes); } } else { // If there's no remaining work, we can clear the set of already failed @@ -2212,13 +1983,11 @@ function commitRootImpl(root, renderPriorityLevel) { // Otherwise, we'll wait until after the passive effects are flushed. // Wait to do this until after remaining work has been scheduled, // so that we don't prematurely signal complete for interactions when there's e.g. hidden work. - finishPendingInteractions(root, expirationTime); + finishPendingInteractions(root, lanes); } } - if ( - isSameExpirationTime(remainingExpirationTime, (Sync: ExpirationTimeOpaque)) - ) { + if (remainingLanes === SyncLane) { // Count the number of times the root synchronously re-renders without // finishing. If there are too many, it indicates an infinite update loop. if (root === rootWithNestedUpdates) { @@ -2366,10 +2135,7 @@ function commitMutationEffects(root: FiberRoot, renderPriorityLevel) { } } -function commitLayoutEffects( - root: FiberRoot, - committedExpirationTime: ExpirationTimeOpaque, -) { +function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) { // TODO: Should probably move the bulk of this function to commitWork. while (nextEffect !== null) { setCurrentDebugFiberInDEV(nextEffect); @@ -2378,12 +2144,7 @@ function commitLayoutEffects( if (effectTag & (Update | Callback)) { const current = nextEffect.alternate; - commitLayoutEffectOnFiber( - root, - current, - nextEffect, - committedExpirationTime, - ); + commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes); } if (effectTag & Ref) { @@ -2471,9 +2232,9 @@ function flushPassiveEffectsImpl() { } const root = rootWithPendingPassiveEffects; - const expirationTime = pendingPassiveEffectsExpirationTime; + const lanes = pendingPassiveEffectsLanes; rootWithPendingPassiveEffects = null; - pendingPassiveEffectsExpirationTime = NoWork; + pendingPassiveEffectsLanes = NoLanes; invariant( (executionContext & (RenderContext | CommitContext)) === NoContext, @@ -2651,7 +2412,7 @@ function flushPassiveEffectsImpl() { if (enableSchedulerTracing) { popInteractions(((prevInteractions: any): Set)); - finishPendingInteractions(root, expirationTime); + finishPendingInteractions(root, lanes); } if (__DEV__) { @@ -2699,19 +2460,12 @@ function captureCommitPhaseErrorOnRoot( error: mixed, ) { const errorInfo = createCapturedValue(error, sourceFiber); - const update = createRootErrorUpdate( - rootFiber, - errorInfo, - (Sync: ExpirationTimeOpaque), - ); + const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane)); enqueueUpdate(rootFiber, update); - const root = markUpdateTimeFromFiberToRoot( - rootFiber, - (Sync: ExpirationTimeOpaque), - ); + const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane)); if (root !== null) { ensureRootIsScheduled(root); - schedulePendingInteractions(root, (Sync: ExpirationTimeOpaque)); + schedulePendingInteractions(root, SyncLane); } } @@ -2740,16 +2494,13 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) { const update = createClassErrorUpdate( fiber, errorInfo, - (Sync: ExpirationTimeOpaque), + (SyncLane: Lane), ); enqueueUpdate(fiber, update); - const root = markUpdateTimeFromFiberToRoot( - fiber, - (Sync: ExpirationTimeOpaque), - ); + const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane)); if (root !== null) { ensureRootIsScheduled(root); - schedulePendingInteractions(root, (Sync: ExpirationTimeOpaque)); + schedulePendingInteractions(root, SyncLane); } return; } @@ -2761,7 +2512,7 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) { export function pingSuspendedRoot( root: FiberRoot, wakeable: Wakeable, - suspendedTime: ExpirationTimeOpaque, + pingedLanes: Lanes, ) { const pingCache = root.pingCache; if (pingCache !== null) { @@ -2770,9 +2521,11 @@ export function pingSuspendedRoot( pingCache.delete(wakeable); } + markRootPinged(root, pingedLanes); + if ( workInProgressRoot === root && - isSameExpirationTime(renderExpirationTime, suspendedTime) + isSubsetOfLanes(workInProgressRootRenderLanes, pingedLanes) ) { // Received a ping at the same priority level at which we're currently // rendering. We might want to restart this render. This should mirror @@ -2793,69 +2546,52 @@ export function pingSuspendedRoot( workInProgressRootLatestProcessedEventTime === -1 && now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS) ) { - // Restart from the root. Don't need to schedule a ping because - // we're already working on this tree. - prepareFreshStack(root, renderExpirationTime); + // Restart from the root. + prepareFreshStack(root, NoLanes); } else { // Even though we can't restart right now, we might get an // opportunity later. So we mark this render as having a ping. - workInProgressRootHasPendingPing = true; + workInProgressRootPingedLanes = mergeLanes( + workInProgressRootPingedLanes, + pingedLanes, + ); } - return; - } - - if (!isRootSuspendedAtTime(root, suspendedTime)) { - // The root is no longer suspended at this time. - return; } - const lastPingedTime = root.lastPingedTime_opaque; - if ( - !isSameExpirationTime(lastPingedTime, (NoWork: ExpirationTimeOpaque)) && - !isSameOrHigherPriority(lastPingedTime, suspendedTime) - ) { - // There's already a lower priority ping scheduled. - return; - } - - // Mark the time at which this ping was scheduled. - root.lastPingedTime_opaque = suspendedTime; - ensureRootIsScheduled(root); - schedulePendingInteractions(root, suspendedTime); + schedulePendingInteractions(root, pingedLanes); } -function retryTimedOutBoundary( - boundaryFiber: Fiber, - retryTime: ExpirationTimeOpaque, -) { +function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) { // The boundary fiber (a Suspense component or SuspenseList component) // previously was rendered in its fallback state. One of the promises that // suspended it has resolved, which means at least part of the tree was // likely unblocked. Try rendering again, at a new expiration time. - if (isSameExpirationTime(retryTime, (NoWork: ExpirationTimeOpaque))) { + if (retryLane === NoLane) { const suspenseConfig = null; // Retries don't carry over the already committed update. - retryTime = requestUpdateExpirationTime(boundaryFiber, suspenseConfig); + // TODO: Should retries get their own lane? Maybe it could share with + // transitions. + retryLane = requestUpdateLane(boundaryFiber, suspenseConfig); } // TODO: Special case idle priority? - const root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime); + const root = markUpdateLaneFromFiberToRoot(boundaryFiber, retryLane); if (root !== null) { ensureRootIsScheduled(root); - schedulePendingInteractions(root, retryTime); + schedulePendingInteractions(root, retryLane); } } export function retryDehydratedSuspenseBoundary(boundaryFiber: Fiber) { const suspenseState: null | SuspenseState = boundaryFiber.memoizedState; - let retryTime = NoWork; + let retryLane = NoLane; if (suspenseState !== null) { - retryTime = suspenseState.retryTime; + retryLane = suspenseState.retryLane; } - retryTimedOutBoundary(boundaryFiber, retryTime); + retryTimedOutBoundary(boundaryFiber, retryLane); } export function resolveRetryWakeable(boundaryFiber: Fiber, wakeable: Wakeable) { - let retryTime = NoWork; // Default + let retryLane = NoLane; // Default let retryCache: WeakSet | Set | null; if (enableSuspenseServerRenderer) { switch (boundaryFiber.tag) { @@ -2863,7 +2599,7 @@ export function resolveRetryWakeable(boundaryFiber: Fiber, wakeable: Wakeable) { retryCache = boundaryFiber.stateNode; const suspenseState: null | SuspenseState = boundaryFiber.memoizedState; if (suspenseState !== null) { - retryTime = suspenseState.retryTime; + retryLane = suspenseState.retryLane; } break; case SuspenseListComponent: @@ -2886,7 +2622,7 @@ export function resolveRetryWakeable(boundaryFiber: Fiber, wakeable: Wakeable) { retryCache.delete(wakeable); } - retryTimedOutBoundary(boundaryFiber, retryTime); + retryTimedOutBoundary(boundaryFiber, retryLane); } // Computes the next Just Noticeable Difference (JND) boundary. @@ -3054,7 +2790,7 @@ function warnAboutUpdateOnUnmountedFiberInDEV(fiber) { let beginWork; if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { const dummyFiber = null; - beginWork = (current, unitOfWork, expirationTime) => { + beginWork = (current, unitOfWork, lanes) => { // If a component throws an error, we replay it again in a synchronously // dispatched event, so that the debugger will treat it as an uncaught // error See ReactErrorUtils for more information. @@ -3066,7 +2802,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { unitOfWork, ); try { - return originalBeginWork(current, unitOfWork, expirationTime); + return originalBeginWork(current, unitOfWork, lanes); } catch (originalError) { if ( originalError !== null && @@ -3102,7 +2838,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { null, current, unitOfWork, - expirationTime, + lanes, ); if (hasCaughtError()) { @@ -3316,31 +3052,28 @@ export function warnIfUnmockedScheduler(fiber: Fiber) { } } -function computeThreadID( - root: FiberRoot, - expirationTime: ExpirationTimeOpaque, -) { +function computeThreadID(root: FiberRoot, lane: Lane | Lanes) { // Interaction threads are unique per root and expiration time. // NOTE: Intentionally unsound cast. All that matters is that it's a number // and it represents a batch of work. Could make a helper function instead, // but meh this is fine for now. - return (expirationTime: any) * 1000 + root.interactionThreadID; + return (lane: any) * 1000 + root.interactionThreadID; } -export function markSpawnedWork(expirationTime: ExpirationTimeOpaque) { +export function markSpawnedWork(lane: Lane | Lanes) { if (!enableSchedulerTracing) { return; } if (spawnedWorkDuringRender === null) { - spawnedWorkDuringRender = [expirationTime]; + spawnedWorkDuringRender = [lane]; } else { - spawnedWorkDuringRender.push(expirationTime); + spawnedWorkDuringRender.push(lane); } } function scheduleInteractions( root: FiberRoot, - expirationTime: ExpirationTimeOpaque, + lane: Lane | Lanes, interactions: Set, ) { if (!enableSchedulerTracing) { @@ -3349,7 +3082,7 @@ function scheduleInteractions( if (interactions.size > 0) { const pendingInteractionMap = root.pendingInteractionMap_new; - const pendingInteractions = pendingInteractionMap.get(expirationTime); + const pendingInteractions = pendingInteractionMap.get(lane); if (pendingInteractions != null) { interactions.forEach(interaction => { if (!pendingInteractions.has(interaction)) { @@ -3360,7 +3093,7 @@ function scheduleInteractions( pendingInteractions.add(interaction); }); } else { - pendingInteractionMap.set(expirationTime, new Set(interactions)); + pendingInteractionMap.set(lane, new Set(interactions)); // Update the pending async work count for the current interactions. interactions.forEach(interaction => { @@ -3370,16 +3103,13 @@ function scheduleInteractions( const subscriber = __subscriberRef.current; if (subscriber !== null) { - const threadID = computeThreadID(root, expirationTime); + const threadID = computeThreadID(root, lane); subscriber.onWorkScheduled(interactions, threadID); } } } -function schedulePendingInteractions( - root: FiberRoot, - expirationTime: ExpirationTimeOpaque, -) { +function schedulePendingInteractions(root: FiberRoot, lane: Lane | Lanes) { // This is called when work is scheduled on a root. // It associates the current interactions with the newly-scheduled expiration. // They will be restored when that expiration is later committed. @@ -3387,13 +3117,10 @@ function schedulePendingInteractions( return; } - scheduleInteractions(root, expirationTime, __interactionsRef.current); + scheduleInteractions(root, lane, __interactionsRef.current); } -function startWorkOnPendingInteractions( - root: FiberRoot, - expirationTime: ExpirationTimeOpaque, -) { +function startWorkOnPendingInteractions(root: FiberRoot, lanes: Lanes) { // This is called when new work is started on a root. if (!enableSchedulerTracing) { return; @@ -3404,8 +3131,8 @@ function startWorkOnPendingInteractions( // work triggered during the render phase will be associated with it. const interactions: Set = new Set(); root.pendingInteractionMap_new.forEach( - (scheduledInteractions, scheduledExpirationTime) => { - if (isSameOrHigherPriority(scheduledExpirationTime, expirationTime)) { + (scheduledInteractions, scheduledLane) => { + if (includesSomeLane(lanes, scheduledLane)) { scheduledInteractions.forEach(interaction => interactions.add(interaction), ); @@ -3423,7 +3150,7 @@ function startWorkOnPendingInteractions( if (interactions.size > 0) { const subscriber = __subscriberRef.current; if (subscriber !== null) { - const threadID = computeThreadID(root, expirationTime); + const threadID = computeThreadID(root, lanes); try { subscriber.onWorkStarted(interactions, threadID); } catch (error) { @@ -3436,19 +3163,20 @@ function startWorkOnPendingInteractions( } } -function finishPendingInteractions(root, committedExpirationTime) { +function finishPendingInteractions(root, committedLanes) { if (!enableSchedulerTracing) { return; } - const earliestRemainingTimeAfterCommit = root.firstPendingTime_opaque; + const remainingLanesAfterCommit = root.pendingLanes; let subscriber; try { subscriber = __subscriberRef.current; if (subscriber !== null && root.memoizedInteractions.size > 0) { - const threadID = computeThreadID(root, committedExpirationTime); + // FIXME: More than one lane can finish in a single commit. + const threadID = computeThreadID(root, committedLanes); subscriber.onWorkStopped(root.memoizedInteractions, threadID); } } catch (error) { @@ -3461,36 +3189,29 @@ function finishPendingInteractions(root, committedExpirationTime) { // Unless the render was suspended or cascading work was scheduled, // In which case– leave pending interactions until the subsequent render. const pendingInteractionMap = root.pendingInteractionMap_new; - pendingInteractionMap.forEach( - (scheduledInteractions, scheduledExpirationTime) => { - // Only decrement the pending interaction count if we're done. - // If there's still work at the current priority, - // That indicates that we are waiting for suspense data. - if ( - !isSameOrHigherPriority( - earliestRemainingTimeAfterCommit, - scheduledExpirationTime, - ) - ) { - pendingInteractionMap.delete(scheduledExpirationTime); + pendingInteractionMap.forEach((scheduledInteractions, lane) => { + // Only decrement the pending interaction count if we're done. + // If there's still work at the current priority, + // That indicates that we are waiting for suspense data. + if (!includesSomeLane(remainingLanesAfterCommit, lane)) { + pendingInteractionMap.delete(lane); - scheduledInteractions.forEach(interaction => { - interaction.__count--; + scheduledInteractions.forEach(interaction => { + interaction.__count--; - if (subscriber !== null && interaction.__count === 0) { - try { - subscriber.onInteractionScheduledWorkCompleted(interaction); - } catch (error) { - // If the subscriber throws, rethrow it in a separate task - scheduleCallback(ImmediateSchedulerPriority, () => { - throw error; - }); - } + if (subscriber !== null && interaction.__count === 0) { + try { + subscriber.onInteractionScheduledWorkCompleted(interaction); + } catch (error) { + // If the subscriber throws, rethrow it in a separate task + scheduleCallback(ImmediateSchedulerPriority, () => { + throw error; + }); } - }); - } - }, - ); + } + }); + } + }); } } diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index edca38409314..2b12735c8c58 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -23,7 +23,7 @@ import type {WorkTag} from './ReactWorkTags'; import type {TypeOfMode} from './ReactTypeOfMode'; import type {SideEffectTag} from './ReactSideEffectTags'; import type {ExpirationTime} from './ReactFiberExpirationTime.old'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; +import type {Lane, Lanes} from './ReactFiberLane'; import type {HookType} from './ReactFiberHooks.old'; import type {RootTag} from './ReactRootTags'; import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig'; @@ -52,7 +52,7 @@ export type Dependencies_old = { }; export type Dependencies_new = { - expirationTime: ExpirationTimeOpaque, + lanes: Lanes, firstContext: ContextDependency | null, responders: Map< ReactEventResponder, @@ -148,16 +148,13 @@ export type Fiber = {| firstEffect: Fiber | null, lastEffect: Fiber | null, - // Represents a time in the future by which this work should be completed. - // Does not include work found in its subtree. + // Only used by old reconciler expirationTime: ExpirationTime, - - // This is used to quickly determine if a subtree has no pending changes. childExpirationTime: ExpirationTime, // Only used by new reconciler - expirationTime_opaque: ExpirationTimeOpaque, - childExpirationTime_opaque: ExpirationTimeOpaque, + lanes: Lanes, + childLanes: Lanes, // 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 @@ -255,8 +252,7 @@ type BaseFiberRootProperties = {| // Represents the next task that the root should work on, or the current one // if it's already working. - // TODO: In the new system, this will be a Lanes bitmask. - callbackId: ExpirationTimeOpaque, + callbackId: Lanes, // Whether the currently scheduled task for this root is synchronous or // batched/concurrent. We have to track this because Scheduler does not // support synchronous tasks, so we put those on a separate queue. So you @@ -269,17 +265,13 @@ type BaseFiberRootProperties = {| // timestamp, in milliseconds. expiresAt: number, - // Same as corresponding fields in the old reconciler, but opaque. These will - // become bitmasks. - finishedExpirationTime_opaque: ExpirationTimeOpaque, - firstPendingTime_opaque: ExpirationTimeOpaque, - lastPendingTime_opaque: ExpirationTimeOpaque, - firstSuspendedTime_opaque: ExpirationTimeOpaque, - lastSuspendedTime_opaque: ExpirationTimeOpaque, - nextKnownPendingLevel_opaque: ExpirationTimeOpaque, - lastPingedTime_opaque: ExpirationTimeOpaque, - lastExpiredTime_opaque: ExpirationTimeOpaque, - mutableSourceLastPendingUpdateTime_opaque: ExpirationTimeOpaque, + pendingLanes: Lanes, + suspendedLanes: Lanes, + pingedLanes: Lanes, + expiredLanes: Lanes, + mutableReadLanes: Lanes, + + finishedLanes: Lanes, |}; // The following attributes are only used by interaction tracing builds. @@ -289,7 +281,7 @@ type BaseFiberRootProperties = {| type ProfilingOnlyFiberRootProperties = {| interactionThreadID: number, memoizedInteractions: Set, - pendingInteractionMap_new: Map>, + pendingInteractionMap_new: Map>, pendingInteractionMap_old: Map>, |}; diff --git a/packages/react-reconciler/src/ReactMutableSource.new.js b/packages/react-reconciler/src/ReactMutableSource.new.js index bc11adcd3305..be7bb4dfdf59 100644 --- a/packages/react-reconciler/src/ReactMutableSource.new.js +++ b/packages/react-reconciler/src/ReactMutableSource.new.js @@ -7,16 +7,9 @@ * @flow */ -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; -import type {FiberRoot} from './ReactInternalTypes'; import type {MutableSource, MutableSourceVersion} from 'shared/ReactTypes'; import {isPrimaryRenderer} from './ReactFiberHostConfig'; -import { - NoWork, - isSameOrHigherPriority, - isSameExpirationTime, -} from './ReactFiberExpirationTime.new'; // Work in progress version numbers only apply to a single render, // and should be reset before starting a new render. @@ -30,44 +23,6 @@ if (__DEV__) { rendererSigil = {}; } -export function clearPendingUpdates( - root: FiberRoot, - expirationTime: ExpirationTimeOpaque, -): void { - if ( - isSameOrHigherPriority( - root.mutableSourceLastPendingUpdateTime_opaque, - expirationTime, - ) - ) { - // All updates for this source have been processed. - root.mutableSourceLastPendingUpdateTime_opaque = NoWork; - } -} - -export function getLastPendingExpirationTime( - root: FiberRoot, -): ExpirationTimeOpaque { - return root.mutableSourceLastPendingUpdateTime_opaque; -} - -export function setPendingExpirationTime( - root: FiberRoot, - expirationTime: ExpirationTimeOpaque, -): void { - const mutableSourceLastPendingUpdateTime = - root.mutableSourceLastPendingUpdateTime_opaque; - if ( - isSameExpirationTime( - mutableSourceLastPendingUpdateTime, - (NoWork: ExpirationTimeOpaque), - ) || - !isSameOrHigherPriority(expirationTime, mutableSourceLastPendingUpdateTime) - ) { - root.mutableSourceLastPendingUpdateTime_opaque = expirationTime; - } -} - export function markSourceAsDirty(mutableSource: MutableSource): void { if (isPrimaryRenderer) { workInProgressPrimarySources.push(mutableSource); diff --git a/packages/react-reconciler/src/ReactUpdateQueue.new.js b/packages/react-reconciler/src/ReactUpdateQueue.new.js index 695dcfecda1d..948be27071d2 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.new.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.new.js @@ -85,14 +85,10 @@ // resources, but the final state is always the same. import type {Fiber} from './ReactInternalTypes'; -import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new'; +import type {Lanes, Lane} from './ReactFiberLane'; import type {SuspenseConfig} from './ReactFiberSuspenseConfig'; -import { - NoWork, - Sync, - isSameOrHigherPriority, -} from './ReactFiberExpirationTime.new'; +import {NoLane, NoLanes, isSubsetOfLanes, mergeLanes} from './ReactFiberLane'; import { enterDisallowedContextReadInDEV, exitDisallowedContextReadInDEV, @@ -104,7 +100,7 @@ import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags import {StrictMode} from './ReactTypeOfMode'; import { markRenderEventTimeAndConfig, - markUnprocessedUpdateTime, + markSkippedUpdateLanes, } from './ReactFiberWorkLoop.new'; import invariant from 'shared/invariant'; @@ -115,7 +111,7 @@ export type Update = {| // TODO: Temporary field. Will remove this by storing a map of // transition -> event time on the root. eventTime: number, - expirationTime: ExpirationTimeOpaque, + lane: Lane, suspenseConfig: null | SuspenseConfig, tag: 0 | 1 | 2 | 3, @@ -192,12 +188,12 @@ export function cloneUpdateQueue( export function createUpdate( eventTime: number, - expirationTime: ExpirationTimeOpaque, + lane: Lane, suspenseConfig: null | SuspenseConfig, ): Update<*> { const update: Update<*> = { eventTime, - expirationTime, + lane, suspenseConfig, tag: UpdateState, @@ -272,7 +268,7 @@ export function enqueueCapturedUpdate( do { const clone: Update = { eventTime: update.eventTime, - expirationTime: update.expirationTime, + lane: update.lane, suspenseConfig: update.suspenseConfig, tag: update.tag, @@ -410,7 +406,7 @@ export function processUpdateQueue( workInProgress: Fiber, props: any, instance: any, - renderExpirationTime: ExpirationTimeOpaque, + renderLanes: Lanes, ): void { // This is always non-null on a ClassComponent or HostRoot const queue: UpdateQueue = (workInProgress.updateQueue: any); @@ -467,7 +463,9 @@ export function processUpdateQueue( if (firstBaseUpdate !== null) { // Iterate through the list of updates to compute the result. let newState = queue.baseState; - let newExpirationTime = NoWork; + // TODO: Don't need to accumulate this. Instead, we can remove renderLanes + // from the original lanes. + let newLanes = NoLanes; let newBaseState = null; let newFirstBaseUpdate = null; @@ -475,15 +473,15 @@ export function processUpdateQueue( let update = firstBaseUpdate; do { + const updateLane = update.lane; const updateEventTime = update.eventTime; - const updateExpirationTime = update.expirationTime; - if (!isSameOrHigherPriority(updateExpirationTime, renderExpirationTime)) { + if (!isSubsetOfLanes(renderLanes, updateLane)) { // Priority is insufficient. Skip this update. If this is the first // skipped update, the previous update/state is the new base // update/state. const clone: Update = { eventTime: updateEventTime, - expirationTime: updateExpirationTime, + lane: updateLane, suspenseConfig: update.suspenseConfig, tag: update.tag, @@ -499,16 +497,17 @@ export function processUpdateQueue( newLastBaseUpdate = newLastBaseUpdate.next = clone; } // Update the remaining priority in the queue. - if (!isSameOrHigherPriority(newExpirationTime, updateExpirationTime)) { - newExpirationTime = updateExpirationTime; - } + newLanes = mergeLanes(newLanes, updateLane); } else { // This update does have sufficient priority. if (newLastBaseUpdate !== null) { const clone: Update = { eventTime: updateEventTime, - expirationTime: Sync, // This update is going to be committed so we never want uncommit it. + // This update is going to be committed so we never want uncommit + // it. Using NoLane works because 0 is a subset of all bitmasks, so + // this will never be skipped by the check above. + lane: NoLane, suspenseConfig: update.suspenseConfig, tag: update.tag, @@ -583,8 +582,8 @@ export function processUpdateQueue( // dealt with the props. Context in components that specify // shouldComponentUpdate is tricky; but we'll have to account for // that regardless. - markUnprocessedUpdateTime(newExpirationTime); - workInProgress.expirationTime_opaque = newExpirationTime; + markSkippedUpdateLanes(newLanes); + workInProgress.lanes = newLanes; workInProgress.memoizedState = newState; } diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js index 0e6c1d7aefc7..90a327d7345f 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js @@ -170,22 +170,32 @@ describe('ReactIncrementalUpdates', () => { // Now flush the remaining work. Even though e and f were already processed, // they should be processed again, to ensure that the terminal state // is deterministic. - expect(Scheduler).toFlushAndYield([ - 'a', - 'b', - 'c', - - // e, f, and g are in a separate batch from a, b, and c because they - // were scheduled in the middle of a render - 'e', - 'f', - 'g', - - 'd', - 'e', - 'f', - 'g', - ]); + expect(Scheduler).toFlushAndYield( + gate(flags => + flags.new + ? ['a', 'b', 'c', 'd', 'e', 'f', 'g'] + : [ + 'a', + 'b', + 'c', + + // The old reconciler has a quirk where `d` has slightly lower + // priority than `g`, because it was scheduled in the middle of a + // render. This is an implementation detail, but I've left the + // test in this branch as-is since this was written so long ago. + // This first render does not include d. + 'e', + 'f', + 'g', + + // This second render does. + 'd', + 'e', + 'f', + 'g', + ], + ), + ); expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]); }); @@ -244,22 +254,32 @@ describe('ReactIncrementalUpdates', () => { // Now flush the remaining work. Even though e and f were already processed, // they should be processed again, to ensure that the terminal state // is deterministic. - expect(Scheduler).toFlushAndYield([ - 'a', - 'b', - 'c', - - // e, f, and g are in a separate batch from a, b, and c because they - // were scheduled in the middle of a render - 'e', - 'f', - 'g', - - 'd', - 'e', - 'f', - 'g', - ]); + expect(Scheduler).toFlushAndYield( + gate(flags => + flags.new + ? ['a', 'b', 'c', 'd', 'e', 'f', 'g'] + : [ + 'a', + 'b', + 'c', + + // The old reconciler has a quirk where `d` has slightly lower + // priority than `g`, because it was scheduled in the middle of a + // render. This is an implementation detail, but I've left the + // test in this branch as-is since this was written so long ago. + // This first render does not include d. + 'e', + 'f', + 'g', + + // This second render does. + 'd', + 'e', + 'f', + 'g', + ], + ), + ); expect(ReactNoop.getChildren()).toEqual([span('fg')]); }); @@ -348,7 +368,7 @@ describe('ReactIncrementalUpdates', () => { expect(Scheduler).toHaveYielded(['componentWillReceiveProps', 'render']); }); - it('enqueues setState inside an updater function as if the in-progress update is progressed (and warns)', () => { + it('updates triggered from inside a class setState updater', () => { let instance; class Foo extends React.Component { state = {}; @@ -372,12 +392,26 @@ describe('ReactIncrementalUpdates', () => { }); expect(() => - expect(Scheduler).toFlushAndYield([ - 'setState updater', - // Update b is enqueued with the same priority as update a, so it should - // be flushed in the same commit. - 'render', - ]), + expect(Scheduler).toFlushAndYield( + gate(flags => + flags.new + ? [ + 'setState updater', + // In the new reconciler, updates inside the render phase are + // treated as if they came from an event, so the update gets + // shifted to a subsequent render. + 'render', + 'render', + ] + : [ + 'setState updater', + // In the old reconciler, updates in the render phase receive + // the currently rendering expiration time, so the update + // flushes immediately in the same render. + 'render', + ], + ), + ), ).toErrorDev( 'An update (setState, replaceState, or forceUpdate) was scheduled ' + 'from inside an update function. Update functions should be pure, ' + @@ -391,7 +425,19 @@ describe('ReactIncrementalUpdates', () => { this.setState({a: 'a'}); return {b: 'b'}; }); - expect(Scheduler).toFlushAndYield(['render']); + expect(Scheduler).toFlushAndYield( + gate(flags => + flags.new + ? // In the new reconciler, updates inside the render phase are + // treated as if they came from an event, so the update gets shifted + // to a subsequent render. + ['render', 'render'] + : // In the old reconciler, updates in the render phase receive + // the currently rendering expiration time, so the update flushes + // immediately in the same render. + ['render'], + ), + ); }); it('getDerivedStateFromProps should update base state of updateQueue (based on product bug)', () => { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index 85e5d4b2b58c..89f9c08e8976 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -322,6 +322,7 @@ describe('ReactSuspense', () => { }, ); + // @gate experimental it( 'interrupts current render when something suspends with a ' + "delay and we've already skipped over a lower priority update in " + @@ -360,9 +361,14 @@ describe('ReactSuspense', () => { // Do a bit of work expect(Scheduler).toFlushAndYieldThrough(['A1']); - // Schedule another update. This will get bumped into a different batch - // because we're already in the middle of rendering. - root.update(); + // Schedule another update. This will have lower priority because it's + // a transition. + React.unstable_withSuspenseConfig( + () => { + root.update(); + }, + {timeoutMs: 10000}, + ); // Interrupt to trigger a restart. interrupt(); @@ -389,6 +395,7 @@ describe('ReactSuspense', () => { }, ); + // @gate experimental it( 'interrupts current render when something suspends with a ' + "delay and we've already bailed out lower priority update in " + @@ -450,16 +457,20 @@ describe('ReactSuspense', () => { setShouldSuspend(true); // Need to move into the next async bucket. - Scheduler.unstable_advanceTime(1000); // Do a bit of work, then interrupt to trigger a restart. expect(Scheduler).toFlushAndYieldThrough(['A']); interrupt(); // Should not have committed loading state expect(root).toMatchRenderedOutput('ABC'); - // Schedule another update. This will have lower priority because of - // the interrupt trick above. - setShouldHideInParent(true); + // Schedule another update. This will have lower priority because it's + // a transition. + React.unstable_withSuspenseConfig( + () => { + setShouldHideInParent(true); + }, + {timeoutMs: 10000}, + ); expect(Scheduler).toFlushAndYieldThrough([ // Should have restarted the first update, because of the interruption diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.internal.js index c28fe61618a4..2db8f7ecbf73 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.internal.js @@ -31,23 +31,20 @@ describe('ReactSuspense', () => { function createThenable() { let completed = false; - const resolveRef = {current: null}; - const promise = { - then(resolve, reject) { - resolveRef.current = () => { - completed = true; - resolve(); - }; - }, - }; - + let resolve; + const promise = new Promise(res => { + resolve = () => { + completed = true; + res(); + }; + }); const PromiseComp = () => { if (!completed) { throw promise; } return 'Done'; }; - return {promise, resolveRef, PromiseComp}; + return {promise, resolve, PromiseComp}; } it('check type', () => { @@ -74,8 +71,8 @@ describe('ReactSuspense', () => { expect(() => Scheduler.unstable_flushAll()).toErrorDev([]); }); - it('1 then 0 suspense callback', () => { - const {promise, resolveRef, PromiseComp} = createThenable(); + it('1 then 0 suspense callback', async () => { + const {promise, resolve, PromiseComp} = createThenable(); let ops = []; const suspenseCallback = thenables => { @@ -94,21 +91,21 @@ describe('ReactSuspense', () => { expect(ops).toEqual([new Set([promise])]); ops = []; - resolveRef.current(); + await resolve(); expect(Scheduler).toFlushWithoutYielding(); expect(ReactNoop.getChildren()).toEqual([text('Done')]); expect(ops).toEqual([]); }); - it('2 then 1 then 0 suspense callback', () => { + it('2 then 1 then 0 suspense callback', async () => { const { promise: promise1, - resolveRef: resolveRef1, + resolve: resolve1, PromiseComp: PromiseComp1, } = createThenable(); const { promise: promise2, - resolveRef: resolveRef2, + resolve: resolve2, PromiseComp: PromiseComp2, } = createThenable(); @@ -132,14 +129,14 @@ describe('ReactSuspense', () => { expect(ops).toEqual([new Set([promise1, promise2])]); ops = []; - resolveRef1.current(); + await resolve1(); ReactNoop.render(element); expect(Scheduler).toFlushWithoutYielding(); expect(ReactNoop.getChildren()).toEqual([text('Waiting Tier 1')]); expect(ops).toEqual([new Set([promise2])]); ops = []; - resolveRef2.current(); + await resolve2(); ReactNoop.render(element); expect(Scheduler).toFlushWithoutYielding(); expect(ReactNoop.getChildren()).toEqual([text('Done'), text('Done')]); @@ -177,15 +174,15 @@ describe('ReactSuspense', () => { expect(ops2).toEqual([new Set([promise])]); }); - it('competing suspense promises', () => { + it('competing suspense promises', async () => { const { promise: promise1, - resolveRef: resolveRef1, + resolve: resolve1, PromiseComp: PromiseComp1, } = createThenable(); const { promise: promise2, - resolveRef: resolveRef2, + resolve: resolve2, PromiseComp: PromiseComp2, } = createThenable(); @@ -219,7 +216,7 @@ describe('ReactSuspense', () => { ops1 = []; ops2 = []; - resolveRef1.current(); + await resolve1(); ReactNoop.render(element); expect(Scheduler).toFlushWithoutYielding(); expect(ReactNoop.getChildren()).toEqual([ @@ -231,7 +228,7 @@ describe('ReactSuspense', () => { ops1 = []; ops2 = []; - resolveRef2.current(); + await resolve2(); ReactNoop.render(element); expect(Scheduler).toFlushWithoutYielding(); expect(ReactNoop.getChildren()).toEqual([text('Done'), text('Done')]); diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 6489bd5687bb..3d302e784018 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -354,5 +354,8 @@ "354": "getInspectorDataForViewAtPoint() is not available in production.", "355": "The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.", "356": "Could not read the cache.", - "357": "The current renderer does not support React Scopes. This error is likely caused by a bug in React. Please file an issue." + "357": "The current renderer does not support React Scopes. This error is likely caused by a bug in React. Please file an issue.", + "358": "Invalid update priority: %s. This is a bug in React.", + "359": "Invalid transition priority: %s. This is a bug in React.", + "360": "Invalid lane: %s. This is a bug in React." }