Skip to content

Commit 952dfff

Browse files
committedOct 29, 2022
Split suspended work loop logic into separate functions
Refactors the logic for handling when the work loop is suspended into separate functions for replaying versus unwinding. This allows us to hoist certain checks into the caller. For example, when rendering due to flushSync, we will always unwind the stack without yielding the microtasks. No intentional behavior change in this commit.
1 parent d2c0ab1 commit 952dfff

File tree

2 files changed

+242
-168
lines changed

2 files changed

+242
-168
lines changed
 

‎packages/react-reconciler/src/ReactFiberWorkLoop.new.js

+121-84
Original file line numberDiff line numberDiff line change
@@ -1936,6 +1936,42 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
19361936

19371937
do {
19381938
try {
1939+
if (
1940+
workInProgressSuspendedReason !== NotSuspended &&
1941+
workInProgress !== null
1942+
) {
1943+
// The work loop is suspended. We need to either unwind the stack or
1944+
// replay the suspended component.
1945+
const unitOfWork = workInProgress;
1946+
const thrownValue = workInProgressThrownValue;
1947+
workInProgressSuspendedReason = NotSuspended;
1948+
workInProgressThrownValue = null;
1949+
1950+
// TODO: This check is only here to account for thenables that
1951+
// synchronously resolve. Otherwise we would always unwind when
1952+
// rendering with renderRootSync. (In the future, discrete updates will
1953+
// use renderRootConcurrent instead.) We should account for
1954+
// synchronously resolved thenables before hitting this path.
1955+
switch (workInProgressSuspendedReason) {
1956+
case SuspendedOnError: {
1957+
// Unwind then continue with the normal work loop.
1958+
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
1959+
break;
1960+
}
1961+
default: {
1962+
const wasPinged =
1963+
workInProgressSuspendedThenableState !== null &&
1964+
isThenableStateResolved(workInProgressSuspendedThenableState);
1965+
if (wasPinged) {
1966+
replaySuspendedUnitOfWork(unitOfWork, thrownValue);
1967+
} else {
1968+
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
1969+
}
1970+
// Continue with the normal work loop.
1971+
break;
1972+
}
1973+
}
1974+
}
19391975
workLoopSync();
19401976
break;
19411977
} catch (thrownValue) {
@@ -1980,18 +2016,6 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
19802016
/** @noinline */
19812017
function workLoopSync() {
19822018
// Perform work without checking if we need to yield between fiber.
1983-
1984-
if (workInProgressSuspendedReason !== NotSuspended) {
1985-
// The current work-in-progress was already attempted. We need to unwind
1986-
// it before we continue the normal work loop.
1987-
const thrownValue = workInProgressThrownValue;
1988-
workInProgressSuspendedReason = NotSuspended;
1989-
workInProgressThrownValue = null;
1990-
if (workInProgress !== null) {
1991-
resumeSuspendedUnitOfWork(workInProgress, thrownValue);
1992-
}
1993-
}
1994-
19952019
while (workInProgress !== null) {
19962020
performUnitOfWork(workInProgress);
19972021
}
@@ -2039,6 +2063,36 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
20392063

20402064
do {
20412065
try {
2066+
if (
2067+
workInProgressSuspendedReason !== NotSuspended &&
2068+
workInProgress !== null
2069+
) {
2070+
// The work loop is suspended. We need to either unwind the stack or
2071+
// replay the suspended component.
2072+
const unitOfWork = workInProgress;
2073+
const thrownValue = workInProgressThrownValue;
2074+
workInProgressSuspendedReason = NotSuspended;
2075+
workInProgressThrownValue = null;
2076+
switch (workInProgressSuspendedReason) {
2077+
case SuspendedOnError: {
2078+
// Unwind then continue with the normal work loop.
2079+
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2080+
break;
2081+
}
2082+
default: {
2083+
const wasPinged =
2084+
workInProgressSuspendedThenableState !== null &&
2085+
isThenableStateResolved(workInProgressSuspendedThenableState);
2086+
if (wasPinged) {
2087+
replaySuspendedUnitOfWork(unitOfWork, thrownValue);
2088+
} else {
2089+
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2090+
}
2091+
// Continue with the normal work loop.
2092+
break;
2093+
}
2094+
}
2095+
}
20422096
workLoopConcurrent();
20432097
break;
20442098
} catch (thrownValue) {
@@ -2091,18 +2145,6 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
20912145
/** @noinline */
20922146
function workLoopConcurrent() {
20932147
// Perform work until Scheduler asks us to yield
2094-
2095-
if (workInProgressSuspendedReason !== NotSuspended) {
2096-
// The current work-in-progress was already attempted. We need to unwind
2097-
// it before we continue the normal work loop.
2098-
const thrownValue = workInProgressThrownValue;
2099-
workInProgressSuspendedReason = NotSuspended;
2100-
workInProgressThrownValue = null;
2101-
if (workInProgress !== null) {
2102-
resumeSuspendedUnitOfWork(workInProgress, thrownValue);
2103-
}
2104-
}
2105-
21062148
while (workInProgress !== null && !shouldYield()) {
21072149
// $FlowFixMe[incompatible-call] found when upgrading Flow
21082150
performUnitOfWork(workInProgress);
@@ -2137,69 +2179,15 @@ function performUnitOfWork(unitOfWork: Fiber): void {
21372179
ReactCurrentOwner.current = null;
21382180
}
21392181

2140-
function resumeSuspendedUnitOfWork(
2182+
function replaySuspendedUnitOfWork(
21412183
unitOfWork: Fiber,
21422184
thrownValue: mixed,
21432185
): void {
2144-
// This is a fork of performUnitOfWork specifcally for resuming a fiber that
2145-
// just suspended. In some cases, we may choose to retry the fiber immediately
2146-
// instead of unwinding the stack. It's a separate function to keep the
2147-
// additional logic out of the work loop's hot path.
2148-
2149-
const wasPinged =
2150-
workInProgressSuspendedThenableState !== null &&
2151-
isThenableStateResolved(workInProgressSuspendedThenableState);
2152-
2153-
if (!wasPinged) {
2154-
// The thenable wasn't pinged. Return to the normal work loop. This will
2155-
// unwind the stack, and potentially result in showing a fallback.
2156-
workInProgressSuspendedThenableState = null;
2157-
2158-
const returnFiber = unitOfWork.return;
2159-
if (returnFiber === null || workInProgressRoot === null) {
2160-
// Expected to be working on a non-root fiber. This is a fatal error
2161-
// because there's no ancestor that can handle it; the root is
2162-
// supposed to capture all errors that weren't caught by an error
2163-
// boundary.
2164-
workInProgressRootExitStatus = RootFatalErrored;
2165-
workInProgressRootFatalError = thrownValue;
2166-
// Set `workInProgress` to null. This represents advancing to the next
2167-
// sibling, or the parent if there are no siblings. But since the root
2168-
// has no siblings nor a parent, we set it to null. Usually this is
2169-
// handled by `completeUnitOfWork` or `unwindWork`, but since we're
2170-
// intentionally not calling those, we need set it here.
2171-
// TODO: Consider calling `unwindWork` to pop the contexts.
2172-
workInProgress = null;
2173-
return;
2174-
}
2175-
2176-
try {
2177-
// Find and mark the nearest Suspense or error boundary that can handle
2178-
// this "exception".
2179-
throwException(
2180-
workInProgressRoot,
2181-
returnFiber,
2182-
unitOfWork,
2183-
thrownValue,
2184-
workInProgressRootRenderLanes,
2185-
);
2186-
} catch (error) {
2187-
// We had trouble processing the error. An example of this happening is
2188-
// when accessing the `componentDidCatch` property of an error boundary
2189-
// throws an error. A weird edge case. There's a regression test for this.
2190-
// To prevent an infinite loop, bubble the error up to the next parent.
2191-
workInProgress = returnFiber;
2192-
throw error;
2193-
}
2194-
2195-
// Return to the normal work loop.
2196-
completeUnitOfWork(unitOfWork);
2197-
return;
2198-
}
2199-
2200-
// The work-in-progress was immediately pinged. Instead of unwinding the
2201-
// stack and potentially showing a fallback, unwind only the last stack frame,
2202-
// reset the fiber, and try rendering it again.
2186+
// This is a fork of performUnitOfWork specifcally for replaying a fiber that
2187+
// just suspended.
2188+
//
2189+
// Instead of unwinding the stack and potentially showing a fallback, unwind
2190+
// only the last stack frame, reset the fiber, and try rendering it again.
22032191
const current = unitOfWork.alternate;
22042192
unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
22052193
unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
@@ -2232,6 +2220,55 @@ function resumeSuspendedUnitOfWork(
22322220
ReactCurrentOwner.current = null;
22332221
}
22342222

2223+
function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) {
2224+
// This is a fork of performUnitOfWork specifcally for unwinding a fiber
2225+
// that threw an exception.
2226+
//
2227+
// Return to the normal work loop. This will unwind the stack, and potentially
2228+
// result in showing a fallback.
2229+
workInProgressSuspendedThenableState = null;
2230+
2231+
const returnFiber = unitOfWork.return;
2232+
if (returnFiber === null || workInProgressRoot === null) {
2233+
// Expected to be working on a non-root fiber. This is a fatal error
2234+
// because there's no ancestor that can handle it; the root is
2235+
// supposed to capture all errors that weren't caught by an error
2236+
// boundary.
2237+
workInProgressRootExitStatus = RootFatalErrored;
2238+
workInProgressRootFatalError = thrownValue;
2239+
// Set `workInProgress` to null. This represents advancing to the next
2240+
// sibling, or the parent if there are no siblings. But since the root
2241+
// has no siblings nor a parent, we set it to null. Usually this is
2242+
// handled by `completeUnitOfWork` or `unwindWork`, but since we're
2243+
// intentionally not calling those, we need set it here.
2244+
// TODO: Consider calling `unwindWork` to pop the contexts.
2245+
workInProgress = null;
2246+
return;
2247+
}
2248+
2249+
try {
2250+
// Find and mark the nearest Suspense or error boundary that can handle
2251+
// this "exception".
2252+
throwException(
2253+
workInProgressRoot,
2254+
returnFiber,
2255+
unitOfWork,
2256+
thrownValue,
2257+
workInProgressRootRenderLanes,
2258+
);
2259+
} catch (error) {
2260+
// We had trouble processing the error. An example of this happening is
2261+
// when accessing the `componentDidCatch` property of an error boundary
2262+
// throws an error. A weird edge case. There's a regression test for this.
2263+
// To prevent an infinite loop, bubble the error up to the next parent.
2264+
workInProgress = returnFiber;
2265+
throw error;
2266+
}
2267+
2268+
// Return to the normal work loop.
2269+
completeUnitOfWork(unitOfWork);
2270+
}
2271+
22352272
export function getSuspendedThenableState(): ThenableState | null {
22362273
return workInProgressSuspendedThenableState;
22372274
}

‎packages/react-reconciler/src/ReactFiberWorkLoop.old.js

+121-84
Original file line numberDiff line numberDiff line change
@@ -1936,6 +1936,42 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
19361936

19371937
do {
19381938
try {
1939+
if (
1940+
workInProgressSuspendedReason !== NotSuspended &&
1941+
workInProgress !== null
1942+
) {
1943+
// The work loop is suspended. We need to either unwind the stack or
1944+
// replay the suspended component.
1945+
const unitOfWork = workInProgress;
1946+
const thrownValue = workInProgressThrownValue;
1947+
workInProgressSuspendedReason = NotSuspended;
1948+
workInProgressThrownValue = null;
1949+
1950+
// TODO: This check is only here to account for thenables that
1951+
// synchronously resolve. Otherwise we would always unwind when
1952+
// rendering with renderRootSync. (In the future, discrete updates will
1953+
// use renderRootConcurrent instead.) We should account for
1954+
// synchronously resolved thenables before hitting this path.
1955+
switch (workInProgressSuspendedReason) {
1956+
case SuspendedOnError: {
1957+
// Unwind then continue with the normal work loop.
1958+
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
1959+
break;
1960+
}
1961+
default: {
1962+
const wasPinged =
1963+
workInProgressSuspendedThenableState !== null &&
1964+
isThenableStateResolved(workInProgressSuspendedThenableState);
1965+
if (wasPinged) {
1966+
replaySuspendedUnitOfWork(unitOfWork, thrownValue);
1967+
} else {
1968+
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
1969+
}
1970+
// Continue with the normal work loop.
1971+
break;
1972+
}
1973+
}
1974+
}
19391975
workLoopSync();
19401976
break;
19411977
} catch (thrownValue) {
@@ -1980,18 +2016,6 @@ function renderRootSync(root: FiberRoot, lanes: Lanes) {
19802016
/** @noinline */
19812017
function workLoopSync() {
19822018
// Perform work without checking if we need to yield between fiber.
1983-
1984-
if (workInProgressSuspendedReason !== NotSuspended) {
1985-
// The current work-in-progress was already attempted. We need to unwind
1986-
// it before we continue the normal work loop.
1987-
const thrownValue = workInProgressThrownValue;
1988-
workInProgressSuspendedReason = NotSuspended;
1989-
workInProgressThrownValue = null;
1990-
if (workInProgress !== null) {
1991-
resumeSuspendedUnitOfWork(workInProgress, thrownValue);
1992-
}
1993-
}
1994-
19952019
while (workInProgress !== null) {
19962020
performUnitOfWork(workInProgress);
19972021
}
@@ -2039,6 +2063,36 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
20392063

20402064
do {
20412065
try {
2066+
if (
2067+
workInProgressSuspendedReason !== NotSuspended &&
2068+
workInProgress !== null
2069+
) {
2070+
// The work loop is suspended. We need to either unwind the stack or
2071+
// replay the suspended component.
2072+
const unitOfWork = workInProgress;
2073+
const thrownValue = workInProgressThrownValue;
2074+
workInProgressSuspendedReason = NotSuspended;
2075+
workInProgressThrownValue = null;
2076+
switch (workInProgressSuspendedReason) {
2077+
case SuspendedOnError: {
2078+
// Unwind then continue with the normal work loop.
2079+
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2080+
break;
2081+
}
2082+
default: {
2083+
const wasPinged =
2084+
workInProgressSuspendedThenableState !== null &&
2085+
isThenableStateResolved(workInProgressSuspendedThenableState);
2086+
if (wasPinged) {
2087+
replaySuspendedUnitOfWork(unitOfWork, thrownValue);
2088+
} else {
2089+
unwindSuspendedUnitOfWork(unitOfWork, thrownValue);
2090+
}
2091+
// Continue with the normal work loop.
2092+
break;
2093+
}
2094+
}
2095+
}
20422096
workLoopConcurrent();
20432097
break;
20442098
} catch (thrownValue) {
@@ -2091,18 +2145,6 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
20912145
/** @noinline */
20922146
function workLoopConcurrent() {
20932147
// Perform work until Scheduler asks us to yield
2094-
2095-
if (workInProgressSuspendedReason !== NotSuspended) {
2096-
// The current work-in-progress was already attempted. We need to unwind
2097-
// it before we continue the normal work loop.
2098-
const thrownValue = workInProgressThrownValue;
2099-
workInProgressSuspendedReason = NotSuspended;
2100-
workInProgressThrownValue = null;
2101-
if (workInProgress !== null) {
2102-
resumeSuspendedUnitOfWork(workInProgress, thrownValue);
2103-
}
2104-
}
2105-
21062148
while (workInProgress !== null && !shouldYield()) {
21072149
// $FlowFixMe[incompatible-call] found when upgrading Flow
21082150
performUnitOfWork(workInProgress);
@@ -2137,69 +2179,15 @@ function performUnitOfWork(unitOfWork: Fiber): void {
21372179
ReactCurrentOwner.current = null;
21382180
}
21392181

2140-
function resumeSuspendedUnitOfWork(
2182+
function replaySuspendedUnitOfWork(
21412183
unitOfWork: Fiber,
21422184
thrownValue: mixed,
21432185
): void {
2144-
// This is a fork of performUnitOfWork specifcally for resuming a fiber that
2145-
// just suspended. In some cases, we may choose to retry the fiber immediately
2146-
// instead of unwinding the stack. It's a separate function to keep the
2147-
// additional logic out of the work loop's hot path.
2148-
2149-
const wasPinged =
2150-
workInProgressSuspendedThenableState !== null &&
2151-
isThenableStateResolved(workInProgressSuspendedThenableState);
2152-
2153-
if (!wasPinged) {
2154-
// The thenable wasn't pinged. Return to the normal work loop. This will
2155-
// unwind the stack, and potentially result in showing a fallback.
2156-
workInProgressSuspendedThenableState = null;
2157-
2158-
const returnFiber = unitOfWork.return;
2159-
if (returnFiber === null || workInProgressRoot === null) {
2160-
// Expected to be working on a non-root fiber. This is a fatal error
2161-
// because there's no ancestor that can handle it; the root is
2162-
// supposed to capture all errors that weren't caught by an error
2163-
// boundary.
2164-
workInProgressRootExitStatus = RootFatalErrored;
2165-
workInProgressRootFatalError = thrownValue;
2166-
// Set `workInProgress` to null. This represents advancing to the next
2167-
// sibling, or the parent if there are no siblings. But since the root
2168-
// has no siblings nor a parent, we set it to null. Usually this is
2169-
// handled by `completeUnitOfWork` or `unwindWork`, but since we're
2170-
// intentionally not calling those, we need set it here.
2171-
// TODO: Consider calling `unwindWork` to pop the contexts.
2172-
workInProgress = null;
2173-
return;
2174-
}
2175-
2176-
try {
2177-
// Find and mark the nearest Suspense or error boundary that can handle
2178-
// this "exception".
2179-
throwException(
2180-
workInProgressRoot,
2181-
returnFiber,
2182-
unitOfWork,
2183-
thrownValue,
2184-
workInProgressRootRenderLanes,
2185-
);
2186-
} catch (error) {
2187-
// We had trouble processing the error. An example of this happening is
2188-
// when accessing the `componentDidCatch` property of an error boundary
2189-
// throws an error. A weird edge case. There's a regression test for this.
2190-
// To prevent an infinite loop, bubble the error up to the next parent.
2191-
workInProgress = returnFiber;
2192-
throw error;
2193-
}
2194-
2195-
// Return to the normal work loop.
2196-
completeUnitOfWork(unitOfWork);
2197-
return;
2198-
}
2199-
2200-
// The work-in-progress was immediately pinged. Instead of unwinding the
2201-
// stack and potentially showing a fallback, unwind only the last stack frame,
2202-
// reset the fiber, and try rendering it again.
2186+
// This is a fork of performUnitOfWork specifcally for replaying a fiber that
2187+
// just suspended.
2188+
//
2189+
// Instead of unwinding the stack and potentially showing a fallback, unwind
2190+
// only the last stack frame, reset the fiber, and try rendering it again.
22032191
const current = unitOfWork.alternate;
22042192
unwindInterruptedWork(current, unitOfWork, workInProgressRootRenderLanes);
22052193
unitOfWork = workInProgress = resetWorkInProgress(unitOfWork, renderLanes);
@@ -2232,6 +2220,55 @@ function resumeSuspendedUnitOfWork(
22322220
ReactCurrentOwner.current = null;
22332221
}
22342222

2223+
function unwindSuspendedUnitOfWork(unitOfWork: Fiber, thrownValue: mixed) {
2224+
// This is a fork of performUnitOfWork specifcally for unwinding a fiber
2225+
// that threw an exception.
2226+
//
2227+
// Return to the normal work loop. This will unwind the stack, and potentially
2228+
// result in showing a fallback.
2229+
workInProgressSuspendedThenableState = null;
2230+
2231+
const returnFiber = unitOfWork.return;
2232+
if (returnFiber === null || workInProgressRoot === null) {
2233+
// Expected to be working on a non-root fiber. This is a fatal error
2234+
// because there's no ancestor that can handle it; the root is
2235+
// supposed to capture all errors that weren't caught by an error
2236+
// boundary.
2237+
workInProgressRootExitStatus = RootFatalErrored;
2238+
workInProgressRootFatalError = thrownValue;
2239+
// Set `workInProgress` to null. This represents advancing to the next
2240+
// sibling, or the parent if there are no siblings. But since the root
2241+
// has no siblings nor a parent, we set it to null. Usually this is
2242+
// handled by `completeUnitOfWork` or `unwindWork`, but since we're
2243+
// intentionally not calling those, we need set it here.
2244+
// TODO: Consider calling `unwindWork` to pop the contexts.
2245+
workInProgress = null;
2246+
return;
2247+
}
2248+
2249+
try {
2250+
// Find and mark the nearest Suspense or error boundary that can handle
2251+
// this "exception".
2252+
throwException(
2253+
workInProgressRoot,
2254+
returnFiber,
2255+
unitOfWork,
2256+
thrownValue,
2257+
workInProgressRootRenderLanes,
2258+
);
2259+
} catch (error) {
2260+
// We had trouble processing the error. An example of this happening is
2261+
// when accessing the `componentDidCatch` property of an error boundary
2262+
// throws an error. A weird edge case. There's a regression test for this.
2263+
// To prevent an infinite loop, bubble the error up to the next parent.
2264+
workInProgress = returnFiber;
2265+
throw error;
2266+
}
2267+
2268+
// Return to the normal work loop.
2269+
completeUnitOfWork(unitOfWork);
2270+
}
2271+
22352272
export function getSuspendedThenableState(): ThenableState | null {
22362273
return workInProgressSuspendedThenableState;
22372274
}

0 commit comments

Comments
 (0)
Please sign in to comment.