diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 1b3e274191d35..360b9910262e9 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -1849,141 +1849,6 @@ export function renderHasNotSuspendedYet(): boolean { return workInProgressRootExitStatus === RootInProgress; } -// TODO: Over time, this function and renderRootConcurrent have become more -// and more similar. Not sure it makes sense to maintain forked paths. Consider -// unifying them again. -function renderRootSync(root: FiberRoot, lanes: Lanes) { - const prevExecutionContext = executionContext; - executionContext |= RenderContext; - const prevDispatcher = pushDispatcher(root.containerInfo); - const prevCacheDispatcher = pushCacheDispatcher(); - - // 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 (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) { - if (enableUpdaterTracking) { - if (isDevToolsPresent) { - const memoizedUpdaters = root.memoizedUpdaters; - if (memoizedUpdaters.size > 0) { - restorePendingUpdaters(root, workInProgressRootRenderLanes); - memoizedUpdaters.clear(); - } - - // At this point, move Fibers that scheduled the upcoming work from the Map to the Set. - // If we bailout on this work, we'll move them back (like above). - // It's important to move them now in case the work spawns more work at the same priority with different updaters. - // That way we can keep the current update and future updates separate. - movePendingFibersToMemoized(root, lanes); - } - } - - workInProgressTransitions = getTransitionsForLanes(root, lanes); - prepareFreshStack(root, lanes); - } - - if (__DEV__) { - if (enableDebugTracing) { - logRenderStarted(lanes); - } - } - - if (enableSchedulingProfiler) { - markRenderStarted(lanes); - } - - let didSuspendInShell = false; - outer: do { - try { - if ( - workInProgressSuspendedReason !== NotSuspended && - workInProgress !== null - ) { - // The work loop is suspended. During a synchronous render, we don't - // yield to the main thread. Immediately unwind the stack. This will - // trigger either a fallback or an error boundary. - // TODO: For discrete and "default" updates (anything that's not - // flushSync), we want to wait for the microtasks the flush before - // unwinding. Will probably implement this using renderRootConcurrent, - // or merge renderRootSync and renderRootConcurrent into the same - // function and fork the behavior some other way. - const unitOfWork = workInProgress; - const thrownValue = workInProgressThrownValue; - switch (workInProgressSuspendedReason) { - case SuspendedOnHydration: { - // Selective hydration. An update flowed into a dehydrated tree. - // Interrupt the current render so the work loop can switch to the - // hydration lane. - resetWorkInProgressStack(); - workInProgressRootExitStatus = RootDidNotComplete; - break outer; - } - case SuspendedOnImmediate: - case SuspendedOnData: { - if (!didSuspendInShell && getSuspenseHandler() === null) { - didSuspendInShell = true; - } - // Intentional fallthrough - } - default: { - // Unwind then continue with the normal work loop. - workInProgressSuspendedReason = NotSuspended; - workInProgressThrownValue = null; - throwAndUnwindWorkLoop(unitOfWork, thrownValue); - break; - } - } - } - workLoopSync(); - break; - } catch (thrownValue) { - handleThrow(root, thrownValue); - } - } while (true); - - // Check if something suspended in the shell. We use this to detect an - // infinite ping loop caused by an uncached promise. - // - // Only increment this counter once per synchronous render attempt across the - // whole tree. Even if there are many sibling components that suspend, this - // counter only gets incremented once. - if (didSuspendInShell) { - root.shellSuspendCounter++; - } - - resetContextDependencies(); - - executionContext = prevExecutionContext; - popDispatcher(prevDispatcher); - popCacheDispatcher(prevCacheDispatcher); - - if (workInProgress !== null) { - // This is a sync render, so we should have finished the whole tree. - throw new Error( - 'Cannot commit an incomplete root. This error is likely caused by a ' + - 'bug in React. Please file an issue.', - ); - } - - if (__DEV__) { - if (enableDebugTracing) { - logRenderStopped(); - } - } - - if (enableSchedulingProfiler) { - markRenderStopped(); - } - - // Set this to null to indicate there's no in-progress render. - workInProgressRoot = null; - workInProgressRootRenderLanes = NoLanes; - - // It's safe to process the queue now that the render phase is complete. - finishQueueingConcurrentUpdates(); - - return workInProgressRootExitStatus; -} - // The work loop is an extremely hot path. Tell Closure not to inline it. /** @noinline */ function workLoopSync() { @@ -1993,7 +1858,24 @@ function workLoopSync() { } } +/** @noinline */ +function workLoopConcurrent() { + // Perform work until Scheduler asks us to yield + while (workInProgress !== null && !shouldYield()) { + // $FlowFixMe[incompatible-call] found when upgrading Flow + performUnitOfWork(workInProgress); + } +} + function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { + return renderRoot(root, lanes, true); +} + +function renderRootSync(root: FiberRoot, lanes: Lanes) { + return renderRoot(root, lanes, false); +} + +function renderRoot(root: FiberRoot, lanes: Lanes, isConcurrent: boolean) { const prevExecutionContext = executionContext; executionContext |= RenderContext; const prevDispatcher = pushDispatcher(root.containerInfo); @@ -2019,7 +1901,18 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { } workInProgressTransitions = getTransitionsForLanes(root, lanes); - resetRenderTimer(); + if (isConcurrent) { + resetRenderTimer(); + } else { + // We don't reset the CPU render timer (used by SuspenseList) for + // synchronous renders because the timer should start at the beginning + // of the sequence of work. We don't reset it for sync renders because + // if there's an additional layout render pass, we haven't yet yielded + // for paint. See original PR for more context: https://github.com/facebook/react/pull/19643 + // TODO: This mechanism could be improved. I think we could move all of + // this logic to the ReactFiberRootScheduler module, since that + // controls when we enter/leave the work loop. + } prepareFreshStack(root, lanes); } @@ -2033,6 +1926,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { markRenderStarted(lanes); } + let didSuspendSynchronouslyInShell = false; outer: do { try { if ( @@ -2046,8 +1940,6 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { resumeOrUnwind: switch (workInProgressSuspendedReason) { case SuspendedOnError: { // Unwind then continue with the normal work loop. - workInProgressSuspendedReason = NotSuspended; - workInProgressThrownValue = null; throwAndUnwindWorkLoop(unitOfWork, thrownValue); break; } @@ -2055,57 +1947,81 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { const thenable: Thenable = (thrownValue: any); if (isThenableResolved(thenable)) { // The data resolved. Try rendering the component again. - workInProgressSuspendedReason = NotSuspended; - workInProgressThrownValue = null; replaySuspendedUnitOfWork(unitOfWork); break; } - // The work loop is suspended on data. We should wait for it to - // resolve before continuing to render. - // TODO: Handle the case where the promise resolves synchronously. - // Usually this is handled when we instrument the promise to add a - // `status` field, but if the promise already has a status, we won't - // have added a listener until right here. - const onResolution = () => { - // Check if the root is still suspended on this promise. - if ( - workInProgressSuspendedReason === SuspendedOnData && - workInProgressRoot === root - ) { - // Mark the root as ready to continue rendering. - workInProgressSuspendedReason = SuspendedAndReadyToContinue; - } - // Ensure the root is scheduled. We should do this even if we're - // currently working on a different root, so that we resume - // rendering later. - ensureRootIsScheduled(root); - }; - thenable.then(onResolution, onResolution); - break outer; + + if (isConcurrent) { + // The work loop is suspended on data. We should wait for it to + // resolve before continuing to render. + // TODO: Handle the case where the promise resolves synchronously. + // Usually this is handled when we instrument the promise to add a + // `status` field, but if the promise already has a status, we + // won't have added a listener until right here. + const onResolution = () => { + // Check if the root is still suspended on this promise. + if ( + workInProgressSuspendedReason === SuspendedOnData && + workInProgressRoot === root + ) { + // Mark the root as ready to continue rendering. + workInProgressSuspendedReason = SuspendedAndReadyToContinue; + } + // Ensure the root is scheduled. We should do this even if we're + // currently working on a different root, so that we resume + // rendering later. + ensureRootIsScheduled(root); + }; + thenable.then(onResolution, onResolution); + break outer; + } + + if (getSuspenseHandler() === null) { + didSuspendSynchronouslyInShell = true; + } + + // Unwind then continue with the normal work loop. + throwAndUnwindWorkLoop(unitOfWork, thrownValue); + break; } case SuspendedOnImmediate: { - // If this fiber just suspended, it's possible the data is already - // cached. Yield to the main thread to give it a chance to ping. If - // it does, we can retry immediately without unwinding the stack. - workInProgressSuspendedReason = SuspendedAndReadyToContinue; - break outer; + if (isConcurrent) { + // If this fiber just suspended, it's possible the data is already + // cached. Yield to the main thread to give it a chance to ping. + // If it does, we can retry immediately without unwinding + // the stack. + workInProgressSuspendedReason = SuspendedAndReadyToContinue; + break outer; + } + + if (getSuspenseHandler() === null) { + didSuspendSynchronouslyInShell = true; + } + + // Unwind then continue with the normal work loop. + throwAndUnwindWorkLoop(unitOfWork, thrownValue); + break; } case SuspendedOnInstance: { - workInProgressSuspendedReason = - SuspendedOnInstanceAndReadyToContinue; - break outer; + if (isConcurrent) { + // If this fiber just suspended, it's possible the data is already + // cached. Yield to the main thread to give it a chance to ping. + // If it does, we can retry immediately without unwinding + // the stack. + workInProgressSuspendedReason = + SuspendedOnInstanceAndReadyToContinue; + break outer; + } + throwAndUnwindWorkLoop(unitOfWork, thrownValue); + break; } case SuspendedAndReadyToContinue: { const thenable: Thenable = (thrownValue: any); if (isThenableResolved(thenable)) { // The data resolved. Try rendering the component again. - workInProgressSuspendedReason = NotSuspended; - workInProgressThrownValue = null; replaySuspendedUnitOfWork(unitOfWork); } else { // Otherwise, unwind then continue with the normal work loop. - workInProgressSuspendedReason = NotSuspended; - workInProgressThrownValue = null; throwAndUnwindWorkLoop(unitOfWork, thrownValue); } break; @@ -2161,8 +2077,6 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { } } // Otherwise, unwind then continue with the normal work loop. - workInProgressSuspendedReason = NotSuspended; - workInProgressThrownValue = null; throwAndUnwindWorkLoop(unitOfWork, thrownValue); break; } @@ -2171,8 +2085,6 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // pattern. The newer replaying behavior can cause subtle issues // like infinite ping loops. So we maintain the old behavior and // always unwind. - workInProgressSuspendedReason = NotSuspended; - workInProgressThrownValue = null; throwAndUnwindWorkLoop(unitOfWork, thrownValue); break; } @@ -2180,6 +2092,8 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // Selective hydration. An update flowed into a dehydrated tree. // Interrupt the current render so the work loop can switch to the // hydration lane. + // + // NOTE: This happens even during synchronous renders! resetWorkInProgressStack(); workInProgressRootExitStatus = RootDidNotComplete; break outer; @@ -2192,21 +2106,35 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { } } - if (__DEV__ && ReactCurrentActQueue.current !== null) { + if ( + isConcurrent && // `act` special case: If we're inside an `act` scope, don't consult // `shouldYield`. Always keep working until the render is complete. // This is not just an optimization: in a unit test environment, we // can't trust the result of `shouldYield`, because the host I/O is // likely mocked. - workLoopSync(); - } else { + !(__DEV__ && ReactCurrentActQueue.current !== null) + ) { workLoopConcurrent(); + } else { + workLoopSync(); } break; } catch (thrownValue) { handleThrow(root, thrownValue); } } while (true); + + // Check if something suspended in the shell. We use this to detect an + // infinite ping loop caused by an uncached promise. + // + // Only increment this counter once per synchronous render attempt across the + // whole tree. Even if there are many sibling components that suspend, this + // counter only gets incremented once. + if (didSuspendSynchronouslyInShell) { + root.shellSuspendCounter++; + } + resetContextDependencies(); popDispatcher(prevDispatcher); @@ -2221,11 +2149,19 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // Check if the tree has completed. if (workInProgress !== null) { - // Still work remaining. - if (enableSchedulingProfiler) { - markRenderYielded(); + if (isConcurrent) { + // Still work remaining. + if (enableSchedulingProfiler) { + markRenderYielded(); + } + return RootInProgress; + } else { + // This is a sync render, so we should have finished the whole tree. + throw new Error( + 'Cannot commit an incomplete root. This error is likely caused by a ' + + 'bug in React. Please file an issue.', + ); } - return RootInProgress; } else { // Completed the tree. if (enableSchedulingProfiler) { @@ -2244,15 +2180,6 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { } } -/** @noinline */ -function workLoopConcurrent() { - // Perform work until Scheduler asks us to yield - while (workInProgress !== null && !shouldYield()) { - // $FlowFixMe[incompatible-call] found when upgrading Flow - performUnitOfWork(workInProgress); - } -} - function performUnitOfWork(unitOfWork: Fiber): void { // 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 @@ -2284,7 +2211,8 @@ function performUnitOfWork(unitOfWork: Fiber): void { function replaySuspendedUnitOfWork(unitOfWork: Fiber): void { // This is a fork of performUnitOfWork specifcally for replaying a fiber that // just suspended. - // + workInProgressSuspendedReason = NotSuspended; + workInProgressThrownValue = null; const current = unitOfWork.alternate; setCurrentDebugFiberInDEV(unitOfWork); @@ -2404,6 +2332,8 @@ function throwAndUnwindWorkLoop(unitOfWork: Fiber, thrownValue: mixed) { // // Return to the normal work loop. This will unwind the stack, and potentially // result in showing a fallback. + workInProgressSuspendedReason = NotSuspended; + workInProgressThrownValue = null; resetSuspendedWorkLoopOnUnwind(unitOfWork); const returnFiber = unitOfWork.return;