From 3d0790a20fc9374525a0c4f84b0cc6175861a7c9 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 22 Apr 2020 00:59:27 -0700 Subject: [PATCH] Initial Lanes implementation --- .../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 +- scripts/error-codes/codes.json | 5 +- 27 files changed, 1935 insertions(+), 2247 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 65e4188dc9c44..c087ec93765c8 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 e993e4d571e8e..5b0e858d6af96 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 741083a8650cc..b428e11374f43 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 576016d2f6945..5a183f1b852f8 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 b568278d452be..20e544b00a056 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 8e71d3a99bef6..91e74031dded8 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 e2280673232ba..e420bb6916c70 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 f77179fdb105c..26e055718cf84 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 d8d3cfd69d5ab..0000000000000 --- 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 818a7e529bff5..64c17405dc3fe 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 2c834382eea92..21819bf9edbc1 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 f1ed2e73cccdb..3f4c83da829ba 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 4e389fe737b80..65d35d7640115 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 3c03586bb5474..ab603a08e5f3d 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 0808c4c193177..58f2360501774 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 0cb5fada23b7c..4b694f7665125 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 ae03056235482..b3fc36d51724a 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 a45f1710138fb..e151ab0be0088 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 a63deaaef1133..190ca9d4730d4 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 64bed32254618..735888f3215d2 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 544d4f72b14a4..0e9b9caaf08ae 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 edca38409314f..2b12735c8c58f 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 bc11adcd33050..be7bb4dfdf594 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 695dcfecda1d1..948be27071d2d 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 0e6c1d7aefc7f..90a327d7345fb 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 85e5d4b2b58c4..89f9c08e89760 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/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 6489bd5687bb1..3d302e784018e 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." }