diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 0bad69e81aa23..2dc18e603415b 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -2085,10 +2085,10 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) { break outer; } default: { - // Continue with the normal work loop. + // Unwind then continue with the normal work loop. workInProgressSuspendedReason = NotSuspended; workInProgressThrownValue = null; - unwindSuspendedUnitOfWork(unitOfWork, thrownValue); + throwAndUnwindWorkLoop(unitOfWork, thrownValue); break; } } @@ -2197,7 +2197,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // Unwind then continue with the normal work loop. workInProgressSuspendedReason = NotSuspended; workInProgressThrownValue = null; - unwindSuspendedUnitOfWork(unitOfWork, thrownValue); + throwAndUnwindWorkLoop(unitOfWork, thrownValue); break; } case SuspendedOnData: { @@ -2250,7 +2250,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // Otherwise, unwind then continue with the normal work loop. workInProgressSuspendedReason = NotSuspended; workInProgressThrownValue = null; - unwindSuspendedUnitOfWork(unitOfWork, thrownValue); + throwAndUnwindWorkLoop(unitOfWork, thrownValue); } break; } @@ -2261,7 +2261,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // always unwind. workInProgressSuspendedReason = NotSuspended; workInProgressThrownValue = null; - unwindSuspendedUnitOfWork(unitOfWork, thrownValue); + throwAndUnwindWorkLoop(unitOfWork, thrownValue); break; } case SuspendedOnHydration: { @@ -2461,7 +2461,7 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void { ReactCurrentOwner.current = null; } -function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) { +function throwAndUnwindWorkLoop(unitOfWork: Fiber, thrownValue: mixed) { // This is a fork of performUnitOfWork specifcally for unwinding a fiber // that threw an exception. // @@ -2506,8 +2506,21 @@ function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) { throw error; } - // Return to the normal work loop. - completeUnitOfWork(unitOfWork); + if (unitOfWork.flags & Incomplete) { + // Unwind the stack until we reach the nearest boundary. + unwindUnitOfWork(unitOfWork); + } else { + // Although the fiber suspended, we're intentionally going to commit it in + // an inconsistent state. We can do this safely in cases where we know the + // inconsistent tree will be hidden. + // + // This currently only applies to Legacy Suspense implementation, but we may + // port a version of this to concurrent roots, too, when performing a + // synchronous render. Because that will allow us to mutate the tree as we + // go instead of buffering mutations until the end. Though it's unclear if + // this particular path is how that would be implemented. + completeUnitOfWork(unitOfWork); + } } function completeUnitOfWork(unitOfWork: Fiber): void { @@ -2515,81 +2528,37 @@ function completeUnitOfWork(unitOfWork: Fiber): void { // sibling. If there are no more siblings, return to the parent fiber. let completedWork: Fiber = unitOfWork; do { + if ((completedWork.flags & Incomplete) !== NoFlags) { + // This fiber did not complete, because one of its children did not + // complete. Switch to unwinding the stack instead of completing it. + // + // The reason "unwind" and "complete" is interleaved + unwindUnitOfWork(completedWork); + return; + } + // The current, flushed, state of this fiber is the alternate. Ideally // nothing should rely on this, but relying on it here means that we don't // need an additional field on the work in progress. const current = completedWork.alternate; const returnFiber = completedWork.return; - // Check if the work completed or if something threw. - if ((completedWork.flags & Incomplete) === NoFlags) { - setCurrentDebugFiberInDEV(completedWork); - let next; - if ( - !enableProfilerTimer || - (completedWork.mode & ProfileMode) === NoMode - ) { - next = completeWork(current, completedWork, renderLanes); - } else { - startProfilerTimer(completedWork); - next = completeWork(current, completedWork, renderLanes); - // Update render duration assuming we didn't error. - stopProfilerTimerIfRunningAndRecordDelta(completedWork, false); - } - resetCurrentDebugFiberInDEV(); - - if (next !== null) { - // Completing this fiber spawned new work. Work on that next. - workInProgress = next; - return; - } + setCurrentDebugFiberInDEV(completedWork); + let next; + if (!enableProfilerTimer || (completedWork.mode & ProfileMode) === NoMode) { + next = completeWork(current, completedWork, renderLanes); } else { - // 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(current, completedWork, renderLanes); - - // Because this fiber did not complete, don't reset its lanes. - - if (next !== null) { - // If completing this work spawned new work, do that next. We'll come - // back here again. - // Since we're restarting, remove anything that is not a host effect - // from the effect tag. - next.flags &= HostEffectMask; - workInProgress = next; - return; - } - - if ( - enableProfilerTimer && - (completedWork.mode & ProfileMode) !== NoMode - ) { - // Record the render duration for the fiber that errored. - stopProfilerTimerIfRunningAndRecordDelta(completedWork, false); - - // Include the time spent working on failed children before continuing. - let actualDuration = completedWork.actualDuration; - let child = completedWork.child; - while (child !== null) { - // $FlowFixMe[unsafe-addition] addition with possible null/undefined value - actualDuration += child.actualDuration; - child = child.sibling; - } - completedWork.actualDuration = actualDuration; - } + startProfilerTimer(completedWork); + next = completeWork(current, completedWork, renderLanes); + // Update render duration assuming we didn't error. + stopProfilerTimerIfRunningAndRecordDelta(completedWork, false); + } + resetCurrentDebugFiberInDEV(); - if (returnFiber !== null) { - // Mark the parent fiber as incomplete and clear its subtree flags. - returnFiber.flags |= Incomplete; - returnFiber.subtreeFlags = NoFlags; - returnFiber.deletions = null; - } else { - // We've unwound all the way to the root. - workInProgressRootExitStatus = RootDidNotComplete; - workInProgress = null; - return; - } + if (next !== null) { + // Completing this fiber spawned new work. Work on that next. + workInProgress = next; + return; } const siblingFiber = completedWork.sibling; @@ -2611,6 +2580,87 @@ function completeUnitOfWork(unitOfWork: Fiber): void { } } +function unwindUnitOfWork(unitOfWork: Fiber): void { + let incompleteWork: Fiber = unitOfWork; + do { + // The current, flushed, state of this fiber is the alternate. Ideally + // nothing should rely on this, but relying on it here means that we don't + // need an additional field on the work in progress. + const current = incompleteWork.alternate; + + // 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(current, incompleteWork, renderLanes); + + // Because this fiber did not complete, don't reset its lanes. + + if (next !== null) { + // Found a boundary that can handle this exception. Re-renter the + // begin phase. This branch will return us to the normal work loop. + // + // Since we're restarting, remove anything that is not a host effect + // from the effect tag. + next.flags &= HostEffectMask; + workInProgress = next; + return; + } + + // Keep unwinding until we reach either a boundary or the root. + + if (enableProfilerTimer && (incompleteWork.mode & ProfileMode) !== NoMode) { + // Record the render duration for the fiber that errored. + stopProfilerTimerIfRunningAndRecordDelta(incompleteWork, false); + + // Include the time spent working on failed children before continuing. + let actualDuration = incompleteWork.actualDuration; + let child = incompleteWork.child; + while (child !== null) { + // $FlowFixMe[unsafe-addition] addition with possible null/undefined value + actualDuration += child.actualDuration; + child = child.sibling; + } + incompleteWork.actualDuration = actualDuration; + } + + // TODO: Once we stop prerendering siblings, instead of resetting the parent + // of the node being unwound, we should be able to reset node itself as we + // unwind the stack. Saves an additional null check. + const returnFiber = incompleteWork.return; + if (returnFiber !== null) { + // Mark the parent fiber as incomplete and clear its subtree flags. + // TODO: Once we stop prerendering siblings, we may be able to get rid of + // the Incomplete flag because unwinding to the nearest boundary will + // happen synchronously. + returnFiber.flags |= Incomplete; + returnFiber.subtreeFlags = NoFlags; + returnFiber.deletions = null; + } + + // If there are siblings, work on them now even though they're going to be + // replaced by a fallback. We're "prerendering" them. Historically our + // rationale for this behavior has been to initiate any lazy data requests + // in the siblings, and also to warm up the CPU cache. + // TODO: Don't prerender siblings. With `use`, we suspend the work loop + // until the data has resolved, anyway. + const siblingFiber = incompleteWork.sibling; + if (siblingFiber !== null) { + // This branch will return us to the normal work loop. + workInProgress = siblingFiber; + return; + } + // Otherwise, return to the parent + // $FlowFixMe[incompatible-type] we bail out when we get a null + incompleteWork = returnFiber; + // Update the next thing we're working on in case something throws. + workInProgress = incompleteWork; + } while (incompleteWork !== null); + + // We've unwound all the way to the root. + workInProgressRootExitStatus = RootDidNotComplete; + workInProgress = null; +} + function commitRoot( root: FiberRoot, recoverableErrors: null | Array>,