diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 65af5f42d6fa..de614eb92fbd 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -662,7 +662,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { '- ' + // need to explicitly coerce Symbol to a string (fiber.type ? fiber.type.name || fiber.type.toString() : '[root]'), - '[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']', + '[' + + fiber.childExpirationTime + + (fiber.pendingProps ? '*' : '') + + ']', ); if (fiber.updateQueue) { logUpdateQueue(fiber.updateQueue, depth); diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 06f4f7a4c81e..a39f5f3328f5 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -149,9 +149,12 @@ export type Fiber = {| lastEffect: Fiber | null, // Represents a time in the future by which this work should be completed. - // This is also used to quickly determine if a subtree has no pending changes. + // Does not include work found in its subtree. expirationTime: ExpirationTime, + // This is used to quickly determine if a subtree has no pending changes. + childExpirationTime: ExpirationTime, + // This is a pooled version of a Fiber. Every fiber that gets updated will // eventually have a pair. There are cases when we can clean up pairs to save // memory if we need to. @@ -229,6 +232,7 @@ function FiberNode( this.lastEffect = null; this.expirationTime = NoWork; + this.childExpirationTime = NoWork; this.alternate = null; @@ -330,7 +334,15 @@ export function createWorkInProgress( } } - workInProgress.expirationTime = expirationTime; + // Don't touching the subtree's expiration time, which has not changed. + workInProgress.childExpirationTime = current.childExpirationTime; + if (pendingProps !== current.pendingProps) { + // This fiber has new props. + workInProgress.expirationTime = expirationTime; + } else { + // This fiber's props have not changed. + workInProgress.expirationTime = current.expirationTime; + } workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; @@ -572,6 +584,7 @@ export function assignFiberPropertiesInDEV( target.firstEffect = source.firstEffect; target.lastEffect = source.lastEffect; target.expirationTime = source.expirationTime; + target.childExpirationTime = source.childExpirationTime; target.alternate = source.alternate; if (enableProfilerTimer) { target.actualDuration = source.actualDuration; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 1db331860b30..220ad227e4ac 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -112,17 +112,7 @@ if (__DEV__) { didWarnAboutStatelessRefs = {}; } -// TODO: Remove this and use reconcileChildrenAtExpirationTime directly. -function reconcileChildren(current, workInProgress, nextChildren) { - reconcileChildrenAtExpirationTime( - current, - workInProgress, - nextChildren, - workInProgress.expirationTime, - ); -} - -export function reconcileChildrenAtExpirationTime( +export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, @@ -155,7 +145,7 @@ export function reconcileChildrenAtExpirationTime( } } -function updateForwardRef(current, workInProgress) { +function updateForwardRef(current, workInProgress, renderExpirationTime) { const render = workInProgress.type.render; const nextProps = workInProgress.pendingProps; const ref = workInProgress.ref; @@ -165,7 +155,11 @@ function updateForwardRef(current, workInProgress) { } else if (workInProgress.memoizedProps === nextProps) { const currentRef = current !== null ? current.ref : null; if (ref === currentRef) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); } } @@ -179,50 +173,52 @@ function updateForwardRef(current, workInProgress) { nextChildren = render(nextProps, ref); } - reconcileChildren(current, workInProgress, nextChildren); + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); memoizeProps(workInProgress, nextProps); return workInProgress.child; } -function updateFragment(current, workInProgress) { +function updateFragment(current, workInProgress, renderExpirationTime) { const nextChildren = workInProgress.pendingProps; - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (workInProgress.memoizedProps === nextChildren) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - reconcileChildren(current, workInProgress, nextChildren); + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); memoizeProps(workInProgress, nextChildren); return workInProgress.child; } -function updateMode(current, workInProgress) { +function updateMode(current, workInProgress, renderExpirationTime) { const nextChildren = workInProgress.pendingProps.children; - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if ( - nextChildren === null || - workInProgress.memoizedProps === nextChildren - ) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - reconcileChildren(current, workInProgress, nextChildren); + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); memoizeProps(workInProgress, nextChildren); return workInProgress.child; } -function updateProfiler(current, workInProgress) { - const nextProps = workInProgress.pendingProps; +function updateProfiler(current, workInProgress, renderExpirationTime) { if (enableProfilerTimer) { workInProgress.effectTag |= Update; } - if (workInProgress.memoizedProps === nextProps) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } + const nextProps = workInProgress.pendingProps; const nextChildren = nextProps.children; - reconcileChildren(current, workInProgress, nextChildren); + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); memoizeProps(workInProgress, nextProps); return workInProgress.child; } @@ -245,25 +241,11 @@ function updateFunctionalComponent( ) { const fn = workInProgress.type; const nextProps = workInProgress.pendingProps; - - const hasPendingContext = prepareToReadContext( - workInProgress, - renderExpirationTime, - ); - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (workInProgress.memoizedProps === nextProps && !hasPendingContext) { - // TODO: consider bringing fn.shouldComponentUpdate() back. - // It used to be here. - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - const unmaskedContext = getUnmaskedContext(workInProgress); const context = getMaskedContext(workInProgress, unmaskedContext); let nextChildren; - + prepareToReadContext(workInProgress, renderExpirationTime); if (__DEV__) { ReactCurrentOwner.current = workInProgress; ReactCurrentFiber.setCurrentPhase('render'); @@ -275,7 +257,12 @@ function updateFunctionalComponent( // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; - reconcileChildren(current, workInProgress, nextChildren); + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); memoizeProps(workInProgress, nextProps); return workInProgress.child; } @@ -349,7 +336,11 @@ function finishClassComponent( invalidateContextProvider(workInProgress, false); } - return bailoutOnAlreadyFinishedWork(current, workInProgress); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); } const ctor = workInProgress.type; @@ -395,18 +386,13 @@ function finishClassComponent( if (current !== null && didCaptureError) { // If we're recovering from an error, reconcile twice: first to delete // all the existing children. - reconcileChildrenAtExpirationTime( - current, - workInProgress, - null, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, null, renderExpirationTime); workInProgress.child = null; // Now we can continue reconciling like normal. This has the effect of // remounting all children regardless of whether their their // identity matches. } - reconcileChildrenAtExpirationTime( + reconcileChildren( current, workInProgress, nextChildren, @@ -442,66 +428,75 @@ function pushHostRootContext(workInProgress) { function updateHostRoot(current, workInProgress, renderExpirationTime) { pushHostRootContext(workInProgress); - let updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - const nextProps = workInProgress.pendingProps; - const prevState = workInProgress.memoizedState; - const prevChildren = prevState !== null ? prevState.element : null; - processUpdateQueue( + const updateQueue = workInProgress.updateQueue; + invariant( + updateQueue !== null, + 'If the root does not have an updateQueue, we should have already ' + + 'bailed out. This error is likely caused by a bug in React. Please ' + + 'file an issue.', + ); + const nextProps = workInProgress.pendingProps; + const prevState = workInProgress.memoizedState; + const prevChildren = prevState !== null ? prevState.element : null; + processUpdateQueue( + workInProgress, + updateQueue, + nextProps, + null, + renderExpirationTime, + ); + 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, + ); + } + const root: FiberRoot = workInProgress.stateNode; + if ( + (current === null || current.child === null) && + root.hydrate && + enterHydrationState(workInProgress) + ) { + // If we don't have any current children this might be the first pass. + // We always try to hydrate. If this isn't a hydration pass there won't + // be any children to hydrate which is effectively the same thing as + // not hydrating. + + // This is a bit of a hack. We track the host root as a placement to + // know that we're currently in a mounting state. That way isMounted + // works as expected. We must reset this before committing. + // TODO: Delete this when we delete isMounted and findDOMNode. + workInProgress.effectTag |= Placement; + + // Ensure that children mount into this root without tracking + // side-effects. This ensures that we don't store Placement effects on + // nodes that will be hydrated. + workInProgress.child = mountChildFibers( workInProgress, - updateQueue, - nextProps, null, + nextChildren, renderExpirationTime, ); - 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); - } - const root: FiberRoot = workInProgress.stateNode; - if ( - (current === null || current.child === null) && - root.hydrate && - enterHydrationState(workInProgress) - ) { - // If we don't have any current children this might be the first pass. - // We always try to hydrate. If this isn't a hydration pass there won't - // be any children to hydrate which is effectively the same thing as - // not hydrating. - - // This is a bit of a hack. We track the host root as a placement to - // know that we're currently in a mounting state. That way isMounted - // works as expected. We must reset this before committing. - // TODO: Delete this when we delete isMounted and findDOMNode. - workInProgress.effectTag |= Placement; - - // Ensure that children mount into this root without tracking - // side-effects. This ensures that we don't store Placement effects on - // nodes that will be hydrated. - workInProgress.child = mountChildFibers( - workInProgress, - null, - nextChildren, - renderExpirationTime, - ); - } else { - // Otherwise reset hydration state in case we aborted and resumed another - // root. - resetHydrationState(); - reconcileChildren(current, workInProgress, nextChildren); - } - return workInProgress.child; + } else { + // Otherwise reset hydration state in case we aborted and resumed another + // root. + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); + resetHydrationState(); } - resetHydrationState(); - // If there is no update queue, that's a bailout because the root has no props. - return bailoutOnAlreadyFinishedWork(current, workInProgress); + return workInProgress.child; } function updateHostComponent(current, workInProgress, renderExpirationTime) { @@ -512,28 +507,9 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) { } const type = workInProgress.type; - const memoizedProps = workInProgress.memoizedProps; const nextProps = workInProgress.pendingProps; const prevProps = current !== null ? current.memoizedProps : null; - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (memoizedProps === nextProps) { - const isHidden = - workInProgress.mode & AsyncMode && - shouldDeprioritizeSubtree(type, nextProps); - if (isHidden) { - // Before bailing out, make sure we've deprioritized a hidden component. - workInProgress.expirationTime = Never; - } - if (!isHidden || renderExpirationTime !== Never) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - // If we're rendering a hidden node at hidden priority, don't bailout. The - // parent is complete, but the children may not be. - } - let nextChildren = nextProps.children; const isDirectTextChild = shouldSetTextContent(type, nextProps); @@ -543,7 +519,7 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) { // this in the host environment that also have access to this prop. That // avoids allocating another HostText fiber and traversing it. nextChildren = null; - } else if (prevProps && shouldSetTextContent(type, prevProps)) { + } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) { // If we're switching from a direct text child to a normal child, or to // empty, we need to schedule the text content to be reset. workInProgress.effectTag |= ContentReset; @@ -557,14 +533,18 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) { workInProgress.mode & AsyncMode && shouldDeprioritizeSubtree(type, nextProps) ) { - // Down-prioritize the children. + // Schedule this fiber to re-render at offscreen priority. Then bailout. workInProgress.expirationTime = Never; - // Bailout and come back to this fiber later. workInProgress.memoizedProps = nextProps; return null; } - reconcileChildren(current, workInProgress, nextChildren); + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); memoizeProps(workInProgress, nextProps); return workInProgress.child; } @@ -714,7 +694,7 @@ function mountIndeterminateComponent( } } } - reconcileChildren(current, workInProgress, value); + reconcileChildren(current, workInProgress, value, renderExpirationTime); memoizeProps(workInProgress, props); return workInProgress.child; } @@ -727,9 +707,6 @@ function updatePlaceholderComponent( ) { if (enableSuspense) { const nextProps = workInProgress.pendingProps; - const prevProps = workInProgress.memoizedProps; - - const prevDidTimeout = workInProgress.memoizedState === true; // Check if we already attempted to render the normal state. If we did, // and we timed out, render the placeholder state. @@ -744,12 +721,7 @@ function updatePlaceholderComponent( nextDidTimeout = true; // If we're recovering from an error, reconcile twice: first to delete // all the existing children. - reconcileChildrenAtExpirationTime( - current, - workInProgress, - null, - renderExpirationTime, - ); + reconcileChildren(current, workInProgress, null, renderExpirationTime); current.child = null; // Now we can continue reconciling like normal. This has the effect of // remounting all children regardless of whether their their @@ -758,13 +730,6 @@ function updatePlaceholderComponent( nextDidTimeout = !alreadyCaptured; } - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (nextProps === prevProps && nextDidTimeout === prevDidTimeout) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - if ((workInProgress.mode & StrictMode) !== NoEffect) { if (nextDidTimeout) { // If the timed-out view commits, schedule an update effect to record @@ -789,7 +754,12 @@ function updatePlaceholderComponent( workInProgress.memoizedProps = nextProps; workInProgress.memoizedState = nextDidTimeout; - reconcileChildren(current, workInProgress, nextChildren); + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); return workInProgress.child; } else { return null; @@ -799,13 +769,6 @@ function updatePlaceholderComponent( function updatePortalComponent(current, workInProgress, renderExpirationTime) { pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); const nextChildren = workInProgress.pendingProps; - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (workInProgress.memoizedProps === nextChildren) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - if (current === null) { // Portals are special because we don't append the children during mount // but at commit. Therefore we need to track insertions which the normal @@ -820,7 +783,12 @@ function updatePortalComponent(current, workInProgress, renderExpirationTime) { ); memoizeProps(workInProgress, nextChildren); } else { - reconcileChildren(current, workInProgress, nextChildren); + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); memoizeProps(workInProgress, nextChildren); } return workInProgress.child; @@ -832,17 +800,6 @@ function updateContextProvider(current, workInProgress, renderExpirationTime) { const newProps = workInProgress.pendingProps; const oldProps = workInProgress.memoizedProps; - let canBailOnProps = true; - - if (hasLegacyContextChanged()) { - canBailOnProps = false; - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (oldProps === newProps) { - workInProgress.stateNode = 0; - pushProvider(workInProgress); - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } const newValue = newProps.value; workInProgress.memoizedProps = newProps; @@ -868,10 +825,17 @@ function updateContextProvider(current, workInProgress, renderExpirationTime) { } else { if (oldProps.value === newProps.value) { // No change. Bailout early if children are the same. - if (oldProps.children === newProps.children && canBailOnProps) { + if ( + oldProps.children === newProps.children && + !hasLegacyContextChanged() + ) { workInProgress.stateNode = 0; pushProvider(workInProgress); - return bailoutOnAlreadyFinishedWork(current, workInProgress); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); } changedBits = 0; } else { @@ -885,10 +849,17 @@ function updateContextProvider(current, workInProgress, renderExpirationTime) { (oldValue !== oldValue && newValue !== newValue) // eslint-disable-line no-self-compare ) { // No change. Bailout early if children are the same. - if (oldProps.children === newProps.children && canBailOnProps) { + if ( + oldProps.children === newProps.children && + !hasLegacyContextChanged() + ) { workInProgress.stateNode = 0; pushProvider(workInProgress); - return bailoutOnAlreadyFinishedWork(current, workInProgress); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); } changedBits = 0; } else { @@ -908,10 +879,17 @@ function updateContextProvider(current, workInProgress, renderExpirationTime) { if (changedBits === 0) { // No change. Bailout early if children are the same. - if (oldProps.children === newProps.children && canBailOnProps) { + if ( + oldProps.children === newProps.children && + !hasLegacyContextChanged() + ) { workInProgress.stateNode = 0; pushProvider(workInProgress); - return bailoutOnAlreadyFinishedWork(current, workInProgress); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); } } else { propagateContextChange( @@ -929,28 +907,13 @@ function updateContextProvider(current, workInProgress, renderExpirationTime) { pushProvider(workInProgress); const newChildren = newProps.children; - reconcileChildren(current, workInProgress, newChildren); + reconcileChildren(current, workInProgress, newChildren, renderExpirationTime); return workInProgress.child; } function updateContextConsumer(current, workInProgress, renderExpirationTime) { const context: ReactContext = workInProgress.type; const newProps = workInProgress.pendingProps; - const oldProps = workInProgress.memoizedProps; - - const hasPendingContext = prepareToReadContext( - workInProgress, - renderExpirationTime, - ); - if (hasLegacyContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - } else if (oldProps === newProps && !hasPendingContext) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - - workInProgress.memoizedProps = newProps; - const render = newProps.children; if (__DEV__) { @@ -963,6 +926,7 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) { ); } + prepareToReadContext(workInProgress, renderExpirationTime); const newValue = readContext(context, newProps.unstable_observedBits); let newChildren; if (__DEV__) { @@ -976,7 +940,8 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) { // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; - reconcileChildren(current, workInProgress, newChildren); + reconcileChildren(current, workInProgress, newChildren, renderExpirationTime); + workInProgress.memoizedProps = newProps; return workInProgress.child; } @@ -1000,12 +965,14 @@ function updateContextConsumer(current, workInProgress, renderExpirationTime) { */ function bailoutOnAlreadyFinishedWork( - current, + current: Fiber | null, workInProgress: Fiber, + renderExpirationTime: ExpirationTime, ): Fiber | null { cancelWorkTimer(workInProgress); if (current !== null) { + // Reuse previous context list workInProgress.firstContextDependency = current.firstContextDependency; } @@ -1014,51 +981,22 @@ function bailoutOnAlreadyFinishedWork( stopBaseRenderTimerIfRunning(); } - // TODO: We should ideally be able to bail out early if the children have no - // more work to do. However, since we don't have a separation of this - // Fiber's priority and its children yet - we don't know without doing lots - // of the same work we do anyway. Once we have that separation we can just - // bail out here if the children has no more work at this priority level. - // if (workInProgress.priorityOfChildren <= priorityLevel) { - // // If there are side-effects in these children that have not yet been - // // committed we need to ensure that they get properly transferred up. - // if (current && current.child !== workInProgress.child) { - // reuseChildrenEffects(workInProgress, child); - // } - // return null; - // } - - cloneChildFibers(current, workInProgress); - return workInProgress.child; -} - -function bailoutOnLowPriority(current, workInProgress) { - cancelWorkTimer(workInProgress); - - if (enableProfilerTimer) { - // Don't update "base" render times for bailouts. - stopBaseRenderTimerIfRunning(); - } - - // TODO: Handle HostComponent tags here as well and call pushHostContext()? - // See PR 8590 discussion for context - switch (workInProgress.tag) { - case HostRoot: - pushHostRootContext(workInProgress); - break; - case ClassComponent: - pushLegacyContextProvider(workInProgress); - break; - case HostPortal: - pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); - break; - case ContextProvider: - pushProvider(workInProgress); - break; + // Check if the children have any pending work. + const childExpirationTime = workInProgress.childExpirationTime; + if ( + childExpirationTime === NoWork || + childExpirationTime > renderExpirationTime + ) { + // 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. + return null; + } else { + // This fiber doesn't have work, but its subtree does. Clone the child + // fibers and continue. + cloneChildFibers(current, workInProgress); + return workInProgress.child; } - // TODO: What if this is currently in progress? - // How can that happen? How is this not being cloned? - return null; } // TODO: Delete memoizeProps/State and move to reconcile/bailout instead @@ -1083,13 +1021,52 @@ function beginWork( } } + const updateExpirationTime = workInProgress.expirationTime; if ( - workInProgress.expirationTime === NoWork || - workInProgress.expirationTime > renderExpirationTime + !hasLegacyContextChanged() && + (updateExpirationTime === NoWork || + updateExpirationTime > renderExpirationTime) ) { - return bailoutOnLowPriority(current, workInProgress); + // 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 + // in this optimized path, mostly pushing stuff onto the stack. + switch (workInProgress.tag) { + case HostRoot: + pushHostRootContext(workInProgress); + resetHydrationState(); + break; + case HostComponent: + pushHostContext(workInProgress); + break; + case ClassComponent: + pushLegacyContextProvider(workInProgress); + break; + case HostPortal: + pushHostContainer( + workInProgress, + workInProgress.stateNode.containerInfo, + ); + break; + case ContextProvider: + workInProgress.stateNode = 0; + pushProvider(workInProgress); + break; + case Profiler: + if (enableProfilerTimer) { + workInProgress.effectTag |= Update; + } + break; + } + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); } + // Before entering the begin phase, clear the expiration time. + workInProgress.expirationTime = NoWork; + switch (workInProgress.tag) { case IndeterminateComponent: return mountIndeterminateComponent( @@ -1128,13 +1105,13 @@ function beginWork( renderExpirationTime, ); case ForwardRef: - return updateForwardRef(current, workInProgress); + return updateForwardRef(current, workInProgress, renderExpirationTime); case Fragment: - return updateFragment(current, workInProgress); + return updateFragment(current, workInProgress, renderExpirationTime); case Mode: - return updateMode(current, workInProgress); + return updateMode(current, workInProgress, renderExpirationTime); case Profiler: - return updateProfiler(current, workInProgress); + return updateProfiler(current, workInProgress, renderExpirationTime); case ContextProvider: return updateContextProvider( current, diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 6ef80abb42af..cc2103fe22f8 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -162,7 +162,7 @@ export function applyDerivedStateFromProps( // Once the update queue is empty, persist the derived state onto the // base state. const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null && updateQueue.expirationTime === NoWork) { + if (updateQueue !== null && workInProgress.expirationTime === NoWork) { updateQueue.baseState = memoizedState; } } @@ -183,7 +183,7 @@ const classComponentUpdater = { update.callback = callback; } - enqueueUpdate(fiber, update, expirationTime); + enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); }, enqueueReplaceState(inst, payload, callback) { @@ -202,7 +202,7 @@ const classComponentUpdater = { update.callback = callback; } - enqueueUpdate(fiber, update, expirationTime); + enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); }, enqueueForceUpdate(inst, callback) { @@ -220,7 +220,7 @@ const classComponentUpdater = { update.callback = callback; } - enqueueUpdate(fiber, update, expirationTime); + enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); }, }; diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 92035588440e..39d51c642ce0 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -118,29 +118,44 @@ export function propagateContextChange( dependency.context === context && (dependency.observedBits & changedBits) !== 0 ) { - // Match! Update the expiration time of all the ancestors, including + // Match! Schedule an update on this fiber. + if ( + fiber.expirationTime === NoWork || + fiber.expirationTime > renderExpirationTime + ) { + fiber.expirationTime = renderExpirationTime; + } + let alternate = fiber.alternate; + if ( + alternate !== null && + (alternate.expirationTime === NoWork || + alternate.expirationTime > renderExpirationTime) + ) { + alternate.expirationTime = renderExpirationTime; + } + // Update the child expiration time of all the ancestors, including // the alternates. - let node = fiber; + let node = fiber.return; while (node !== null) { - const alternate = node.alternate; + alternate = node.alternate; if ( - node.expirationTime === NoWork || - node.expirationTime > renderExpirationTime + node.childExpirationTime === NoWork || + node.childExpirationTime > renderExpirationTime ) { - node.expirationTime = renderExpirationTime; + node.childExpirationTime = renderExpirationTime; if ( alternate !== null && - (alternate.expirationTime === NoWork || - alternate.expirationTime > renderExpirationTime) + (alternate.childExpirationTime === NoWork || + alternate.childExpirationTime > renderExpirationTime) ) { - alternate.expirationTime = renderExpirationTime; + alternate.childExpirationTime = renderExpirationTime; } } else if ( alternate !== null && - (alternate.expirationTime === NoWork || - alternate.expirationTime > renderExpirationTime) + (alternate.childExpirationTime === NoWork || + alternate.childExpirationTime > renderExpirationTime) ) { - alternate.expirationTime = renderExpirationTime; + alternate.childExpirationTime = renderExpirationTime; } else { // Neither alternate was updated, which means the rest of the // ancestor path already has sufficient priority. diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index ce9e54c97237..140b75e3aa70 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -135,7 +135,7 @@ function scheduleRootUpdate( ); update.callback = callback; } - enqueueUpdate(current, update, expirationTime); + enqueueUpdate(current, update); scheduleWork(current, expirationTime); return expirationTime; diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 52e16db4c4d4..8cf634d5aaea 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -533,8 +533,15 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void { // Update the pending priority levels to account for the work that we are // about to commit. This needs to happen before calling the lifecycles, since // they may schedule additional updates. - const earliestRemainingTime = finishedWork.expirationTime; - markCommittedPriorityLevels(root, earliestRemainingTime); + const updateExpirationTimeBeforeCommit = finishedWork.expirationTime; + const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime; + const earliestRemainingTimeBeforeCommit = + updateExpirationTimeBeforeCommit === NoWork || + (childExpirationTimeBeforeCommit !== NoWork && + childExpirationTimeBeforeCommit < updateExpirationTimeBeforeCommit) + ? childExpirationTimeBeforeCommit + : updateExpirationTimeBeforeCommit; + markCommittedPriorityLevels(root, earliestRemainingTimeBeforeCommit); // Reset this to null before calling lifecycles ReactCurrentOwner.current = null; @@ -708,71 +715,85 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void { ReactFiberInstrumentation.debugTool.onCommitWork(finishedWork); } - const expirationTime = root.expirationTime; - if (expirationTime === NoWork) { + const updateExpirationTimeAfterCommit = finishedWork.expirationTime; + const childExpirationTimeAfterCommit = finishedWork.childExpirationTime; + const earliestRemainingTimeAfterCommit = + updateExpirationTimeAfterCommit === NoWork || + (childExpirationTimeAfterCommit !== NoWork && + childExpirationTimeAfterCommit < updateExpirationTimeAfterCommit) + ? childExpirationTimeAfterCommit + : updateExpirationTimeAfterCommit; + if (earliestRemainingTimeAfterCommit === NoWork) { // If there's no remaining work, we can clear the set of already failed // error boundaries. legacyErrorBoundariesThatAlreadyFailed = null; } - onCommit(root, expirationTime); + onCommit(root, earliestRemainingTimeAfterCommit); } -function resetExpirationTime( +function resetChildExpirationTime( workInProgress: Fiber, renderTime: ExpirationTime, ) { - if (renderTime !== Never && workInProgress.expirationTime === Never) { + if (renderTime !== Never && workInProgress.childExpirationTime === Never) { // The children of this component are hidden. Don't bubble their // expiration times. return; } - // Check for pending updates. - let newExpirationTime = NoWork; - switch (workInProgress.tag) { - case HostRoot: - case ClassComponent: { - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - newExpirationTime = updateQueue.expirationTime; - } - } - } - - // TODO: Calls need to visit stateNode + let newChildExpirationTime = NoWork; // Bubble up the earliest expiration time. - // (And "base" render timers if that feature flag is enabled) if (enableProfilerTimer && workInProgress.mode & ProfileMode) { + // We're in profiling mode. Let's use this same traversal to update the + // "base" render times. let treeBaseDuration = workInProgress.selfBaseDuration; let child = workInProgress.child; while (child !== null) { - treeBaseDuration += child.treeBaseDuration; + const childUpdateExpirationTime = child.expirationTime; + const childChildExpirationTime = child.childExpirationTime; if ( - child.expirationTime !== NoWork && - (newExpirationTime === NoWork || - newExpirationTime > child.expirationTime) + newChildExpirationTime === NoWork || + (childUpdateExpirationTime !== NoWork && + childUpdateExpirationTime < newChildExpirationTime) ) { - newExpirationTime = child.expirationTime; + newChildExpirationTime = childUpdateExpirationTime; } + if ( + newChildExpirationTime === NoWork || + (childChildExpirationTime !== NoWork && + childChildExpirationTime < newChildExpirationTime) + ) { + newChildExpirationTime = childChildExpirationTime; + } + treeBaseDuration += child.treeBaseDuration; child = child.sibling; } workInProgress.treeBaseDuration = treeBaseDuration; } else { let child = workInProgress.child; while (child !== null) { + const childUpdateExpirationTime = child.expirationTime; + const childChildExpirationTime = child.childExpirationTime; if ( - child.expirationTime !== NoWork && - (newExpirationTime === NoWork || - newExpirationTime > child.expirationTime) + newChildExpirationTime === NoWork || + (childUpdateExpirationTime !== NoWork && + childUpdateExpirationTime < newChildExpirationTime) ) { - newExpirationTime = child.expirationTime; + newChildExpirationTime = childUpdateExpirationTime; + } + if ( + newChildExpirationTime === NoWork || + (childChildExpirationTime !== NoWork && + childChildExpirationTime < newChildExpirationTime) + ) { + newChildExpirationTime = childChildExpirationTime; } child = child.sibling; } } - workInProgress.expirationTime = newExpirationTime; + workInProgress.childExpirationTime = newChildExpirationTime; } function completeUnitOfWork(workInProgress: Fiber): Fiber | null { @@ -800,7 +821,7 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null { nextRenderExpirationTime, ); stopWorkTimer(workInProgress); - resetExpirationTime(workInProgress, nextRenderExpirationTime); + resetChildExpirationTime(workInProgress, nextRenderExpirationTime); if (__DEV__) { ReactCurrentFiber.resetCurrentFiber(); } @@ -1271,7 +1292,7 @@ function dispatch( errorInfo, expirationTime, ); - enqueueUpdate(fiber, update, expirationTime); + enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); return; } @@ -1279,7 +1300,7 @@ function dispatch( case HostRoot: { const errorInfo = createCapturedValue(value, sourceFiber); const update = createRootErrorUpdate(fiber, errorInfo, expirationTime); - enqueueUpdate(fiber, update, expirationTime); + enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); return; } @@ -1293,7 +1314,7 @@ function dispatch( const rootFiber = sourceFiber; const errorInfo = createCapturedValue(value, rootFiber); const update = createRootErrorUpdate(rootFiber, errorInfo, expirationTime); - enqueueUpdate(rootFiber, update, expirationTime); + enqueueUpdate(rootFiber, update); scheduleWork(rootFiber, expirationTime); } } @@ -1412,35 +1433,52 @@ function retrySuspendedRoot( } function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null { - // Walk the parent path to the root and update each node's - // expiration time. - let node = fiber; - do { - const alternate = node.alternate; + // Update the source fiber's expiration time + if ( + fiber.expirationTime === NoWork || + fiber.expirationTime > expirationTime + ) { + fiber.expirationTime = expirationTime; + } + let alternate = fiber.alternate; + if ( + alternate !== null && + (alternate.expirationTime === NoWork || + alternate.expirationTime > expirationTime) + ) { + alternate.expirationTime = expirationTime; + } + // Walk the parent path to the root and update the child expiration time. + let node = fiber.return; + if (node === null && fiber.tag === HostRoot) { + return fiber.stateNode; + } + while (node !== null) { + alternate = node.alternate; if ( - node.expirationTime === NoWork || - node.expirationTime > expirationTime + node.childExpirationTime === NoWork || + node.childExpirationTime > expirationTime ) { - node.expirationTime = expirationTime; + node.childExpirationTime = expirationTime; if ( alternate !== null && - (alternate.expirationTime === NoWork || - alternate.expirationTime > expirationTime) + (alternate.childExpirationTime === NoWork || + alternate.childExpirationTime > expirationTime) ) { - alternate.expirationTime = expirationTime; + alternate.childExpirationTime = expirationTime; } } else if ( alternate !== null && - (alternate.expirationTime === NoWork || - alternate.expirationTime > expirationTime) + (alternate.childExpirationTime === NoWork || + alternate.childExpirationTime > expirationTime) ) { - alternate.expirationTime = expirationTime; + alternate.childExpirationTime = expirationTime; } if (node.return === null && node.tag === HostRoot) { return node.stateNode; } node = node.return; - } while (node !== null); + } return null; } diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 432c30d4423d..8ce94b12ba25 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -70,7 +70,7 @@ import { LOW_PRIORITY_EXPIRATION, } from './ReactFiberExpirationTime'; import {findEarliestOutstandingPriorityLevel} from './ReactFiberPendingPriority'; -import {reconcileChildrenAtExpirationTime} from './ReactFiberBeginWork'; +import {reconcileChildren} from './ReactFiberBeginWork'; function NoopComponent() { return null; @@ -238,7 +238,7 @@ function throwException( // Unmount the source fiber's children const nextChildren = null; - reconcileChildrenAtExpirationTime( + reconcileChildren( sourceFiber.alternate, sourceFiber, nextChildren, @@ -310,6 +310,7 @@ function throwException( renderDidSuspend(root, absoluteTimeoutMs, renderExpirationTime); workInProgress.effectTag |= ShouldCapture; + workInProgress.expirationTime = renderExpirationTime; return; } // This boundary already captured during this render. Continue to the @@ -334,12 +335,13 @@ function throwException( case HostRoot: { const errorInfo = value; workInProgress.effectTag |= ShouldCapture; + workInProgress.expirationTime = renderExpirationTime; const update = createRootErrorUpdate( workInProgress, errorInfo, renderExpirationTime, ); - enqueueCapturedUpdate(workInProgress, update, renderExpirationTime); + enqueueCapturedUpdate(workInProgress, update); return; } case ClassComponent: @@ -356,13 +358,14 @@ function throwException( !isAlreadyFailedLegacyErrorBoundary(instance))) ) { workInProgress.effectTag |= ShouldCapture; + workInProgress.expirationTime = renderExpirationTime; // Schedule the error boundary to re-render using updated state const update = createClassErrorUpdate( workInProgress, errorInfo, renderExpirationTime, ); - enqueueCapturedUpdate(workInProgress, update, renderExpirationTime); + enqueueCapturedUpdate(workInProgress, update); return; } break; diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js index f592d9e54fc5..22e88ae8cc48 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -117,7 +117,6 @@ export type Update = { }; export type UpdateQueue = { - expirationTime: ExpirationTime, baseState: State, firstUpdate: Update | null, @@ -156,7 +155,6 @@ if (__DEV__) { export function createUpdateQueue(baseState: State): UpdateQueue { const queue: UpdateQueue = { - expirationTime: NoWork, baseState, firstUpdate: null, lastUpdate: null, @@ -174,7 +172,6 @@ function cloneUpdateQueue( currentQueue: UpdateQueue, ): UpdateQueue { const queue: UpdateQueue = { - expirationTime: currentQueue.expirationTime, baseState: currentQueue.baseState, firstUpdate: currentQueue.firstUpdate, lastUpdate: currentQueue.lastUpdate, @@ -209,7 +206,6 @@ export function createUpdate(expirationTime: ExpirationTime): Update<*> { function appendUpdateToQueue( queue: UpdateQueue, update: Update, - expirationTime: ExpirationTime, ) { // Append the update to the end of the list. if (queue.lastUpdate === null) { @@ -219,21 +215,9 @@ function appendUpdateToQueue( queue.lastUpdate.next = update; queue.lastUpdate = update; } - if ( - queue.expirationTime === NoWork || - queue.expirationTime > expirationTime - ) { - // The incoming update has the earliest expiration of any update in the - // queue. Update the queue's expiration time. - queue.expirationTime = expirationTime; - } } -export function enqueueUpdate( - fiber: Fiber, - update: Update, - expirationTime: ExpirationTime, -) { +export function enqueueUpdate(fiber: Fiber, update: Update) { // Update queues are created lazily. const alternate = fiber.alternate; let queue1; @@ -271,19 +255,19 @@ export function enqueueUpdate( } if (queue2 === null || queue1 === queue2) { // There's only a single queue. - appendUpdateToQueue(queue1, update, expirationTime); + appendUpdateToQueue(queue1, update); } else { // There are two queues. We need to append the update to both queues, // while accounting for the persistent structure of the list — we don't // want the same update to be added multiple times. if (queue1.lastUpdate === null || queue2.lastUpdate === null) { // One of the queues is not empty. We must add the update to both queues. - appendUpdateToQueue(queue1, update, expirationTime); - appendUpdateToQueue(queue2, update, expirationTime); + appendUpdateToQueue(queue1, update); + appendUpdateToQueue(queue2, update); } else { // Both queues are non-empty. The last update is the same in both lists, // because of structural sharing. So, only append to one of the lists. - appendUpdateToQueue(queue1, update, expirationTime); + appendUpdateToQueue(queue1, update); // But we still need to update the `lastUpdate` pointer of queue2. queue2.lastUpdate = update; } @@ -311,7 +295,6 @@ export function enqueueUpdate( export function enqueueCapturedUpdate( workInProgress: Fiber, update: Update, - renderExpirationTime: ExpirationTime, ) { // Captured updates go into a separate list, and only on the work-in- // progress queue. @@ -338,14 +321,6 @@ export function enqueueCapturedUpdate( workInProgressQueue.lastCapturedUpdate.next = update; workInProgressQueue.lastCapturedUpdate = update; } - if ( - workInProgressQueue.expirationTime === NoWork || - workInProgressQueue.expirationTime > renderExpirationTime - ) { - // The incoming update has the earliest expiration of any update in the - // queue. Update the queue's expiration time. - workInProgressQueue.expirationTime = renderExpirationTime; - } } function ensureWorkInProgressQueueIsAClone( @@ -438,14 +413,6 @@ export function processUpdateQueue( ): void { hasForceUpdate = false; - if ( - queue.expirationTime === NoWork || - queue.expirationTime > renderExpirationTime - ) { - // Insufficient priority. Bailout. - return; - } - queue = ensureWorkInProgressQueueIsAClone(workInProgress, queue); if (__DEV__) { @@ -577,8 +544,15 @@ export function processUpdateQueue( queue.baseState = newBaseState; queue.firstUpdate = newFirstUpdate; queue.firstCapturedUpdate = newFirstCapturedUpdate; - queue.expirationTime = newExpirationTime; + // Set the remaining expiration time to be whatever is remaining in the queue. + // This should be fine because the only two other things that contribute to + // expiration time are props and context. We're already in the middle of the + // begin phase by the time we start processing the queue, so we've already + // dealt with the props. Context in components that specify + // shouldComponentUpdate is tricky; but we'll have to account for + // that regardless. + workInProgress.expirationTime = newExpirationTime; workInProgress.memoizedState = resultState; if (__DEV__) {