Skip to content

Commit 6175521

Browse files
committed
Fork completeUnitOfWork into unwind and complete
A mini-refactor to split completeUnitOfWork into two functions: completeUnitOfWork and unwindUnitOfWork. The existing function is already almost complete forked. I think splitting them up makes sense because it makes it easier to specialize the behavior. My practical motivation is that I'm going to change the "unwind" phase to synchronously unwind to the nearest Suspense/error boundary. This means we'll no longer prerender the siblings of a suspended tree. I'll address this in a subsequent step.
1 parent cd20376 commit 6175521

File tree

1 file changed

+125
-75
lines changed

1 file changed

+125
-75
lines changed

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 125 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2085,10 +2085,10 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
20852085
break outer;
20862086
}
20872087
default: {
2088-
// Continue with the normal work loop.
2088+
// Unwind then continue with the normal work loop.
20892089
workInProgressSuspendedReason = NotSuspended;
20902090
workInProgressThrownValue = null;
2091-
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2091+
throwAndUnwindWorkLoop(unitOfWork, thrownValue);
20922092
break;
20932093
}
20942094
}
@@ -2197,7 +2197,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
21972197
// Unwind then continue with the normal work loop.
21982198
workInProgressSuspendedReason = NotSuspended;
21992199
workInProgressThrownValue = null;
2200-
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2200+
throwAndUnwindWorkLoop(unitOfWork, thrownValue);
22012201
break;
22022202
}
22032203
case SuspendedOnData: {
@@ -2250,7 +2250,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
22502250
// Otherwise, unwind then continue with the normal work loop.
22512251
workInProgressSuspendedReason = NotSuspended;
22522252
workInProgressThrownValue = null;
2253-
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2253+
throwAndUnwindWorkLoop(unitOfWork, thrownValue);
22542254
}
22552255
break;
22562256
}
@@ -2261,7 +2261,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
22612261
// always unwind.
22622262
workInProgressSuspendedReason = NotSuspended;
22632263
workInProgressThrownValue = null;
2264-
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2264+
throwAndUnwindWorkLoop(unitOfWork, thrownValue);
22652265
break;
22662266
}
22672267
case SuspendedOnHydration: {
@@ -2461,7 +2461,7 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
24612461
ReactCurrentOwner.current = null;
24622462
}
24632463

2464-
function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) {
2464+
function throwAndUnwindWorkLoop(unitOfWork: Fiber, thrownValue: mixed) {
24652465
// This is a fork of performUnitOfWork specifcally for unwinding a fiber
24662466
// that threw an exception.
24672467
//
@@ -2506,90 +2506,59 @@ function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) {
25062506
throw error;
25072507
}
25082508

2509-
// Return to the normal work loop.
2510-
completeUnitOfWork(unitOfWork);
2509+
if (unitOfWork.flags & Incomplete) {
2510+
// Unwind the stack until we reach the nearest boundary.
2511+
unwindUnitOfWork(unitOfWork);
2512+
} else {
2513+
// Although the fiber suspended, we're intentionally going to commit it in
2514+
// an inconsistent state. We can do this safely in cases where we know the
2515+
// inconsistent tree will be hidden.
2516+
//
2517+
// This currently only applies to Legacy Suspense implementation, but we may
2518+
// port a version of this to concurrent roots, too, when performing a
2519+
// synchronous render. Because that will allow us to mutate the tree as we
2520+
// go instead of buffering mutations until the end. Though it's unclear if
2521+
// this particular path is how that would be implemented.
2522+
completeUnitOfWork(unitOfWork);
2523+
}
25112524
}
25122525

25132526
function completeUnitOfWork(unitOfWork: Fiber): void {
25142527
// Attempt to complete the current unit of work, then move to the next
25152528
// sibling. If there are no more siblings, return to the parent fiber.
25162529
let completedWork: Fiber = unitOfWork;
25172530
do {
2531+
if ((completedWork.flags & Incomplete) !== NoFlags) {
2532+
// This fiber did not complete, because one of its children did not
2533+
// complete. Switch to unwinding the stack instead of completing it.
2534+
//
2535+
// The reason "unwind" and "complete" is interleaved
2536+
unwindUnitOfWork(completedWork);
2537+
return;
2538+
}
2539+
25182540
// The current, flushed, state of this fiber is the alternate. Ideally
25192541
// nothing should rely on this, but relying on it here means that we don't
25202542
// need an additional field on the work in progress.
25212543
const current = completedWork.alternate;
25222544
const returnFiber = completedWork.return;
25232545

2524-
// Check if the work completed or if something threw.
2525-
if ((completedWork.flags & Incomplete) === NoFlags) {
2526-
setCurrentDebugFiberInDEV(completedWork);
2527-
let next;
2528-
if (
2529-
!enableProfilerTimer ||
2530-
(completedWork.mode & ProfileMode) === NoMode
2531-
) {
2532-
next = completeWork(current, completedWork, renderLanes);
2533-
} else {
2534-
startProfilerTimer(completedWork);
2535-
next = completeWork(current, completedWork, renderLanes);
2536-
// Update render duration assuming we didn't error.
2537-
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
2538-
}
2539-
resetCurrentDebugFiberInDEV();
2540-
2541-
if (next !== null) {
2542-
// Completing this fiber spawned new work. Work on that next.
2543-
workInProgress = next;
2544-
return;
2545-
}
2546+
setCurrentDebugFiberInDEV(completedWork);
2547+
let next;
2548+
if (!enableProfilerTimer || (completedWork.mode & ProfileMode) === NoMode) {
2549+
next = completeWork(current, completedWork, renderLanes);
25462550
} else {
2547-
// This fiber did not complete because something threw. Pop values off
2548-
// the stack without entering the complete phase. If this is a boundary,
2549-
// capture values if possible.
2550-
const next = unwindWork(current, completedWork, renderLanes);
2551-
2552-
// Because this fiber did not complete, don't reset its lanes.
2553-
2554-
if (next !== null) {
2555-
// If completing this work spawned new work, do that next. We'll come
2556-
// back here again.
2557-
// Since we're restarting, remove anything that is not a host effect
2558-
// from the effect tag.
2559-
next.flags &= HostEffectMask;
2560-
workInProgress = next;
2561-
return;
2562-
}
2563-
2564-
if (
2565-
enableProfilerTimer &&
2566-
(completedWork.mode & ProfileMode) !== NoMode
2567-
) {
2568-
// Record the render duration for the fiber that errored.
2569-
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
2570-
2571-
// Include the time spent working on failed children before continuing.
2572-
let actualDuration = completedWork.actualDuration;
2573-
let child = completedWork.child;
2574-
while (child !== null) {
2575-
// $FlowFixMe[unsafe-addition] addition with possible null/undefined value
2576-
actualDuration += child.actualDuration;
2577-
child = child.sibling;
2578-
}
2579-
completedWork.actualDuration = actualDuration;
2580-
}
2551+
startProfilerTimer(completedWork);
2552+
next = completeWork(current, completedWork, renderLanes);
2553+
// Update render duration assuming we didn't error.
2554+
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
2555+
}
2556+
resetCurrentDebugFiberInDEV();
25812557

2582-
if (returnFiber !== null) {
2583-
// Mark the parent fiber as incomplete and clear its subtree flags.
2584-
returnFiber.flags |= Incomplete;
2585-
returnFiber.subtreeFlags = NoFlags;
2586-
returnFiber.deletions = null;
2587-
} else {
2588-
// We've unwound all the way to the root.
2589-
workInProgressRootExitStatus = RootDidNotComplete;
2590-
workInProgress = null;
2591-
return;
2592-
}
2558+
if (next !== null) {
2559+
// Completing this fiber spawned new work. Work on that next.
2560+
workInProgress = next;
2561+
return;
25932562
}
25942563

25952564
const siblingFiber = completedWork.sibling;
@@ -2611,6 +2580,87 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
26112580
}
26122581
}
26132582

2583+
function unwindUnitOfWork(unitOfWork: Fiber): void {
2584+
let incompleteWork: Fiber = unitOfWork;
2585+
do {
2586+
// The current, flushed, state of this fiber is the alternate. Ideally
2587+
// nothing should rely on this, but relying on it here means that we don't
2588+
// need an additional field on the work in progress.
2589+
const current = incompleteWork.alternate;
2590+
2591+
// This fiber did not complete because something threw. Pop values off
2592+
// the stack without entering the complete phase. If this is a boundary,
2593+
// capture values if possible.
2594+
const next = unwindWork(current, incompleteWork, renderLanes);
2595+
2596+
// Because this fiber did not complete, don't reset its lanes.
2597+
2598+
if (next !== null) {
2599+
// Found a boundary that can handle this exception. Re-renter the
2600+
// begin phase. This branch will return us to the normal work loop.
2601+
//
2602+
// Since we're restarting, remove anything that is not a host effect
2603+
// from the effect tag.
2604+
next.flags &= HostEffectMask;
2605+
workInProgress = next;
2606+
return;
2607+
}
2608+
2609+
// Keep unwinding until we reach either a boundary or the root.
2610+
2611+
if (enableProfilerTimer && (incompleteWork.mode & ProfileMode) !== NoMode) {
2612+
// Record the render duration for the fiber that errored.
2613+
stopProfilerTimerIfRunningAndRecordDelta(incompleteWork, false);
2614+
2615+
// Include the time spent working on failed children before continuing.
2616+
let actualDuration = incompleteWork.actualDuration;
2617+
let child = incompleteWork.child;
2618+
while (child !== null) {
2619+
// $FlowFixMe[unsafe-addition] addition with possible null/undefined value
2620+
actualDuration += child.actualDuration;
2621+
child = child.sibling;
2622+
}
2623+
incompleteWork.actualDuration = actualDuration;
2624+
}
2625+
2626+
// TODO: Once we stop prerendering siblings, instead of resetting the parent
2627+
// of the node being unwound, we should be able to reset node itself as we
2628+
// unwind the stack. Saves an additional null check.
2629+
const returnFiber = incompleteWork.return;
2630+
if (returnFiber !== null) {
2631+
// Mark the parent fiber as incomplete and clear its subtree flags.
2632+
// TODO: Once we stop prerendering siblings, we may be able to get rid of
2633+
// the Incomplete flag because unwinding to the nearest boundary will
2634+
// happen synchronously.
2635+
returnFiber.flags |= Incomplete;
2636+
returnFiber.subtreeFlags = NoFlags;
2637+
returnFiber.deletions = null;
2638+
}
2639+
2640+
// If there are siblings, work on them now even though they're going to be
2641+
// replaced by a fallback. We're "prerendering" them. Historically our
2642+
// rationale for this behavior has been to initiate any lazy data requests
2643+
// in the siblings, and also to warm up the CPU cache.
2644+
// TODO: Don't prerender siblings. With `use`, we suspend the work loop
2645+
// until the data has resolved, anyway.
2646+
const siblingFiber = incompleteWork.sibling;
2647+
if (siblingFiber !== null) {
2648+
// This branch will return us to the normal work loop.
2649+
workInProgress = siblingFiber;
2650+
return;
2651+
}
2652+
// Otherwise, return to the parent
2653+
// $FlowFixMe[incompatible-type] we bail out when we get a null
2654+
incompleteWork = returnFiber;
2655+
// Update the next thing we're working on in case something throws.
2656+
workInProgress = incompleteWork;
2657+
} while (incompleteWork !== null);
2658+
2659+
// We've unwound all the way to the root.
2660+
workInProgressRootExitStatus = RootDidNotComplete;
2661+
workInProgress = null;
2662+
}
2663+
26142664
function commitRoot(
26152665
root: FiberRoot,
26162666
recoverableErrors: null | Array<CapturedValue<mixed>>,

0 commit comments

Comments
 (0)