From 1cd90d2ccc791f3ed25d93ceb7137746185f6e34 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 6 Jun 2022 12:15:59 -0400 Subject: [PATCH] Refactor of interleaved ("concurrent") update queue (#24663) * Always push updates to interleaved queue first Interleaves updates (updates that are scheduled while another render is already is progress) go into a special queue that isn't applied until the end of the current render. They are transferred to the "real" queue at the beginning of the next render. Currently we check during `setState` whether an update should go directly onto the real queue or onto the special interleaved queue. The logic is subtle and it can lead to bugs if you mess it up, as in #24400. Instead, this changes it to always go onto the interleaved queue. The behavior is the same but the logic is simpler. As a further step, we can also wait to update the `childLanes` until the end of the current render. I'll do this in the next step. * Move setState return path traversal to own call A lot of the logic around scheduling an update needs access to the fiber root. To obtain this reference, we must walk up the fiber return path. We also do this to update `childLanes` on all the parent nodes, so we can use the same traversal for both purposes. The traversal currently happens inside `scheduleUpdateOnFiber`, but sometimes we need to access it beyond that function, too. So I've hoisted the traversal out of `scheduleUpdateOnFiber` into its own function call that happens at the beginning of the `setState` algorithm. * Rename ReactInterleavedUpdates -> ReactFiberConcurrentUpdates The scope of this module is expanding so I've renamed accordingly. No behavioral changes. * Enqueue and update childLanes in same function During a setState, the childLanes are updated immediately, even if a render is already in progress. This can lead to subtle concurrency bugs, so the plan is to wait until the in-progress render has finished before updating the childLanes, to prevent subtle concurrency bugs. As a step toward that change, when scheduling an update, we should not update the childLanes directly, but instead defer to the ReactConcurrentUpdates module to do it at the appropriate time. This makes markUpdateLaneFromFiberToRoot a private function that is only called from the ReactConcurrentUpdates module. * [FORKED] Don't update childLanes until after current render (This is the riskiest commit in the stack. Only affects the "new" reconciler fork.) Updates that occur in a concurrent event while a render is already in progress can't be processed during that render. This is tricky to get right. Previously we solved this by adding concurrent updates to a special `interleaved` queue, then transferring the `interleaved` queue to the `pending` queue after the render phase had completed. However, we would still mutate the `childLanes` along the parent path immediately, which can lead to its own subtle data races. Instead, we can queue the entire operation until after the render phase has completed. This replaces the need for an `interleaved` field on every fiber/hook queue. The main motivation for this change, aside from simplifying the logic a bit, is so we can read information about the current fiber while we're walking up its return path, like whether it's inside a hidden tree. (I haven't done anything like that in this commit, though.) * Add 17691ac to forked revisions --- .../src/createReactNoop.js | 2 +- .../src/ReactFiberBeginWork.new.js | 13 +- .../src/ReactFiberBeginWork.old.js | 13 +- .../src/ReactFiberClassComponent.new.js | 16 +- .../src/ReactFiberClassComponent.old.js | 16 +- ...w.js => ReactFiberClassUpdateQueue.new.js} | 72 +++--- ...d.js => ReactFiberClassUpdateQueue.old.js} | 58 ++--- .../src/ReactFiberCommitWork.new.js | 4 +- .../src/ReactFiberCommitWork.old.js | 4 +- .../src/ReactFiberConcurrentUpdates.new.js | 206 ++++++++++++++++++ .../src/ReactFiberConcurrentUpdates.old.js | 184 ++++++++++++++++ .../src/ReactFiberHooks.new.js | 94 +++----- .../src/ReactFiberHooks.old.js | 78 +++---- .../src/ReactFiberHotReloading.new.js | 6 +- .../src/ReactFiberHotReloading.old.js | 6 +- .../src/ReactFiberInterleavedUpdates.new.js | 59 ----- .../src/ReactFiberInterleavedUpdates.old.js | 59 ----- .../src/ReactFiberNewContext.new.js | 4 +- .../src/ReactFiberNewContext.old.js | 4 +- .../src/ReactFiberReconciler.new.js | 78 +++++-- .../src/ReactFiberReconciler.old.js | 78 +++++-- .../src/ReactFiberRoot.new.js | 2 +- .../src/ReactFiberRoot.old.js | 2 +- .../src/ReactFiberThrow.new.js | 4 +- .../src/ReactFiberThrow.old.js | 4 +- .../src/ReactFiberWorkLoop.new.js | 117 +++------- .../src/ReactFiberWorkLoop.old.js | 107 ++------- scripts/merge-fork/forked-revisions | 1 + 28 files changed, 729 insertions(+), 562 deletions(-) rename packages/react-reconciler/src/{ReactUpdateQueue.new.js => ReactFiberClassUpdateQueue.new.js} (94%) rename packages/react-reconciler/src/{ReactUpdateQueue.old.js => ReactFiberClassUpdateQueue.old.js} (96%) create mode 100644 packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js create mode 100644 packages/react-reconciler/src/ReactFiberConcurrentUpdates.old.js delete mode 100644 packages/react-reconciler/src/ReactFiberInterleavedUpdates.new.js delete mode 100644 packages/react-reconciler/src/ReactFiberInterleavedUpdates.old.js diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 13a0f49105c27..d055149a09868 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -18,7 +18,7 @@ import type { Fiber, TransitionTracingCallbacks, } from 'react-reconciler/src/ReactInternalTypes'; -import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue'; +import type {UpdateQueue} from 'react-reconciler/src/ReactFiberClassUpdateQueue.new'; import type {ReactNodeList} from 'shared/ReactTypes'; import type {RootTag} from 'react-reconciler/src/ReactRootTags'; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index f1f5913b01433..4f227ed4710c4 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -33,7 +33,7 @@ import type { CacheComponentState, SpawnedCachePool, } from './ReactFiberCacheComponent.new'; -import type {UpdateQueue} from './ReactUpdateQueue.new'; +import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new'; import type {RootState} from './ReactFiberRoot.new'; import { enableSuspenseAvoidThisFallback, @@ -131,7 +131,7 @@ import { cloneUpdateQueue, initializeUpdateQueue, enqueueCapturedUpdate, -} from './ReactUpdateQueue.new'; +} from './ReactFiberClassUpdateQueue.new'; import { NoLane, NoLanes, @@ -234,6 +234,7 @@ import { getWorkInProgressRoot, pushRenderLanes, } from './ReactFiberWorkLoop.new'; +import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.new'; import {setWorkInProgressVersion} from './ReactMutableSource.new'; import {pushCacheProvider, CacheContext} from './ReactFiberCacheComponent.new'; import {createCapturedValue} from './ReactCapturedValue'; @@ -2626,7 +2627,13 @@ function updateDehydratedSuspenseComponent( suspenseState.retryLane = attemptHydrationAtLane; // TODO: Ideally this would inherit the event time of the current render const eventTime = NoTimestamp; - scheduleUpdateOnFiber(current, attemptHydrationAtLane, eventTime); + enqueueConcurrentRenderForLane(current, attemptHydrationAtLane); + scheduleUpdateOnFiber( + root, + current, + attemptHydrationAtLane, + eventTime, + ); } else { // We have already tried to ping at a higher priority than we're rendering with // so if we got here, we must have failed to hydrate at those levels. We must diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index 2303227185d2d..63a8ae2212c7c 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -33,7 +33,7 @@ import type { CacheComponentState, SpawnedCachePool, } from './ReactFiberCacheComponent.old'; -import type {UpdateQueue} from './ReactUpdateQueue.old'; +import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old'; import type {RootState} from './ReactFiberRoot.old'; import { enableSuspenseAvoidThisFallback, @@ -131,7 +131,7 @@ import { cloneUpdateQueue, initializeUpdateQueue, enqueueCapturedUpdate, -} from './ReactUpdateQueue.old'; +} from './ReactFiberClassUpdateQueue.old'; import { NoLane, NoLanes, @@ -234,6 +234,7 @@ import { getWorkInProgressRoot, pushRenderLanes, } from './ReactFiberWorkLoop.old'; +import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.old'; import {setWorkInProgressVersion} from './ReactMutableSource.old'; import {pushCacheProvider, CacheContext} from './ReactFiberCacheComponent.old'; import {createCapturedValue} from './ReactCapturedValue'; @@ -2626,7 +2627,13 @@ function updateDehydratedSuspenseComponent( suspenseState.retryLane = attemptHydrationAtLane; // TODO: Ideally this would inherit the event time of the current render const eventTime = NoTimestamp; - scheduleUpdateOnFiber(current, attemptHydrationAtLane, eventTime); + enqueueConcurrentRenderForLane(current, attemptHydrationAtLane); + scheduleUpdateOnFiber( + root, + current, + attemptHydrationAtLane, + eventTime, + ); } else { // We have already tried to ping at a higher priority than we're rendering with // so if we got here, we must have failed to hydrate at those levels. We must diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.new.js b/packages/react-reconciler/src/ReactFiberClassComponent.new.js index 23bc16ecce99a..45977bfa6fee8 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.new.js @@ -9,7 +9,7 @@ import type {Fiber} from './ReactInternalTypes'; import type {Lanes} from './ReactFiberLane.new'; -import type {UpdateQueue} from './ReactUpdateQueue.new'; +import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new'; import type {Flags} from './ReactFiberFlags'; import * as React from 'react'; @@ -58,7 +58,7 @@ import { ForceUpdate, initializeUpdateQueue, cloneUpdateQueue, -} from './ReactUpdateQueue.new'; +} from './ReactFiberClassUpdateQueue.new'; import {NoLanes} from './ReactFiberLane.new'; import { cacheContext, @@ -215,9 +215,9 @@ const classComponentUpdater = { update.callback = callback; } - enqueueUpdate(fiber, update, lane); - const root = scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = enqueueUpdate(fiber, update, lane); if (root !== null) { + scheduleUpdateOnFiber(root, fiber, lane, eventTime); entangleTransitions(root, fiber, lane); } @@ -250,9 +250,9 @@ const classComponentUpdater = { update.callback = callback; } - enqueueUpdate(fiber, update, lane); - const root = scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = enqueueUpdate(fiber, update, lane); if (root !== null) { + scheduleUpdateOnFiber(root, fiber, lane, eventTime); entangleTransitions(root, fiber, lane); } @@ -284,9 +284,9 @@ const classComponentUpdater = { update.callback = callback; } - enqueueUpdate(fiber, update, lane); - const root = scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = enqueueUpdate(fiber, update, lane); if (root !== null) { + scheduleUpdateOnFiber(root, fiber, lane, eventTime); entangleTransitions(root, fiber, lane); } diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.old.js b/packages/react-reconciler/src/ReactFiberClassComponent.old.js index 793156401efb7..a921cf2f47ba9 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.old.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.old.js @@ -9,7 +9,7 @@ import type {Fiber} from './ReactInternalTypes'; import type {Lanes} from './ReactFiberLane.old'; -import type {UpdateQueue} from './ReactUpdateQueue.old'; +import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old'; import type {Flags} from './ReactFiberFlags'; import * as React from 'react'; @@ -58,7 +58,7 @@ import { ForceUpdate, initializeUpdateQueue, cloneUpdateQueue, -} from './ReactUpdateQueue.old'; +} from './ReactFiberClassUpdateQueue.old'; import {NoLanes} from './ReactFiberLane.old'; import { cacheContext, @@ -215,9 +215,9 @@ const classComponentUpdater = { update.callback = callback; } - enqueueUpdate(fiber, update, lane); - const root = scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = enqueueUpdate(fiber, update, lane); if (root !== null) { + scheduleUpdateOnFiber(root, fiber, lane, eventTime); entangleTransitions(root, fiber, lane); } @@ -250,9 +250,9 @@ const classComponentUpdater = { update.callback = callback; } - enqueueUpdate(fiber, update, lane); - const root = scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = enqueueUpdate(fiber, update, lane); if (root !== null) { + scheduleUpdateOnFiber(root, fiber, lane, eventTime); entangleTransitions(root, fiber, lane); } @@ -284,9 +284,9 @@ const classComponentUpdater = { update.callback = callback; } - enqueueUpdate(fiber, update, lane); - const root = scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = enqueueUpdate(fiber, update, lane); if (root !== null) { + scheduleUpdateOnFiber(root, fiber, lane, eventTime); entangleTransitions(root, fiber, lane); } diff --git a/packages/react-reconciler/src/ReactUpdateQueue.new.js b/packages/react-reconciler/src/ReactFiberClassUpdateQueue.new.js similarity index 94% rename from packages/react-reconciler/src/ReactUpdateQueue.new.js rename to packages/react-reconciler/src/ReactFiberClassUpdateQueue.new.js index 1f665a88dfa49..0311920ba2994 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.new.js +++ b/packages/react-reconciler/src/ReactFiberClassUpdateQueue.new.js @@ -107,9 +107,12 @@ import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags import {StrictLegacyMode} from './ReactTypeOfMode'; import { markSkippedUpdateLanes, - isInterleavedUpdate, + isUnsafeClassRenderPhaseUpdate, } from './ReactFiberWorkLoop.new'; -import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new'; +import { + enqueueConcurrentClassUpdate, + unsafe_markUpdateLaneFromFiberToRoot, +} from './ReactFiberConcurrentUpdates.new'; import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.new'; import assign from 'shared/assign'; @@ -129,7 +132,6 @@ export type Update = {| export type SharedQueue = {| pending: Update | null, - interleaved: Update | null, lanes: Lanes, |}; @@ -169,7 +171,6 @@ export function initializeUpdateQueue(fiber: Fiber): void { lastBaseUpdate: null, shared: { pending: null, - interleaved: null, lanes: NoLanes, }, effects: null, @@ -214,40 +215,15 @@ export function enqueueUpdate( fiber: Fiber, update: Update, lane: Lane, -) { +): FiberRoot | null { const updateQueue = fiber.updateQueue; if (updateQueue === null) { // Only occurs if the fiber has been unmounted. - return; + return null; } const sharedQueue: SharedQueue = (updateQueue: any).shared; - if (isInterleavedUpdate(fiber, lane)) { - const interleaved = sharedQueue.interleaved; - if (interleaved === null) { - // This is the first update. Create a circular list. - update.next = update; - // At the end of the current render, this queue's interleaved updates will - // be transferred to the pending queue. - pushInterleavedQueue(sharedQueue); - } else { - update.next = interleaved.next; - interleaved.next = update; - } - sharedQueue.interleaved = update; - } else { - const pending = sharedQueue.pending; - if (pending === null) { - // This is the first update. Create a circular list. - update.next = update; - } else { - update.next = pending.next; - pending.next = update; - } - sharedQueue.pending = update; - } - if (__DEV__) { if ( currentlyProcessingQueue === sharedQueue && @@ -262,6 +238,28 @@ export function enqueueUpdate( didWarnUpdateInsideUpdate = true; } } + + if (isUnsafeClassRenderPhaseUpdate(fiber)) { + // This is an unsafe render phase update. Add directly to the update + // queue so we can process it immediately during the current render. + const pending = sharedQueue.pending; + if (pending === null) { + // This is the first update. Create a circular list. + update.next = update; + } else { + update.next = pending.next; + pending.next = update; + } + sharedQueue.pending = update; + + // Update the childLanes even though we're most likely already rendering + // this fiber. This is for backwards compatibility in the case where you + // update a different component during render phase than the one that is + // currently renderings (a pattern that is accompanied by a warning). + return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane); + } else { + return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane); + } } export function entangleTransitions(root: FiberRoot, fiber: Fiber, lane: Lane) { @@ -622,17 +620,7 @@ export function processUpdateQueue( queue.firstBaseUpdate = newFirstBaseUpdate; queue.lastBaseUpdate = newLastBaseUpdate; - // Interleaved updates are stored on a separate queue. We aren't going to - // process them during this render, but we do need to track which lanes - // are remaining. - const lastInterleaved = queue.shared.interleaved; - if (lastInterleaved !== null) { - let interleaved = lastInterleaved; - do { - newLanes = mergeLanes(newLanes, interleaved.lane); - interleaved = ((interleaved: any).next: Update); - } while (interleaved !== lastInterleaved); - } else if (firstBaseUpdate === null) { + if (firstBaseUpdate === null) { // `queue.lanes` is used for entangling transitions. We can set it back to // zero once the queue is empty. queue.shared.lanes = NoLanes; diff --git a/packages/react-reconciler/src/ReactUpdateQueue.old.js b/packages/react-reconciler/src/ReactFiberClassUpdateQueue.old.js similarity index 96% rename from packages/react-reconciler/src/ReactUpdateQueue.old.js rename to packages/react-reconciler/src/ReactFiberClassUpdateQueue.old.js index e59ea26f2161a..cc1643eeefed6 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.old.js +++ b/packages/react-reconciler/src/ReactFiberClassUpdateQueue.old.js @@ -107,9 +107,12 @@ import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags import {StrictLegacyMode} from './ReactTypeOfMode'; import { markSkippedUpdateLanes, - isInterleavedUpdate, + isUnsafeClassRenderPhaseUpdate, } from './ReactFiberWorkLoop.old'; -import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old'; +import { + enqueueConcurrentClassUpdate, + unsafe_markUpdateLaneFromFiberToRoot, +} from './ReactFiberConcurrentUpdates.old'; import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.old'; import assign from 'shared/assign'; @@ -214,40 +217,15 @@ export function enqueueUpdate( fiber: Fiber, update: Update, lane: Lane, -) { +): FiberRoot | null { const updateQueue = fiber.updateQueue; if (updateQueue === null) { // Only occurs if the fiber has been unmounted. - return; + return null; } const sharedQueue: SharedQueue = (updateQueue: any).shared; - if (isInterleavedUpdate(fiber, lane)) { - const interleaved = sharedQueue.interleaved; - if (interleaved === null) { - // This is the first update. Create a circular list. - update.next = update; - // At the end of the current render, this queue's interleaved updates will - // be transferred to the pending queue. - pushInterleavedQueue(sharedQueue); - } else { - update.next = interleaved.next; - interleaved.next = update; - } - sharedQueue.interleaved = update; - } else { - const pending = sharedQueue.pending; - if (pending === null) { - // This is the first update. Create a circular list. - update.next = update; - } else { - update.next = pending.next; - pending.next = update; - } - sharedQueue.pending = update; - } - if (__DEV__) { if ( currentlyProcessingQueue === sharedQueue && @@ -262,6 +240,28 @@ export function enqueueUpdate( didWarnUpdateInsideUpdate = true; } } + + if (isUnsafeClassRenderPhaseUpdate(fiber)) { + // This is an unsafe render phase update. Add directly to the update + // queue so we can process it immediately during the current render. + const pending = sharedQueue.pending; + if (pending === null) { + // This is the first update. Create a circular list. + update.next = update; + } else { + update.next = pending.next; + pending.next = update; + } + sharedQueue.pending = update; + + // Update the childLanes even though we're most likely already rendering + // this fiber. This is for backwards compatibility in the case where you + // update a different component during render phase than the one that is + // currently renderings (a pattern that is accompanied by a warning). + return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane); + } else { + return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane); + } } export function entangleTransitions(root: FiberRoot, fiber: Fiber, lane: Lane) { diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 7e8459d496d9f..d023c1f776b3d 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -19,7 +19,7 @@ import type {Fiber} from './ReactInternalTypes'; import type {FiberRoot} from './ReactInternalTypes'; import type {Lanes} from './ReactFiberLane.new'; import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; -import type {UpdateQueue} from './ReactUpdateQueue.new'; +import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new'; import type {Wakeable} from 'shared/ReactTypes'; import type { @@ -100,7 +100,7 @@ import { startPassiveEffectTimer, } from './ReactProfilerTimer.new'; import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode'; -import {commitUpdateQueue} from './ReactUpdateQueue.new'; +import {commitUpdateQueue} from './ReactFiberClassUpdateQueue.new'; import { getPublicInstance, supportsMutation, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index 4dceedd3a3038..a9eae21939df2 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -19,7 +19,7 @@ import type {Fiber} from './ReactInternalTypes'; import type {FiberRoot} from './ReactInternalTypes'; import type {Lanes} from './ReactFiberLane.old'; import type {SuspenseState} from './ReactFiberSuspenseComponent.old'; -import type {UpdateQueue} from './ReactUpdateQueue.old'; +import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.old'; import type {Wakeable} from 'shared/ReactTypes'; import type { @@ -100,7 +100,7 @@ import { startPassiveEffectTimer, } from './ReactProfilerTimer.old'; import {ConcurrentMode, NoMode, ProfileMode} from './ReactTypeOfMode'; -import {commitUpdateQueue} from './ReactUpdateQueue.old'; +import {commitUpdateQueue} from './ReactFiberClassUpdateQueue.old'; import { getPublicInstance, supportsMutation, diff --git a/packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js b/packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js new file mode 100644 index 0000000000000..ed5a02cfea9e2 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js @@ -0,0 +1,206 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {FiberRoot} from './ReactInternalTypes'; +import type { + UpdateQueue as HookQueue, + Update as HookUpdate, +} from './ReactFiberHooks.new'; +import type { + SharedQueue as ClassQueue, + Update as ClassUpdate, +} from './ReactFiberClassUpdateQueue.new'; +import type {Lane, Lanes} from './ReactFiberLane.new'; + +import {warnAboutUpdateOnNotYetMountedFiberInDEV} from './ReactFiberWorkLoop.new'; +import {NoLane, NoLanes, mergeLanes} from './ReactFiberLane.new'; +import {NoFlags, Placement, Hydrating} from './ReactFiberFlags'; +import {HostRoot} from './ReactWorkTags'; + +type ConcurrentUpdate = { + next: ConcurrentUpdate, +}; + +type ConcurrentQueue = { + pending: ConcurrentUpdate | null, +}; + +// If a render is in progress, and we receive an update from a concurrent event, +// we wait until the current render is over (either finished or interrupted) +// before adding it to the fiber/hook queue. Push to this array so we can +// access the queue, fiber, update, et al later. +const concurrentQueues: Array = []; +let concurrentQueuesIndex = 0; + +export function finishQueueingConcurrentUpdates(): Lanes { + const endIndex = concurrentQueuesIndex; + concurrentQueuesIndex = 0; + + let lanes = NoLanes; + + let i = 0; + while (i < endIndex) { + const fiber: Fiber = concurrentQueues[i]; + concurrentQueues[i++] = null; + const queue: ConcurrentQueue = concurrentQueues[i]; + concurrentQueues[i++] = null; + const update: ConcurrentUpdate = concurrentQueues[i]; + concurrentQueues[i++] = null; + const lane: Lane = concurrentQueues[i]; + concurrentQueues[i++] = null; + + if (queue !== null && update !== null) { + const pending = queue.pending; + if (pending === null) { + // This is the first update. Create a circular list. + update.next = update; + } else { + update.next = pending.next; + pending.next = update; + } + queue.pending = update; + } + + if (lane !== NoLane) { + lanes = mergeLanes(lanes, lane); + markUpdateLaneFromFiberToRoot(fiber, lane); + } + } + + return lanes; +} + +function enqueueUpdate( + fiber: Fiber, + queue: ConcurrentQueue | null, + update: ConcurrentUpdate | null, + lane: Lane, +) { + // Don't update the `childLanes` on the return path yet. If we already in + // the middle of rendering, wait until after it has completed. + concurrentQueues[concurrentQueuesIndex++] = fiber; + concurrentQueues[concurrentQueuesIndex++] = queue; + concurrentQueues[concurrentQueuesIndex++] = update; + concurrentQueues[concurrentQueuesIndex++] = lane; + + // The fiber's `lane` field is used in some places to check if any work is + // scheduled, to perform an eager bailout, so we need to update it immediately. + // TODO: We should probably move this to the "shared" queue instead. + fiber.lanes = mergeLanes(fiber.lanes, lane); + const alternate = fiber.alternate; + if (alternate !== null) { + alternate.lanes = mergeLanes(alternate.lanes, lane); + } +} + +export function enqueueConcurrentHookUpdate( + fiber: Fiber, + queue: HookQueue, + update: HookUpdate, + lane: Lane, +): FiberRoot | null { + const concurrentQueue: ConcurrentQueue = (queue: any); + const concurrentUpdate: ConcurrentUpdate = (update: any); + enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane); + return getRootForUpdatedFiber(fiber); +} + +export function enqueueConcurrentHookUpdateAndEagerlyBailout( + fiber: Fiber, + queue: HookQueue, + update: HookUpdate, +): void { + // This function is used to queue an update that doesn't need a rerender. The + // only reason we queue it is in case there's a subsequent higher priority + // update that causes it to be rebased. + const lane = NoLane; + const concurrentQueue: ConcurrentQueue = (queue: any); + const concurrentUpdate: ConcurrentUpdate = (update: any); + enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane); +} + +export function enqueueConcurrentClassUpdate( + fiber: Fiber, + queue: ClassQueue, + update: ClassUpdate, + lane: Lane, +): FiberRoot | null { + const concurrentQueue: ConcurrentQueue = (queue: any); + const concurrentUpdate: ConcurrentUpdate = (update: any); + enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane); + return getRootForUpdatedFiber(fiber); +} + +export function enqueueConcurrentRenderForLane( + fiber: Fiber, + lane: Lane, +): FiberRoot | null { + enqueueUpdate(fiber, null, null, lane); + return getRootForUpdatedFiber(fiber); +} + +// Calling this function outside this module should only be done for backwards +// compatibility and should always be accompanied by a warning. +export function unsafe_markUpdateLaneFromFiberToRoot( + sourceFiber: Fiber, + lane: Lane, +): FiberRoot | null { + markUpdateLaneFromFiberToRoot(sourceFiber, lane); + return getRootForUpdatedFiber(sourceFiber); +} + +function markUpdateLaneFromFiberToRoot(sourceFiber: Fiber, lane: Lane): void { + // Update the source fiber's lanes + sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane); + let alternate = sourceFiber.alternate; + if (alternate !== null) { + alternate.lanes = mergeLanes(alternate.lanes, lane); + } + // Walk the parent path to the root and update the child lanes. + let parent = sourceFiber.return; + while (parent !== null) { + parent.childLanes = mergeLanes(parent.childLanes, lane); + alternate = parent.alternate; + if (alternate !== null) { + alternate.childLanes = mergeLanes(alternate.childLanes, lane); + } + parent = parent.return; + } +} + +function getRootForUpdatedFiber(sourceFiber: Fiber): FiberRoot | null { + // When a setState happens, we must ensure the root is scheduled. Because + // update queues do not have a backpointer to the root, the only way to do + // this currently is to walk up the return path. This used to not be a big + // deal because we would have to walk up the return path to set + // the `childLanes`, anyway, but now those two traversals happen at + // different times. + // TODO: Consider adding a `root` backpointer on the update queue. + detectUpdateOnUnmountedFiber(sourceFiber, sourceFiber); + let node = sourceFiber; + let parent = node.return; + while (parent !== null) { + detectUpdateOnUnmountedFiber(sourceFiber, node); + node = parent; + parent = node.return; + } + return node.tag === HostRoot ? (node.stateNode: FiberRoot) : null; +} + +function detectUpdateOnUnmountedFiber(sourceFiber: Fiber, parent: Fiber) { + if (__DEV__) { + const alternate = parent.alternate; + if ( + alternate === null && + (parent.flags & (Placement | Hydrating)) !== NoFlags + ) { + warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber); + } + } +} diff --git a/packages/react-reconciler/src/ReactFiberConcurrentUpdates.old.js b/packages/react-reconciler/src/ReactFiberConcurrentUpdates.old.js new file mode 100644 index 0000000000000..8f3a11c938f65 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberConcurrentUpdates.old.js @@ -0,0 +1,184 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {FiberRoot} from './ReactInternalTypes'; +import type { + UpdateQueue as HookQueue, + Update as HookUpdate, +} from './ReactFiberHooks.old'; +import type { + SharedQueue as ClassQueue, + Update as ClassUpdate, +} from './ReactFiberClassUpdateQueue.old'; +import type {Lane} from './ReactFiberLane.old'; + +import {warnAboutUpdateOnNotYetMountedFiberInDEV} from './ReactFiberWorkLoop.old'; +import {mergeLanes} from './ReactFiberLane.old'; +import {NoFlags, Placement, Hydrating} from './ReactFiberFlags'; +import {HostRoot} from './ReactWorkTags'; + +// An array of all update queues that received updates during the current +// render. When this render exits, either because it finishes or because it is +// interrupted, the interleaved updates will be transferred onto the main part +// of the queue. +let concurrentQueues: Array< + HookQueue | ClassQueue, +> | null = null; + +export function pushConcurrentUpdateQueue( + queue: HookQueue | ClassQueue, +) { + if (concurrentQueues === null) { + concurrentQueues = [queue]; + } else { + concurrentQueues.push(queue); + } +} + +export function finishQueueingConcurrentUpdates() { + // Transfer the interleaved updates onto the main queue. Each queue has a + // `pending` field and an `interleaved` field. When they are not null, they + // point to the last node in a circular linked list. We need to append the + // interleaved list to the end of the pending list by joining them into a + // single, circular list. + if (concurrentQueues !== null) { + for (let i = 0; i < concurrentQueues.length; i++) { + const queue = concurrentQueues[i]; + const lastInterleavedUpdate = queue.interleaved; + if (lastInterleavedUpdate !== null) { + queue.interleaved = null; + const firstInterleavedUpdate = lastInterleavedUpdate.next; + const lastPendingUpdate = queue.pending; + if (lastPendingUpdate !== null) { + const firstPendingUpdate = lastPendingUpdate.next; + lastPendingUpdate.next = (firstInterleavedUpdate: any); + lastInterleavedUpdate.next = (firstPendingUpdate: any); + } + queue.pending = (lastInterleavedUpdate: any); + } + } + concurrentQueues = null; + } +} + +export function enqueueConcurrentHookUpdate( + fiber: Fiber, + queue: HookQueue, + update: HookUpdate, + lane: Lane, +) { + const interleaved = queue.interleaved; + if (interleaved === null) { + // This is the first update. Create a circular list. + update.next = update; + // At the end of the current render, this queue's interleaved updates will + // be transferred to the pending queue. + pushConcurrentUpdateQueue(queue); + } else { + update.next = interleaved.next; + interleaved.next = update; + } + queue.interleaved = update; + + return markUpdateLaneFromFiberToRoot(fiber, lane); +} + +export function enqueueConcurrentHookUpdateAndEagerlyBailout( + fiber: Fiber, + queue: HookQueue, + update: HookUpdate, + lane: Lane, +): void { + const interleaved = queue.interleaved; + if (interleaved === null) { + // This is the first update. Create a circular list. + update.next = update; + // At the end of the current render, this queue's interleaved updates will + // be transferred to the pending queue. + pushConcurrentUpdateQueue(queue); + } else { + update.next = interleaved.next; + interleaved.next = update; + } + queue.interleaved = update; +} + +export function enqueueConcurrentClassUpdate( + fiber: Fiber, + queue: ClassQueue, + update: ClassUpdate, + lane: Lane, +) { + const interleaved = queue.interleaved; + if (interleaved === null) { + // This is the first update. Create a circular list. + update.next = update; + // At the end of the current render, this queue's interleaved updates will + // be transferred to the pending queue. + pushConcurrentUpdateQueue(queue); + } else { + update.next = interleaved.next; + interleaved.next = update; + } + queue.interleaved = update; + + return markUpdateLaneFromFiberToRoot(fiber, lane); +} + +export function enqueueConcurrentRenderForLane(fiber: Fiber, lane: Lane) { + return markUpdateLaneFromFiberToRoot(fiber, lane); +} + +// Calling this function outside this module should only be done for backwards +// compatibility and should always be accompanied by a warning. +export const unsafe_markUpdateLaneFromFiberToRoot = markUpdateLaneFromFiberToRoot; + +function markUpdateLaneFromFiberToRoot( + sourceFiber: Fiber, + lane: Lane, +): FiberRoot | null { + // Update the source fiber's lanes + sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane); + let alternate = sourceFiber.alternate; + if (alternate !== null) { + alternate.lanes = mergeLanes(alternate.lanes, lane); + } + if (__DEV__) { + if ( + alternate === null && + (sourceFiber.flags & (Placement | Hydrating)) !== NoFlags + ) { + warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber); + } + } + // Walk the parent path to the root and update the child lanes. + let node = sourceFiber; + let parent = sourceFiber.return; + while (parent !== null) { + parent.childLanes = mergeLanes(parent.childLanes, lane); + alternate = parent.alternate; + if (alternate !== null) { + alternate.childLanes = mergeLanes(alternate.childLanes, lane); + } else { + if (__DEV__) { + if ((parent.flags & (Placement | Hydrating)) !== NoFlags) { + warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber); + } + } + } + node = parent; + parent = parent.return; + } + if (node.tag === HostRoot) { + const root: FiberRoot = node.stateNode; + return root; + } else { + return null; + } +} diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 1742d00ae1580..697e87dd01379 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -87,7 +87,6 @@ import { requestUpdateLane, requestEventTime, markSkippedUpdateLanes, - isInterleavedUpdate, } from './ReactFiberWorkLoop.new'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; @@ -111,14 +110,18 @@ import { createUpdate as createLegacyQueueUpdate, enqueueUpdate as enqueueLegacyQueueUpdate, entangleTransitions as entangleLegacyQueueTransitions, -} from './ReactUpdateQueue.new'; -import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new'; +} from './ReactFiberClassUpdateQueue.new'; +import { + enqueueConcurrentHookUpdate, + enqueueConcurrentHookUpdateAndEagerlyBailout, + enqueueConcurrentRenderForLane, +} from './ReactFiberConcurrentUpdates.new'; import {getTreeId} from './ReactFiberTreeContext.new'; import {now} from './Scheduler'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; -type Update = {| +export type Update = {| lane: Lane, action: A, hasEagerState: boolean, @@ -128,7 +131,6 @@ type Update = {| export type UpdateQueue = {| pending: Update | null, - interleaved: Update | null, lanes: Lanes, dispatch: (A => mixed) | null, lastRenderedReducer: ((S, A) => S) | null, @@ -738,7 +740,6 @@ function mountReducer( hook.memoizedState = hook.baseState = initialState; const queue: UpdateQueue = { pending: null, - interleaved: null, lanes: NoLanes, dispatch: null, lastRenderedReducer: reducer, @@ -885,22 +886,7 @@ function updateReducer( queue.lastRenderedState = newState; } - // Interleaved updates are stored on a separate queue. We aren't going to - // process them during this render, but we do need to track which lanes - // are remaining. - const lastInterleaved = queue.interleaved; - if (lastInterleaved !== null) { - let interleaved = lastInterleaved; - do { - const interleavedLane = interleaved.lane; - currentlyRenderingFiber.lanes = mergeLanes( - currentlyRenderingFiber.lanes, - interleavedLane, - ); - markSkippedUpdateLanes(interleavedLane); - interleaved = ((interleaved: any).next: Update); - } while (interleaved !== lastInterleaved); - } else if (baseQueue === null) { + if (baseQueue === null) { // `queue.lanes` is used for entangling transitions. We can set it back to // zero once the queue is empty. queue.lanes = NoLanes; @@ -1208,7 +1194,6 @@ function useMutableSource( // including any interleaving updates that occur. const newQueue: UpdateQueue> = { pending: null, - interleaved: null, lanes: NoLanes, dispatch: null, lastRenderedReducer: basicStateReducer, @@ -1497,7 +1482,10 @@ function checkIfSnapshotChanged(inst) { } function forceStoreRerender(fiber) { - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } } function mountState( @@ -1511,7 +1499,6 @@ function mountState( hook.memoizedState = hook.baseState = initialState; const queue: UpdateQueue> = { pending: null, - interleaved: null, lanes: NoLanes, dispatch: null, lastRenderedReducer: basicStateReducer, @@ -2153,10 +2140,13 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { switch (provider.tag) { case CacheComponent: case HostRoot: { + // Schedule an update on the cache boundary to trigger a refresh. const lane = requestUpdateLane(provider); const eventTime = requestEventTime(); - const root = scheduleUpdateOnFiber(provider, lane, eventTime); + const refreshUpdate = createLegacyQueueUpdate(eventTime, lane); + const root = enqueueLegacyQueueUpdate(provider, refreshUpdate, lane); if (root !== null) { + scheduleUpdateOnFiber(root, provider, lane, eventTime); entangleLegacyQueueTransitions(root, provider, lane); } @@ -2170,13 +2160,10 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { seededCache.data.set(seedKey, seedValue); } - // Schedule an update on the cache boundary to trigger a refresh. - const refreshUpdate = createLegacyQueueUpdate(eventTime, lane); const payload = { cache: seededCache, }; refreshUpdate.payload = payload; - enqueueLegacyQueueUpdate(provider, refreshUpdate, lane); return; } } @@ -2213,10 +2200,10 @@ function dispatchReducerAction( if (isRenderPhaseUpdate(fiber)) { enqueueRenderPhaseUpdate(queue, update); } else { - enqueueUpdate(fiber, queue, update, lane); - const eventTime = requestEventTime(); - const root = scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane); if (root !== null) { + const eventTime = requestEventTime(); + scheduleUpdateOnFiber(root, fiber, lane, eventTime); entangleTransitionUpdate(root, queue, lane); } } @@ -2252,8 +2239,6 @@ function dispatchSetState( if (isRenderPhaseUpdate(fiber)) { enqueueRenderPhaseUpdate(queue, update); } else { - enqueueUpdate(fiber, queue, update, lane); - const alternate = fiber.alternate; if ( fiber.lanes === NoLanes && @@ -2283,6 +2268,8 @@ function dispatchSetState( // It's still possible that we'll need to rebase this update later, // if the component re-renders for a different reason and by that // time the reducer has changed. + // TODO: Do we still need to entangle transitions in this case? + enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update); return; } } catch (error) { @@ -2294,9 +2281,11 @@ function dispatchSetState( } } } - const eventTime = requestEventTime(); - const root = scheduleUpdateOnFiber(fiber, lane, eventTime); + + const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane); if (root !== null) { + const eventTime = requestEventTime(); + scheduleUpdateOnFiber(root, fiber, lane, eventTime); entangleTransitionUpdate(root, queue, lane); } } @@ -2331,38 +2320,7 @@ function enqueueRenderPhaseUpdate( queue.pending = update; } -function enqueueUpdate( - fiber: Fiber, - queue: UpdateQueue, - update: Update, - lane: Lane, -) { - if (isInterleavedUpdate(fiber, lane)) { - const interleaved = queue.interleaved; - if (interleaved === null) { - // This is the first update. Create a circular list. - update.next = update; - // At the end of the current render, this queue's interleaved updates will - // be transferred to the pending queue. - pushInterleavedQueue(queue); - } else { - update.next = interleaved.next; - interleaved.next = update; - } - queue.interleaved = update; - } else { - const pending = queue.pending; - if (pending === null) { - // This is the first update. Create a circular list. - update.next = update; - } else { - update.next = pending.next; - pending.next = update; - } - queue.pending = update; - } -} - +// TODO: Move to ReactFiberConcurrentUpdates? function entangleTransitionUpdate( root: FiberRoot, queue: UpdateQueue, diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 20717bde1cd74..fc25083fe4253 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -87,7 +87,6 @@ import { requestUpdateLane, requestEventTime, markSkippedUpdateLanes, - isInterleavedUpdate, } from './ReactFiberWorkLoop.old'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; @@ -111,14 +110,18 @@ import { createUpdate as createLegacyQueueUpdate, enqueueUpdate as enqueueLegacyQueueUpdate, entangleTransitions as entangleLegacyQueueTransitions, -} from './ReactUpdateQueue.old'; -import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old'; +} from './ReactFiberClassUpdateQueue.old'; +import { + enqueueConcurrentHookUpdate, + enqueueConcurrentHookUpdateAndEagerlyBailout, + enqueueConcurrentRenderForLane, +} from './ReactFiberConcurrentUpdates.old'; import {getTreeId} from './ReactFiberTreeContext.old'; import {now} from './Scheduler'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; -type Update = {| +export type Update = {| lane: Lane, action: A, hasEagerState: boolean, @@ -1497,7 +1500,10 @@ function checkIfSnapshotChanged(inst) { } function forceStoreRerender(fiber) { - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } } function mountState( @@ -2153,10 +2159,13 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { switch (provider.tag) { case CacheComponent: case HostRoot: { + // Schedule an update on the cache boundary to trigger a refresh. const lane = requestUpdateLane(provider); const eventTime = requestEventTime(); - const root = scheduleUpdateOnFiber(provider, lane, eventTime); + const refreshUpdate = createLegacyQueueUpdate(eventTime, lane); + const root = enqueueLegacyQueueUpdate(provider, refreshUpdate, lane); if (root !== null) { + scheduleUpdateOnFiber(root, provider, lane, eventTime); entangleLegacyQueueTransitions(root, provider, lane); } @@ -2170,13 +2179,10 @@ function refreshCache(fiber: Fiber, seedKey: ?() => T, seedValue: T) { seededCache.data.set(seedKey, seedValue); } - // Schedule an update on the cache boundary to trigger a refresh. - const refreshUpdate = createLegacyQueueUpdate(eventTime, lane); const payload = { cache: seededCache, }; refreshUpdate.payload = payload; - enqueueLegacyQueueUpdate(provider, refreshUpdate, lane); return; } } @@ -2213,10 +2219,10 @@ function dispatchReducerAction( if (isRenderPhaseUpdate(fiber)) { enqueueRenderPhaseUpdate(queue, update); } else { - enqueueUpdate(fiber, queue, update, lane); - const eventTime = requestEventTime(); - const root = scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane); if (root !== null) { + const eventTime = requestEventTime(); + scheduleUpdateOnFiber(root, fiber, lane, eventTime); entangleTransitionUpdate(root, queue, lane); } } @@ -2252,8 +2258,6 @@ function dispatchSetState( if (isRenderPhaseUpdate(fiber)) { enqueueRenderPhaseUpdate(queue, update); } else { - enqueueUpdate(fiber, queue, update, lane); - const alternate = fiber.alternate; if ( fiber.lanes === NoLanes && @@ -2283,6 +2287,13 @@ function dispatchSetState( // It's still possible that we'll need to rebase this update later, // if the component re-renders for a different reason and by that // time the reducer has changed. + // TODO: Do we still need to entangle transitions in this case? + enqueueConcurrentHookUpdateAndEagerlyBailout( + fiber, + queue, + update, + lane, + ); return; } } catch (error) { @@ -2294,9 +2305,11 @@ function dispatchSetState( } } } - const eventTime = requestEventTime(); - const root = scheduleUpdateOnFiber(fiber, lane, eventTime); + + const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane); if (root !== null) { + const eventTime = requestEventTime(); + scheduleUpdateOnFiber(root, fiber, lane, eventTime); entangleTransitionUpdate(root, queue, lane); } } @@ -2331,38 +2344,7 @@ function enqueueRenderPhaseUpdate( queue.pending = update; } -function enqueueUpdate( - fiber: Fiber, - queue: UpdateQueue, - update: Update, - lane: Lane, -) { - if (isInterleavedUpdate(fiber, lane)) { - const interleaved = queue.interleaved; - if (interleaved === null) { - // This is the first update. Create a circular list. - update.next = update; - // At the end of the current render, this queue's interleaved updates will - // be transferred to the pending queue. - pushInterleavedQueue(queue); - } else { - update.next = interleaved.next; - interleaved.next = update; - } - queue.interleaved = update; - } else { - const pending = queue.pending; - if (pending === null) { - // This is the first update. Create a circular list. - update.next = update; - } else { - update.next = pending.next; - pending.next = update; - } - queue.pending = update; - } -} - +// TODO: Move to ReactFiberConcurrentUpdates? function entangleTransitionUpdate( root: FiberRoot, queue: UpdateQueue, diff --git a/packages/react-reconciler/src/ReactFiberHotReloading.new.js b/packages/react-reconciler/src/ReactFiberHotReloading.new.js index 0867ffeb78439..22b78280f9cba 100644 --- a/packages/react-reconciler/src/ReactFiberHotReloading.new.js +++ b/packages/react-reconciler/src/ReactFiberHotReloading.new.js @@ -20,6 +20,7 @@ import { scheduleUpdateOnFiber, flushPassiveEffects, } from './ReactFiberWorkLoop.new'; +import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.new'; import {updateContainer} from './ReactFiberReconciler.new'; import {emptyContextObject} from './ReactFiberContext.new'; import {SyncLane, NoTimestamp} from './ReactFiberLane.new'; @@ -321,7 +322,10 @@ function scheduleFibersWithFamiliesRecursively( fiber._debugNeedsRemount = true; } if (needsRemount || needsRender) { - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } } if (child !== null && !needsRemount) { scheduleFibersWithFamiliesRecursively( diff --git a/packages/react-reconciler/src/ReactFiberHotReloading.old.js b/packages/react-reconciler/src/ReactFiberHotReloading.old.js index 4a5f53d3be731..805176560c4bc 100644 --- a/packages/react-reconciler/src/ReactFiberHotReloading.old.js +++ b/packages/react-reconciler/src/ReactFiberHotReloading.old.js @@ -20,6 +20,7 @@ import { scheduleUpdateOnFiber, flushPassiveEffects, } from './ReactFiberWorkLoop.old'; +import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.old'; import {updateContainer} from './ReactFiberReconciler.old'; import {emptyContextObject} from './ReactFiberContext.old'; import {SyncLane, NoTimestamp} from './ReactFiberLane.old'; @@ -321,7 +322,10 @@ function scheduleFibersWithFamiliesRecursively( fiber._debugNeedsRemount = true; } if (needsRemount || needsRender) { - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } } if (child !== null && !needsRemount) { scheduleFibersWithFamiliesRecursively( diff --git a/packages/react-reconciler/src/ReactFiberInterleavedUpdates.new.js b/packages/react-reconciler/src/ReactFiberInterleavedUpdates.new.js deleted file mode 100644 index 010730b1e78e0..0000000000000 --- a/packages/react-reconciler/src/ReactFiberInterleavedUpdates.new.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {UpdateQueue as HookQueue} from './ReactFiberHooks.new'; -import type {SharedQueue as ClassQueue} from './ReactUpdateQueue.new'; - -// An array of all update queues that received updates during the current -// render. When this render exits, either because it finishes or because it is -// interrupted, the interleaved updates will be transferred onto the main part -// of the queue. -let interleavedQueues: Array< - HookQueue | ClassQueue, -> | null = null; - -export function pushInterleavedQueue( - queue: HookQueue | ClassQueue, -) { - if (interleavedQueues === null) { - interleavedQueues = [queue]; - } else { - interleavedQueues.push(queue); - } -} - -export function hasInterleavedUpdates() { - return interleavedQueues !== null; -} - -export function enqueueInterleavedUpdates() { - // Transfer the interleaved updates onto the main queue. Each queue has a - // `pending` field and an `interleaved` field. When they are not null, they - // point to the last node in a circular linked list. We need to append the - // interleaved list to the end of the pending list by joining them into a - // single, circular list. - if (interleavedQueues !== null) { - for (let i = 0; i < interleavedQueues.length; i++) { - const queue = interleavedQueues[i]; - const lastInterleavedUpdate = queue.interleaved; - if (lastInterleavedUpdate !== null) { - queue.interleaved = null; - const firstInterleavedUpdate = lastInterleavedUpdate.next; - const lastPendingUpdate = queue.pending; - if (lastPendingUpdate !== null) { - const firstPendingUpdate = lastPendingUpdate.next; - lastPendingUpdate.next = (firstInterleavedUpdate: any); - lastInterleavedUpdate.next = (firstPendingUpdate: any); - } - queue.pending = (lastInterleavedUpdate: any); - } - } - interleavedQueues = null; - } -} diff --git a/packages/react-reconciler/src/ReactFiberInterleavedUpdates.old.js b/packages/react-reconciler/src/ReactFiberInterleavedUpdates.old.js deleted file mode 100644 index 0d3319801daa6..0000000000000 --- a/packages/react-reconciler/src/ReactFiberInterleavedUpdates.old.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {UpdateQueue as HookQueue} from './ReactFiberHooks.old'; -import type {SharedQueue as ClassQueue} from './ReactUpdateQueue.old'; - -// An array of all update queues that received updates during the current -// render. When this render exits, either because it finishes or because it is -// interrupted, the interleaved updates will be transferred onto the main part -// of the queue. -let interleavedQueues: Array< - HookQueue | ClassQueue, -> | null = null; - -export function pushInterleavedQueue( - queue: HookQueue | ClassQueue, -) { - if (interleavedQueues === null) { - interleavedQueues = [queue]; - } else { - interleavedQueues.push(queue); - } -} - -export function hasInterleavedUpdates() { - return interleavedQueues !== null; -} - -export function enqueueInterleavedUpdates() { - // Transfer the interleaved updates onto the main queue. Each queue has a - // `pending` field and an `interleaved` field. When they are not null, they - // point to the last node in a circular linked list. We need to append the - // interleaved list to the end of the pending list by joining them into a - // single, circular list. - if (interleavedQueues !== null) { - for (let i = 0; i < interleavedQueues.length; i++) { - const queue = interleavedQueues[i]; - const lastInterleavedUpdate = queue.interleaved; - if (lastInterleavedUpdate !== null) { - queue.interleaved = null; - const firstInterleavedUpdate = lastInterleavedUpdate.next; - const lastPendingUpdate = queue.pending; - if (lastPendingUpdate !== null) { - const firstPendingUpdate = lastPendingUpdate.next; - lastPendingUpdate.next = (firstInterleavedUpdate: any); - lastInterleavedUpdate.next = (firstPendingUpdate: any); - } - queue.pending = (lastInterleavedUpdate: any); - } - } - interleavedQueues = null; - } -} diff --git a/packages/react-reconciler/src/ReactFiberNewContext.new.js b/packages/react-reconciler/src/ReactFiberNewContext.new.js index 68862e568818f..eae7b232c5eb5 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.new.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.new.js @@ -15,7 +15,7 @@ import type { } from './ReactInternalTypes'; import type {StackCursor} from './ReactFiberStack.new'; import type {Lanes} from './ReactFiberLane.new'; -import type {SharedQueue} from './ReactUpdateQueue.new'; +import type {SharedQueue} from './ReactFiberClassUpdateQueue.new'; import {isPrimaryRenderer} from './ReactFiberHostConfig'; import {createCursor, push, pop} from './ReactFiberStack.new'; @@ -39,7 +39,7 @@ import { } from './ReactFiberFlags'; import is from 'shared/objectIs'; -import {createUpdate, ForceUpdate} from './ReactUpdateQueue.new'; +import {createUpdate, ForceUpdate} from './ReactFiberClassUpdateQueue.new'; import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork.new'; import { enableLazyContextPropagation, diff --git a/packages/react-reconciler/src/ReactFiberNewContext.old.js b/packages/react-reconciler/src/ReactFiberNewContext.old.js index de96664e774a2..cbc2d0069b356 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.old.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.old.js @@ -15,7 +15,7 @@ import type { } from './ReactInternalTypes'; import type {StackCursor} from './ReactFiberStack.old'; import type {Lanes} from './ReactFiberLane.old'; -import type {SharedQueue} from './ReactUpdateQueue.old'; +import type {SharedQueue} from './ReactFiberClassUpdateQueue.old'; import {isPrimaryRenderer} from './ReactFiberHostConfig'; import {createCursor, push, pop} from './ReactFiberStack.old'; @@ -39,7 +39,7 @@ import { } from './ReactFiberFlags'; import is from 'shared/objectIs'; -import {createUpdate, ForceUpdate} from './ReactUpdateQueue.old'; +import {createUpdate, ForceUpdate} from './ReactFiberClassUpdateQueue.old'; import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork.old'; import { enableLazyContextPropagation, diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index 136276637d3ea..f9899c93b9211 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -68,11 +68,12 @@ import { discreteUpdates, flushPassiveEffects, } from './ReactFiberWorkLoop.new'; +import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.new'; import { createUpdate, enqueueUpdate, entangleTransitions, -} from './ReactUpdateQueue.new'; +} from './ReactFiberClassUpdateQueue.new'; import { isRendering as ReactCurrentFiberIsRendering, current as ReactCurrentFiberCurrent, @@ -377,9 +378,9 @@ export function updateContainer( update.callback = callback; } - enqueueUpdate(current, update, lane); - const root = scheduleUpdateOnFiber(current, lane, eventTime); + const root = enqueueUpdate(current, update, lane); if (root !== null) { + scheduleUpdateOnFiber(root, current, lane, eventTime); entangleTransitions(root, current, lane); } @@ -413,7 +414,7 @@ export function getPublicRootInstance( export function attemptSynchronousHydration(fiber: Fiber): void { switch (fiber.tag) { - case HostRoot: + case HostRoot: { const root: FiberRoot = fiber.stateNode; if (isRootDehydrated(root)) { // Flush the first scheduled "update". @@ -421,15 +422,22 @@ export function attemptSynchronousHydration(fiber: Fiber): void { flushRoot(root, lanes); } break; - case SuspenseComponent: - const eventTime = requestEventTime(); - flushSync(() => scheduleUpdateOnFiber(fiber, SyncLane, eventTime)); + } + case SuspenseComponent: { + flushSync(() => { + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + const eventTime = requestEventTime(); + scheduleUpdateOnFiber(root, fiber, SyncLane, eventTime); + } + }); // If we're still blocked after this, we need to increase // the priority of any promises resolving within this // boundary so that they next attempt also has higher pri. const retryLane = SyncLane; markRetryLaneIfNotHydrated(fiber, retryLane); break; + } } } @@ -460,9 +468,12 @@ export function attemptDiscreteHydration(fiber: Fiber): void { // Suspense. return; } - const eventTime = requestEventTime(); const lane = SyncLane; - scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = enqueueConcurrentRenderForLane(fiber, lane); + if (root !== null) { + const eventTime = requestEventTime(); + scheduleUpdateOnFiber(root, fiber, lane, eventTime); + } markRetryLaneIfNotHydrated(fiber, lane); } @@ -474,9 +485,12 @@ export function attemptContinuousHydration(fiber: Fiber): void { // Suspense. return; } - const eventTime = requestEventTime(); const lane = SelectiveHydrationLane; - scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = enqueueConcurrentRenderForLane(fiber, lane); + if (root !== null) { + const eventTime = requestEventTime(); + scheduleUpdateOnFiber(root, fiber, lane, eventTime); + } markRetryLaneIfNotHydrated(fiber, lane); } @@ -486,9 +500,12 @@ export function attemptHydrationAtCurrentPriority(fiber: Fiber): void { // their priority other than synchronously flush it. return; } - const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); - scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = enqueueConcurrentRenderForLane(fiber, lane); + if (root !== null) { + const eventTime = requestEventTime(); + scheduleUpdateOnFiber(root, fiber, lane, eventTime); + } markRetryLaneIfNotHydrated(fiber, lane); } @@ -664,7 +681,10 @@ if (__DEV__) { // Shallow cloning props works as a workaround for now to bypass the bailout check. fiber.memoizedProps = {...fiber.memoizedProps}; - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } } }; overrideHookStateDeletePath = ( @@ -685,7 +705,10 @@ if (__DEV__) { // Shallow cloning props works as a workaround for now to bypass the bailout check. fiber.memoizedProps = {...fiber.memoizedProps}; - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } } }; overrideHookStateRenamePath = ( @@ -707,7 +730,10 @@ if (__DEV__) { // Shallow cloning props works as a workaround for now to bypass the bailout check. fiber.memoizedProps = {...fiber.memoizedProps}; - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } } }; @@ -717,14 +743,20 @@ if (__DEV__) { if (fiber.alternate) { fiber.alternate.pendingProps = fiber.pendingProps; } - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } }; overridePropsDeletePath = (fiber: Fiber, path: Array) => { fiber.pendingProps = copyWithDelete(fiber.memoizedProps, path); if (fiber.alternate) { fiber.alternate.pendingProps = fiber.pendingProps; } - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } }; overridePropsRenamePath = ( fiber: Fiber, @@ -735,11 +767,17 @@ if (__DEV__) { if (fiber.alternate) { fiber.alternate.pendingProps = fiber.pendingProps; } - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } }; scheduleUpdate = (fiber: Fiber) => { - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } }; setErrorHandler = (newShouldErrorImpl: Fiber => ?boolean) => { diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index e014519320a51..58ee3656d501b 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -68,11 +68,12 @@ import { discreteUpdates, flushPassiveEffects, } from './ReactFiberWorkLoop.old'; +import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates.old'; import { createUpdate, enqueueUpdate, entangleTransitions, -} from './ReactUpdateQueue.old'; +} from './ReactFiberClassUpdateQueue.old'; import { isRendering as ReactCurrentFiberIsRendering, current as ReactCurrentFiberCurrent, @@ -377,9 +378,9 @@ export function updateContainer( update.callback = callback; } - enqueueUpdate(current, update, lane); - const root = scheduleUpdateOnFiber(current, lane, eventTime); + const root = enqueueUpdate(current, update, lane); if (root !== null) { + scheduleUpdateOnFiber(root, current, lane, eventTime); entangleTransitions(root, current, lane); } @@ -413,7 +414,7 @@ export function getPublicRootInstance( export function attemptSynchronousHydration(fiber: Fiber): void { switch (fiber.tag) { - case HostRoot: + case HostRoot: { const root: FiberRoot = fiber.stateNode; if (isRootDehydrated(root)) { // Flush the first scheduled "update". @@ -421,15 +422,22 @@ export function attemptSynchronousHydration(fiber: Fiber): void { flushRoot(root, lanes); } break; - case SuspenseComponent: - const eventTime = requestEventTime(); - flushSync(() => scheduleUpdateOnFiber(fiber, SyncLane, eventTime)); + } + case SuspenseComponent: { + flushSync(() => { + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + const eventTime = requestEventTime(); + scheduleUpdateOnFiber(root, fiber, SyncLane, eventTime); + } + }); // If we're still blocked after this, we need to increase // the priority of any promises resolving within this // boundary so that they next attempt also has higher pri. const retryLane = SyncLane; markRetryLaneIfNotHydrated(fiber, retryLane); break; + } } } @@ -460,9 +468,12 @@ export function attemptDiscreteHydration(fiber: Fiber): void { // Suspense. return; } - const eventTime = requestEventTime(); const lane = SyncLane; - scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = enqueueConcurrentRenderForLane(fiber, lane); + if (root !== null) { + const eventTime = requestEventTime(); + scheduleUpdateOnFiber(root, fiber, lane, eventTime); + } markRetryLaneIfNotHydrated(fiber, lane); } @@ -474,9 +485,12 @@ export function attemptContinuousHydration(fiber: Fiber): void { // Suspense. return; } - const eventTime = requestEventTime(); const lane = SelectiveHydrationLane; - scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = enqueueConcurrentRenderForLane(fiber, lane); + if (root !== null) { + const eventTime = requestEventTime(); + scheduleUpdateOnFiber(root, fiber, lane, eventTime); + } markRetryLaneIfNotHydrated(fiber, lane); } @@ -486,9 +500,12 @@ export function attemptHydrationAtCurrentPriority(fiber: Fiber): void { // their priority other than synchronously flush it. return; } - const eventTime = requestEventTime(); const lane = requestUpdateLane(fiber); - scheduleUpdateOnFiber(fiber, lane, eventTime); + const root = enqueueConcurrentRenderForLane(fiber, lane); + if (root !== null) { + const eventTime = requestEventTime(); + scheduleUpdateOnFiber(root, fiber, lane, eventTime); + } markRetryLaneIfNotHydrated(fiber, lane); } @@ -664,7 +681,10 @@ if (__DEV__) { // Shallow cloning props works as a workaround for now to bypass the bailout check. fiber.memoizedProps = {...fiber.memoizedProps}; - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } } }; overrideHookStateDeletePath = ( @@ -685,7 +705,10 @@ if (__DEV__) { // Shallow cloning props works as a workaround for now to bypass the bailout check. fiber.memoizedProps = {...fiber.memoizedProps}; - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } } }; overrideHookStateRenamePath = ( @@ -707,7 +730,10 @@ if (__DEV__) { // Shallow cloning props works as a workaround for now to bypass the bailout check. fiber.memoizedProps = {...fiber.memoizedProps}; - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } } }; @@ -717,14 +743,20 @@ if (__DEV__) { if (fiber.alternate) { fiber.alternate.pendingProps = fiber.pendingProps; } - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } }; overridePropsDeletePath = (fiber: Fiber, path: Array) => { fiber.pendingProps = copyWithDelete(fiber.memoizedProps, path); if (fiber.alternate) { fiber.alternate.pendingProps = fiber.pendingProps; } - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } }; overridePropsRenamePath = ( fiber: Fiber, @@ -735,11 +767,17 @@ if (__DEV__) { if (fiber.alternate) { fiber.alternate.pendingProps = fiber.pendingProps; } - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } }; scheduleUpdate = (fiber: Fiber) => { - scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane, NoTimestamp); + } }; setErrorHandler = (newShouldErrorImpl: Fiber => ?boolean) => { diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js index 304073ed019df..ca9edcc3ded7f 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.new.js +++ b/packages/react-reconciler/src/ReactFiberRoot.new.js @@ -37,7 +37,7 @@ import { enableUpdaterTracking, enableTransitionTracing, } from 'shared/ReactFeatureFlags'; -import {initializeUpdateQueue} from './ReactUpdateQueue.new'; +import {initializeUpdateQueue} from './ReactFiberClassUpdateQueue.new'; import {LegacyRoot, ConcurrentRoot} from './ReactRootTags'; import {createCache, retainCache} from './ReactFiberCacheComponent.new'; diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js index 3c4086394a1a6..f9c9e8091c8db 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.old.js +++ b/packages/react-reconciler/src/ReactFiberRoot.old.js @@ -37,7 +37,7 @@ import { enableUpdaterTracking, enableTransitionTracing, } from 'shared/ReactFeatureFlags'; -import {initializeUpdateQueue} from './ReactUpdateQueue.old'; +import {initializeUpdateQueue} from './ReactFiberClassUpdateQueue.old'; import {LegacyRoot, ConcurrentRoot} from './ReactRootTags'; import {createCache, retainCache} from './ReactFiberCacheComponent.old'; diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js index 9a3d1b619235d..3d13cd6407b43 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.new.js +++ b/packages/react-reconciler/src/ReactFiberThrow.new.js @@ -11,7 +11,7 @@ import type {Fiber} from './ReactInternalTypes'; import type {FiberRoot} from './ReactInternalTypes'; import type {Lane, Lanes} from './ReactFiberLane.new'; import type {CapturedValue} from './ReactCapturedValue'; -import type {Update} from './ReactUpdateQueue.new'; +import type {Update} from './ReactFiberClassUpdateQueue.new'; import type {Wakeable} from 'shared/ReactTypes'; import type {SuspenseContext} from './ReactFiberSuspenseContext.new'; @@ -48,7 +48,7 @@ import { CaptureUpdate, ForceUpdate, enqueueUpdate, -} from './ReactUpdateQueue.new'; +} from './ReactFiberClassUpdateQueue.new'; import {markFailedErrorBoundaryForHotReloading} from './ReactFiberHotReloading.new'; import { suspenseStackCursor, diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js index 66f91a69d343e..ba0dfb5c32aa7 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.old.js +++ b/packages/react-reconciler/src/ReactFiberThrow.old.js @@ -11,7 +11,7 @@ import type {Fiber} from './ReactInternalTypes'; import type {FiberRoot} from './ReactInternalTypes'; import type {Lane, Lanes} from './ReactFiberLane.old'; import type {CapturedValue} from './ReactCapturedValue'; -import type {Update} from './ReactUpdateQueue.old'; +import type {Update} from './ReactFiberClassUpdateQueue.old'; import type {Wakeable} from 'shared/ReactTypes'; import type {SuspenseContext} from './ReactFiberSuspenseContext.old'; @@ -48,7 +48,7 @@ import { CaptureUpdate, ForceUpdate, enqueueUpdate, -} from './ReactUpdateQueue.old'; +} from './ReactFiberClassUpdateQueue.old'; import {markFailedErrorBoundaryForHotReloading} from './ReactFiberHotReloading.old'; import { suspenseStackCursor, diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 6e438aaa7bb60..9857207dfb23e 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -105,11 +105,9 @@ import { import {LegacyRoot} from './ReactRootTags'; import { NoFlags, - Placement, Incomplete, StoreConsistency, HostEffectMask, - Hydrating, ForceClientRender, BeforeMutationMask, MutationMask, @@ -182,7 +180,7 @@ import { invokePassiveEffectUnmountInDEV, reportUncaughtErrorInDEV, } from './ReactFiberCommitWork.new'; -import {enqueueUpdate} from './ReactUpdateQueue.new'; +import {enqueueUpdate} from './ReactFiberClassUpdateQueue.new'; import {resetContextDependencies} from './ReactFiberNewContext.new'; import { resetHooksAfterThrow, @@ -196,9 +194,9 @@ import { createCursor, } from './ReactFiberStack.new'; import { - enqueueInterleavedUpdates, - hasInterleavedUpdates, -} from './ReactFiberInterleavedUpdates.new'; + enqueueConcurrentRenderForLane, + finishQueueingConcurrentUpdates, +} from './ReactFiberConcurrentUpdates.new'; import { markNestedUpdateScheduled, @@ -521,10 +519,11 @@ function requestRetryLane(fiber: Fiber) { } export function scheduleUpdateOnFiber( + root: FiberRoot, fiber: Fiber, lane: Lane, eventTime: number, -): FiberRoot | null { +) { checkForNestedUpdates(); if (__DEV__) { @@ -533,11 +532,6 @@ export function scheduleUpdateOnFiber( } } - const root = markUpdateLaneFromFiberToRoot(fiber, lane); - if (root === null) { - return null; - } - if (__DEV__) { if (isFlushingPassiveEffects) { didScheduleUpdateDuringPassiveEffects = true; @@ -606,8 +600,6 @@ export function scheduleUpdateOnFiber( } if (root === workInProgressRoot) { - // TODO: Consolidate with `isInterleavedUpdate` check - // Received an update to a tree that's in the middle of rendering. Mark // that there was an interleaved update work on this root. Unless the // `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render @@ -650,7 +642,6 @@ export function scheduleUpdateOnFiber( flushSyncCallbacksOnlyInLegacyMode(); } } - return root; } export function scheduleInitialHydrationOnRoot( @@ -673,73 +664,15 @@ export function scheduleInitialHydrationOnRoot( ensureRootIsScheduled(root, eventTime); } -// This is split into a separate function so we can mark a fiber with pending -// work without treating it as a typical update that originates from an event; -// e.g. retrying a Suspense boundary isn't an update, but it does schedule work -// on a fiber. -function markUpdateLaneFromFiberToRoot( - sourceFiber: Fiber, - lane: Lane, -): FiberRoot | null { - // Update the source fiber's lanes - sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane); - let alternate = sourceFiber.alternate; - if (alternate !== null) { - alternate.lanes = mergeLanes(alternate.lanes, lane); - } - if (__DEV__) { - if ( - alternate === null && - (sourceFiber.flags & (Placement | Hydrating)) !== NoFlags - ) { - warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber); - } - } - // Walk the parent path to the root and update the child lanes. - let node = sourceFiber; - let parent = sourceFiber.return; - while (parent !== null) { - parent.childLanes = mergeLanes(parent.childLanes, lane); - alternate = parent.alternate; - if (alternate !== null) { - alternate.childLanes = mergeLanes(alternate.childLanes, lane); - } else { - if (__DEV__) { - if ((parent.flags & (Placement | Hydrating)) !== NoFlags) { - warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber); - } - } - } - node = parent; - parent = parent.return; - } - if (node.tag === HostRoot) { - const root: FiberRoot = node.stateNode; - return root; - } else { - return null; - } -} - -export function isInterleavedUpdate(fiber: Fiber, lane: Lane) { +export function isUnsafeClassRenderPhaseUpdate(fiber: Fiber) { + // Check if this is a render phase update. Only called by class components, + // which special (deprecated) behavior for UNSAFE_componentWillReceive props. return ( - // TODO: Optimize slightly by comparing to root that fiber belongs to. - // Requires some refactoring. Not a big deal though since it's rare for - // concurrent apps to have more than a single root. - (workInProgressRoot !== null || - // If the interleaved updates queue hasn't been cleared yet, then - // we should treat this as an interleaved update, too. This is also a - // defensive coding measure in case a new update comes in between when - // rendering has finished and when the interleaved updates are transferred - // to the main queue. - hasInterleavedUpdates()) && - (fiber.mode & ConcurrentMode) !== NoMode && - // If this is a render phase update (i.e. UNSAFE_componentWillReceiveProps), - // then don't treat this as an interleaved update. This pattern is - // accompanied by a warning but we haven't fully deprecated it yet. We can - // remove once the deferRenderPhaseUpdateToNextBatch flag is enabled. - (deferRenderPhaseUpdateToNextBatch || - (executionContext & RenderContext) === NoContext) + // TODO: Remove outdated deferRenderPhaseUpdateToNextBatch experiment. We + // decided not to enable it. + (!deferRenderPhaseUpdateToNextBatch || + (fiber.mode & ConcurrentMode) === NoMode) && + (executionContext & RenderContext) !== NoContext ); } @@ -1541,7 +1474,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { workInProgressRootConcurrentErrors = null; workInProgressRootRecoverableErrors = null; - enqueueInterleavedUpdates(); + finishQueueingConcurrentUpdates(); if (__DEV__) { ReactStrictModeWarnings.discardPendingWarnings(); @@ -2113,9 +2046,15 @@ function commitRootImpl( root.callbackNode = null; root.callbackPriority = NoLane; - // Update the first and last pending times on this root. The new first - // pending time is whatever is left on the root fiber. + // Check which lanes no longer have any work scheduled on them, and mark + // those as finished. let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes); + + // Make sure to account for lanes that were updated by a concurrent event + // during the render phase; don't mark them as finished. + const concurrentlyUpdatedLanes = finishQueueingConcurrentUpdates(); + remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes); + markRootFinished(root, remainingLanes); if (root === workInProgressRoot) { @@ -2617,9 +2556,8 @@ function captureCommitPhaseErrorOnRoot( ) { const errorInfo = createCapturedValue(error, sourceFiber); const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane)); - enqueueUpdate(rootFiber, update, (SyncLane: Lane)); + const root = enqueueUpdate(rootFiber, update, (SyncLane: Lane)); const eventTime = requestEventTime(); - const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane)); if (root !== null) { markRootUpdated(root, SyncLane, eventTime); ensureRootIsScheduled(root, eventTime); @@ -2667,9 +2605,8 @@ export function captureCommitPhaseError( errorInfo, (SyncLane: Lane), ); - enqueueUpdate(fiber, update, (SyncLane: Lane)); + const root = enqueueUpdate(fiber, update, (SyncLane: Lane)); const eventTime = requestEventTime(); - const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane)); if (root !== null) { markRootUpdated(root, SyncLane, eventTime); ensureRootIsScheduled(root, eventTime); @@ -2760,7 +2697,7 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) { } // TODO: Special case idle priority? const eventTime = requestEventTime(); - const root = markUpdateLaneFromFiberToRoot(boundaryFiber, retryLane); + const root = enqueueConcurrentRenderForLane(boundaryFiber, retryLane); if (root !== null) { markRootUpdated(root, retryLane, eventTime); ensureRootIsScheduled(root, eventTime); @@ -2931,7 +2868,7 @@ function invokeEffectsInDev( } let didWarnStateUpdateForNotYetMountedComponent: Set | null = null; -function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) { +export function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber: Fiber) { if (__DEV__) { if ((executionContext & RenderContext) !== NoContext) { // We let the other warning about render phase updates deal with this one. diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 3e02a4fe37697..5a9f93bbada24 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -105,11 +105,9 @@ import { import {LegacyRoot} from './ReactRootTags'; import { NoFlags, - Placement, Incomplete, StoreConsistency, HostEffectMask, - Hydrating, ForceClientRender, BeforeMutationMask, MutationMask, @@ -182,7 +180,7 @@ import { invokePassiveEffectUnmountInDEV, reportUncaughtErrorInDEV, } from './ReactFiberCommitWork.old'; -import {enqueueUpdate} from './ReactUpdateQueue.old'; +import {enqueueUpdate} from './ReactFiberClassUpdateQueue.old'; import {resetContextDependencies} from './ReactFiberNewContext.old'; import { resetHooksAfterThrow, @@ -196,9 +194,9 @@ import { createCursor, } from './ReactFiberStack.old'; import { - enqueueInterleavedUpdates, - hasInterleavedUpdates, -} from './ReactFiberInterleavedUpdates.old'; + enqueueConcurrentRenderForLane, + finishQueueingConcurrentUpdates, +} from './ReactFiberConcurrentUpdates.old'; import { markNestedUpdateScheduled, @@ -521,10 +519,11 @@ function requestRetryLane(fiber: Fiber) { } export function scheduleUpdateOnFiber( + root: FiberRoot, fiber: Fiber, lane: Lane, eventTime: number, -): FiberRoot | null { +) { checkForNestedUpdates(); if (__DEV__) { @@ -533,11 +532,6 @@ export function scheduleUpdateOnFiber( } } - const root = markUpdateLaneFromFiberToRoot(fiber, lane); - if (root === null) { - return null; - } - if (__DEV__) { if (isFlushingPassiveEffects) { didScheduleUpdateDuringPassiveEffects = true; @@ -606,8 +600,6 @@ export function scheduleUpdateOnFiber( } if (root === workInProgressRoot) { - // TODO: Consolidate with `isInterleavedUpdate` check - // Received an update to a tree that's in the middle of rendering. Mark // that there was an interleaved update work on this root. Unless the // `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render @@ -650,7 +642,6 @@ export function scheduleUpdateOnFiber( flushSyncCallbacksOnlyInLegacyMode(); } } - return root; } export function scheduleInitialHydrationOnRoot( @@ -673,73 +664,15 @@ export function scheduleInitialHydrationOnRoot( ensureRootIsScheduled(root, eventTime); } -// This is split into a separate function so we can mark a fiber with pending -// work without treating it as a typical update that originates from an event; -// e.g. retrying a Suspense boundary isn't an update, but it does schedule work -// on a fiber. -function markUpdateLaneFromFiberToRoot( - sourceFiber: Fiber, - lane: Lane, -): FiberRoot | null { - // Update the source fiber's lanes - sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane); - let alternate = sourceFiber.alternate; - if (alternate !== null) { - alternate.lanes = mergeLanes(alternate.lanes, lane); - } - if (__DEV__) { - if ( - alternate === null && - (sourceFiber.flags & (Placement | Hydrating)) !== NoFlags - ) { - warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber); - } - } - // Walk the parent path to the root and update the child lanes. - let node = sourceFiber; - let parent = sourceFiber.return; - while (parent !== null) { - parent.childLanes = mergeLanes(parent.childLanes, lane); - alternate = parent.alternate; - if (alternate !== null) { - alternate.childLanes = mergeLanes(alternate.childLanes, lane); - } else { - if (__DEV__) { - if ((parent.flags & (Placement | Hydrating)) !== NoFlags) { - warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber); - } - } - } - node = parent; - parent = parent.return; - } - if (node.tag === HostRoot) { - const root: FiberRoot = node.stateNode; - return root; - } else { - return null; - } -} - -export function isInterleavedUpdate(fiber: Fiber, lane: Lane) { +export function isUnsafeClassRenderPhaseUpdate(fiber: Fiber) { + // Check if this is a render phase update. Only called by class components, + // which special (deprecated) behavior for UNSAFE_componentWillReceive props. return ( - // TODO: Optimize slightly by comparing to root that fiber belongs to. - // Requires some refactoring. Not a big deal though since it's rare for - // concurrent apps to have more than a single root. - (workInProgressRoot !== null || - // If the interleaved updates queue hasn't been cleared yet, then - // we should treat this as an interleaved update, too. This is also a - // defensive coding measure in case a new update comes in between when - // rendering has finished and when the interleaved updates are transferred - // to the main queue. - hasInterleavedUpdates()) && - (fiber.mode & ConcurrentMode) !== NoMode && - // If this is a render phase update (i.e. UNSAFE_componentWillReceiveProps), - // then don't treat this as an interleaved update. This pattern is - // accompanied by a warning but we haven't fully deprecated it yet. We can - // remove once the deferRenderPhaseUpdateToNextBatch flag is enabled. - (deferRenderPhaseUpdateToNextBatch || - (executionContext & RenderContext) === NoContext) + // TODO: Remove outdated deferRenderPhaseUpdateToNextBatch experiment. We + // decided not to enable it. + (!deferRenderPhaseUpdateToNextBatch || + (fiber.mode & ConcurrentMode) === NoMode) && + (executionContext & RenderContext) !== NoContext ); } @@ -1541,7 +1474,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { workInProgressRootConcurrentErrors = null; workInProgressRootRecoverableErrors = null; - enqueueInterleavedUpdates(); + finishQueueingConcurrentUpdates(); if (__DEV__) { ReactStrictModeWarnings.discardPendingWarnings(); @@ -2617,9 +2550,8 @@ function captureCommitPhaseErrorOnRoot( ) { const errorInfo = createCapturedValue(error, sourceFiber); const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane)); - enqueueUpdate(rootFiber, update, (SyncLane: Lane)); + const root = enqueueUpdate(rootFiber, update, (SyncLane: Lane)); const eventTime = requestEventTime(); - const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane)); if (root !== null) { markRootUpdated(root, SyncLane, eventTime); ensureRootIsScheduled(root, eventTime); @@ -2667,9 +2599,8 @@ export function captureCommitPhaseError( errorInfo, (SyncLane: Lane), ); - enqueueUpdate(fiber, update, (SyncLane: Lane)); + const root = enqueueUpdate(fiber, update, (SyncLane: Lane)); const eventTime = requestEventTime(); - const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane)); if (root !== null) { markRootUpdated(root, SyncLane, eventTime); ensureRootIsScheduled(root, eventTime); @@ -2760,7 +2691,7 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) { } // TODO: Special case idle priority? const eventTime = requestEventTime(); - const root = markUpdateLaneFromFiberToRoot(boundaryFiber, retryLane); + const root = enqueueConcurrentRenderForLane(boundaryFiber, retryLane); if (root !== null) { markRootUpdated(root, retryLane, eventTime); ensureRootIsScheduled(root, eventTime); @@ -2931,7 +2862,7 @@ function invokeEffectsInDev( } let didWarnStateUpdateForNotYetMountedComponent: Set | null = null; -function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) { +export function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber: Fiber) { if (__DEV__) { if ((executionContext & RenderContext) !== NoContext) { // We let the other warning about render phase updates deal with this one. diff --git a/scripts/merge-fork/forked-revisions b/scripts/merge-fork/forked-revisions index e69de29bb2d1d..76d977e1f1c10 100644 --- a/scripts/merge-fork/forked-revisions +++ b/scripts/merge-fork/forked-revisions @@ -0,0 +1 @@ +17691acc071d56261d43c3cf183f287d983baa9b [FORKED] Don't update childLanes until after current render