mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Effects list refactor continued: did-bailout flag (#19322)
* Effects list rewrite * Improved deletions approach Process deletions as we traverse the tree during commit, before we process other effects. This has the result of better mimicking the previous sequencing. * Made deletions field nullable * Revert (no longer necessary) change to ReactNative test * Eagerly set Deletions effect on Fiber when adding child to deletions array * Initialize deletions array to null * Null out deletions array instead of splicing 🤡 * Removed TODO comment * Initial exploration on a did-bailout flag * fixed the rest of the bugs * Rolled temporary didBailout attribute into subtreeTag * addressed comments * Removed DidBailout subtree tag * Removed stale comment * use while loop instead of recursion for siblings * move bailout flag from while loop * Removed some unnecessary Deletion effectTags from children * Move Deletion effect assignment to deletions array initialization Co-authored-by: Luna <lunaris.ruan@gmail.com>
This commit is contained in:
@@ -367,7 +367,14 @@ describe('ReactDOMServerPartialHydration', () => {
|
||||
const span2 = container.getElementsByTagName('span')[0];
|
||||
// This is a new node.
|
||||
expect(span).not.toBe(span2);
|
||||
expect(ref.current).toBe(span2);
|
||||
|
||||
if (gate(flags => flags.new)) {
|
||||
// The effects list refactor causes this to be null because the Suspense Offscreen's child
|
||||
// is null. However, since we can't hydrate Suspense in legacy this change in behavior is ok
|
||||
expect(ref.current).toBe(null);
|
||||
} else {
|
||||
expect(ref.current).toBe(span2);
|
||||
}
|
||||
|
||||
// Resolving the promise should render the final content.
|
||||
suspend = false;
|
||||
|
||||
@@ -280,6 +280,7 @@ function ChildReconciler(shouldTrackSideEffects) {
|
||||
// deletions, so we can just append the deletion to the list. The remaining
|
||||
// effects aren't added until the complete phase. Once we implement
|
||||
// resuming, this may not be true.
|
||||
// TODO (effects) Get rid of effects list update here.
|
||||
const last = returnFiber.lastEffect;
|
||||
if (last !== null) {
|
||||
last.nextEffect = childToDelete;
|
||||
@@ -287,8 +288,15 @@ function ChildReconciler(shouldTrackSideEffects) {
|
||||
} else {
|
||||
returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
|
||||
}
|
||||
const deletions = returnFiber.deletions;
|
||||
if (deletions === null) {
|
||||
returnFiber.deletions = [childToDelete];
|
||||
// TODO (effects) Rename this to better reflect its new usage (e.g. ChildDeletions)
|
||||
returnFiber.effectTag |= Deletion;
|
||||
} else {
|
||||
deletions.push(childToDelete);
|
||||
}
|
||||
childToDelete.nextEffect = null;
|
||||
childToDelete.effectTag = Deletion;
|
||||
}
|
||||
|
||||
function deleteRemainingChildren(
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
enableBlocksAPI,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {NoEffect, Placement} from './ReactSideEffectTags';
|
||||
import {NoEffect as NoSubtreeEffect} from './ReactSubtreeTags';
|
||||
import {ConcurrentRoot, BlockingRoot} from './ReactRootTags';
|
||||
import {
|
||||
IndeterminateComponent,
|
||||
@@ -144,6 +145,8 @@ function FiberNode(
|
||||
|
||||
// Effects
|
||||
this.effectTag = NoEffect;
|
||||
this.subtreeTag = NoSubtreeEffect;
|
||||
this.deletions = null;
|
||||
this.nextEffect = null;
|
||||
|
||||
this.firstEffect = null;
|
||||
@@ -287,6 +290,8 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
|
||||
// We already have an alternate.
|
||||
// Reset the effect tag.
|
||||
workInProgress.effectTag = NoEffect;
|
||||
workInProgress.subtreeTag = NoSubtreeEffect;
|
||||
workInProgress.deletions = null;
|
||||
|
||||
// The effect list is no longer valid.
|
||||
workInProgress.nextEffect = null;
|
||||
@@ -826,6 +831,8 @@ export function assignFiberPropertiesInDEV(
|
||||
target.dependencies = source.dependencies;
|
||||
target.mode = source.mode;
|
||||
target.effectTag = source.effectTag;
|
||||
target.subtreeTag = source.subtreeTag;
|
||||
target.deletions = source.deletions;
|
||||
target.nextEffect = source.nextEffect;
|
||||
target.firstEffect = source.firstEffect;
|
||||
target.lastEffect = source.lastEffect;
|
||||
|
||||
@@ -2066,6 +2066,14 @@ function updateSuspensePrimaryChildren(
|
||||
currentFallbackChildFragment.nextEffect = null;
|
||||
currentFallbackChildFragment.effectTag = Deletion;
|
||||
workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChildFragment;
|
||||
const deletions = workInProgress.deletions;
|
||||
if (deletions === null) {
|
||||
workInProgress.deletions = [currentFallbackChildFragment];
|
||||
// TODO (effects) Rename this to better reflect its new usage (e.g. ChildDeletions)
|
||||
workInProgress.effectTag |= Deletion;
|
||||
} else {
|
||||
deletions.push(currentFallbackChildFragment);
|
||||
}
|
||||
}
|
||||
|
||||
workInProgress.child = primaryChildFragment;
|
||||
@@ -2131,9 +2139,11 @@ function updateSuspenseFallbackChildren(
|
||||
workInProgress.firstEffect = primaryChildFragment.firstEffect;
|
||||
workInProgress.lastEffect = progressedLastEffect;
|
||||
progressedLastEffect.nextEffect = null;
|
||||
workInProgress.deletions = null;
|
||||
} else {
|
||||
// TODO: Reset this somewhere else? Lol legacy mode is so weird.
|
||||
workInProgress.firstEffect = workInProgress.lastEffect = null;
|
||||
workInProgress.deletions = null;
|
||||
}
|
||||
} else {
|
||||
primaryChildFragment = createWorkInProgressOffscreenFiber(
|
||||
@@ -3040,8 +3050,15 @@ function remountFiber(
|
||||
} else {
|
||||
returnFiber.firstEffect = returnFiber.lastEffect = current;
|
||||
}
|
||||
const deletions = returnFiber.deletions;
|
||||
if (deletions === null) {
|
||||
returnFiber.deletions = [current];
|
||||
// TODO (effects) Rename this to better reflect its new usage (e.g. ChildDeletions)
|
||||
returnFiber.effectTag |= Deletion;
|
||||
} else {
|
||||
deletions.push(current);
|
||||
}
|
||||
current.nextEffect = null;
|
||||
current.effectTag = Deletion;
|
||||
|
||||
newWorkInProgress.effectTag |= Placement;
|
||||
|
||||
|
||||
@@ -1062,6 +1062,12 @@ function completeWork(
|
||||
// Reset the effect list before doing the second pass since that's now invalid.
|
||||
if (renderState.lastEffect === null) {
|
||||
workInProgress.firstEffect = null;
|
||||
workInProgress.subtreeTag = NoEffect;
|
||||
let child = workInProgress.child;
|
||||
while (child !== null) {
|
||||
child.deletions = null;
|
||||
child = child.sibling;
|
||||
}
|
||||
}
|
||||
workInProgress.lastEffect = renderState.lastEffect;
|
||||
// Reset the child fibers to their original state.
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
HostRoot,
|
||||
SuspenseComponent,
|
||||
} from './ReactWorkTags';
|
||||
import {Deletion, Placement, Hydrating} from './ReactSideEffectTags';
|
||||
import {Deletion, Hydrating, Placement} from './ReactSideEffectTags';
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
import {
|
||||
@@ -125,6 +125,14 @@ function deleteHydratableInstance(
|
||||
childToDelete.stateNode = instance;
|
||||
childToDelete.return = returnFiber;
|
||||
childToDelete.effectTag = Deletion;
|
||||
const deletions = returnFiber.deletions;
|
||||
if (deletions === null) {
|
||||
returnFiber.deletions = [childToDelete];
|
||||
// TODO (effects) Rename this to better reflect its new usage (e.g. ChildDeletions)
|
||||
returnFiber.effectTag |= Deletion;
|
||||
} else {
|
||||
deletions.push(childToDelete);
|
||||
}
|
||||
|
||||
// This might seem like it belongs on progressedFirstDeletion. However,
|
||||
// these children are not part of the reconciliation list of children.
|
||||
|
||||
@@ -98,7 +98,6 @@ import {
|
||||
Placement,
|
||||
Update,
|
||||
PlacementAndUpdate,
|
||||
Deletion,
|
||||
Ref,
|
||||
ContentReset,
|
||||
Snapshot,
|
||||
@@ -109,7 +108,16 @@ import {
|
||||
HostEffectMask,
|
||||
Hydrating,
|
||||
HydratingAndUpdate,
|
||||
BeforeMutationMask,
|
||||
MutationMask,
|
||||
LayoutMask,
|
||||
} from './ReactSideEffectTags';
|
||||
import {
|
||||
NoEffect as NoSubtreeTag,
|
||||
BeforeMutation,
|
||||
Mutation,
|
||||
Layout,
|
||||
} from './ReactSubtreeTags';
|
||||
import {
|
||||
NoLanePriority,
|
||||
SyncLanePriority,
|
||||
@@ -288,7 +296,6 @@ let globalMostRecentFallbackTime: number = 0;
|
||||
const FALLBACK_THROTTLE_MS: number = 500;
|
||||
const DEFAULT_TIMEOUT_MS: number = 5000;
|
||||
|
||||
let nextEffect: Fiber | null = null;
|
||||
let hasUncaughtError = false;
|
||||
let firstUncaughtError = null;
|
||||
let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
|
||||
@@ -1744,6 +1751,8 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
|
||||
// Mark the parent fiber as incomplete and clear its effect list.
|
||||
returnFiber.firstEffect = returnFiber.lastEffect = null;
|
||||
returnFiber.effectTag |= Incomplete;
|
||||
returnFiber.subtreeTag = NoSubtreeTag;
|
||||
returnFiber.deletions = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1780,60 +1789,133 @@ function resetChildLanes(completedWork: Fiber) {
|
||||
return;
|
||||
}
|
||||
|
||||
const didBailout =
|
||||
completedWork.alternate !== null &&
|
||||
completedWork.alternate.child === completedWork.child;
|
||||
|
||||
let newChildLanes = NoLanes;
|
||||
let subtreeTag = NoSubtreeTag;
|
||||
|
||||
// Bubble up the earliest expiration time.
|
||||
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
|
||||
// In profiling mode, resetChildExpirationTime is also used to reset
|
||||
// profiler durations.
|
||||
let actualDuration = completedWork.actualDuration;
|
||||
let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
|
||||
if (!didBailout) {
|
||||
// Bubble up the earliest expiration time.
|
||||
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
|
||||
// In profiling mode, resetChildExpirationTime is also used to reset
|
||||
// profiler durations.
|
||||
let actualDuration = completedWork.actualDuration;
|
||||
let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
|
||||
|
||||
// When a fiber is cloned, its actualDuration is reset to 0. This value will
|
||||
// only be updated if work is done on the fiber (i.e. it doesn't bailout).
|
||||
// When work is done, it should bubble to the parent's actualDuration. If
|
||||
// the fiber has not been cloned though, (meaning no work was done), then
|
||||
// this value will reflect the amount of time spent working on a previous
|
||||
// render. In that case it should not bubble. We determine whether it was
|
||||
// cloned by comparing the child pointer.
|
||||
const shouldBubbleActualDurations =
|
||||
completedWork.alternate === null ||
|
||||
completedWork.child !== completedWork.alternate.child;
|
||||
let child = completedWork.child;
|
||||
while (child !== null) {
|
||||
newChildLanes = mergeLanes(
|
||||
newChildLanes,
|
||||
mergeLanes(child.lanes, child.childLanes),
|
||||
);
|
||||
|
||||
let child = completedWork.child;
|
||||
while (child !== null) {
|
||||
newChildLanes = mergeLanes(
|
||||
newChildLanes,
|
||||
mergeLanes(child.lanes, child.childLanes),
|
||||
);
|
||||
if (shouldBubbleActualDurations) {
|
||||
subtreeTag |= child.subtreeTag;
|
||||
|
||||
const effectTag = child.effectTag;
|
||||
if ((effectTag & BeforeMutationMask) !== NoEffect) {
|
||||
subtreeTag |= BeforeMutation;
|
||||
}
|
||||
if ((effectTag & MutationMask) !== NoEffect) {
|
||||
subtreeTag |= Mutation;
|
||||
}
|
||||
if ((effectTag & LayoutMask) !== NoEffect) {
|
||||
subtreeTag |= Layout;
|
||||
}
|
||||
|
||||
// When a fiber is cloned, its actualDuration is reset to 0. This value will
|
||||
// only be updated if work is done on the fiber (i.e. it doesn't bailout).
|
||||
// When work is done, it should bubble to the parent's actualDuration. If
|
||||
// the fiber has not been cloned though, (meaning no work was done), then
|
||||
// this value will reflect the amount of time spent working on a previous
|
||||
// render. In that case it should not bubble. We determine whether it was
|
||||
// cloned by comparing the child pointer.
|
||||
actualDuration += child.actualDuration;
|
||||
}
|
||||
treeBaseDuration += child.treeBaseDuration;
|
||||
child = child.sibling;
|
||||
}
|
||||
|
||||
const isTimedOutSuspense =
|
||||
completedWork.tag === SuspenseComponent &&
|
||||
completedWork.memoizedState !== null;
|
||||
if (isTimedOutSuspense) {
|
||||
// Don't count time spent in a timed out Suspense subtree as part of the base duration.
|
||||
const primaryChildFragment = completedWork.child;
|
||||
if (primaryChildFragment !== null) {
|
||||
treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
|
||||
treeBaseDuration += child.treeBaseDuration;
|
||||
child = child.sibling;
|
||||
}
|
||||
|
||||
const isTimedOutSuspense =
|
||||
completedWork.tag === SuspenseComponent &&
|
||||
completedWork.memoizedState !== null;
|
||||
if (isTimedOutSuspense) {
|
||||
// Don't count time spent in a timed out Suspense subtree as part of the base duration.
|
||||
const primaryChildFragment = completedWork.child;
|
||||
if (primaryChildFragment !== null) {
|
||||
treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
|
||||
}
|
||||
}
|
||||
|
||||
completedWork.actualDuration = actualDuration;
|
||||
completedWork.treeBaseDuration = treeBaseDuration;
|
||||
} else {
|
||||
let child = completedWork.child;
|
||||
while (child !== null) {
|
||||
newChildLanes = mergeLanes(
|
||||
newChildLanes,
|
||||
mergeLanes(child.lanes, child.childLanes),
|
||||
);
|
||||
|
||||
subtreeTag |= child.subtreeTag;
|
||||
|
||||
const effectTag = child.effectTag;
|
||||
if ((effectTag & BeforeMutationMask) !== NoEffect) {
|
||||
subtreeTag |= BeforeMutation;
|
||||
}
|
||||
if ((effectTag & MutationMask) !== NoEffect) {
|
||||
subtreeTag |= Mutation;
|
||||
}
|
||||
if ((effectTag & LayoutMask) !== NoEffect) {
|
||||
subtreeTag |= Layout;
|
||||
}
|
||||
|
||||
child = child.sibling;
|
||||
}
|
||||
}
|
||||
|
||||
completedWork.actualDuration = actualDuration;
|
||||
completedWork.treeBaseDuration = treeBaseDuration;
|
||||
completedWork.subtreeTag |= subtreeTag;
|
||||
} else {
|
||||
let child = completedWork.child;
|
||||
while (child !== null) {
|
||||
newChildLanes = mergeLanes(
|
||||
newChildLanes,
|
||||
mergeLanes(child.lanes, child.childLanes),
|
||||
);
|
||||
child = child.sibling;
|
||||
// Bubble up the earliest expiration time.
|
||||
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
|
||||
// In profiling mode, resetChildExpirationTime is also used to reset
|
||||
// profiler durations.
|
||||
let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
|
||||
|
||||
let child = completedWork.child;
|
||||
while (child !== null) {
|
||||
newChildLanes = mergeLanes(
|
||||
newChildLanes,
|
||||
mergeLanes(child.lanes, child.childLanes),
|
||||
);
|
||||
|
||||
treeBaseDuration += child.treeBaseDuration;
|
||||
child = child.sibling;
|
||||
}
|
||||
|
||||
const isTimedOutSuspense =
|
||||
completedWork.tag === SuspenseComponent &&
|
||||
completedWork.memoizedState !== null;
|
||||
if (isTimedOutSuspense) {
|
||||
// Don't count time spent in a timed out Suspense subtree as part of the base duration.
|
||||
const primaryChildFragment = completedWork.child;
|
||||
if (primaryChildFragment !== null) {
|
||||
treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
|
||||
}
|
||||
}
|
||||
|
||||
completedWork.treeBaseDuration = treeBaseDuration;
|
||||
} else {
|
||||
let child = completedWork.child;
|
||||
while (child !== null) {
|
||||
newChildLanes = mergeLanes(
|
||||
newChildLanes,
|
||||
mergeLanes(child.lanes, child.childLanes),
|
||||
);
|
||||
|
||||
child = child.sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1952,26 +2034,7 @@ function commitRootImpl(root, renderPriorityLevel) {
|
||||
focusedInstanceHandle = prepareForCommit(root.containerInfo);
|
||||
shouldFireAfterActiveInstanceBlur = false;
|
||||
|
||||
nextEffect = firstEffect;
|
||||
do {
|
||||
if (__DEV__) {
|
||||
invokeGuardedCallback(null, commitBeforeMutationEffects, null);
|
||||
if (hasCaughtError()) {
|
||||
invariant(nextEffect !== null, 'Should be working on an effect.');
|
||||
const error = clearCaughtError();
|
||||
captureCommitPhaseError(nextEffect, error);
|
||||
nextEffect = nextEffect.nextEffect;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
commitBeforeMutationEffects();
|
||||
} catch (error) {
|
||||
invariant(nextEffect !== null, 'Should be working on an effect.');
|
||||
captureCommitPhaseError(nextEffect, error);
|
||||
nextEffect = nextEffect.nextEffect;
|
||||
}
|
||||
}
|
||||
} while (nextEffect !== null);
|
||||
commitBeforeMutationEffects(finishedWork);
|
||||
|
||||
// We no longer need to track the active instance fiber
|
||||
focusedInstanceHandle = null;
|
||||
@@ -1983,32 +2046,7 @@ function commitRootImpl(root, renderPriorityLevel) {
|
||||
}
|
||||
|
||||
// The next phase is the mutation phase, where we mutate the host tree.
|
||||
nextEffect = firstEffect;
|
||||
do {
|
||||
if (__DEV__) {
|
||||
invokeGuardedCallback(
|
||||
null,
|
||||
commitMutationEffects,
|
||||
null,
|
||||
root,
|
||||
renderPriorityLevel,
|
||||
);
|
||||
if (hasCaughtError()) {
|
||||
invariant(nextEffect !== null, 'Should be working on an effect.');
|
||||
const error = clearCaughtError();
|
||||
captureCommitPhaseError(nextEffect, error);
|
||||
nextEffect = nextEffect.nextEffect;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
commitMutationEffects(root, renderPriorityLevel);
|
||||
} catch (error) {
|
||||
invariant(nextEffect !== null, 'Should be working on an effect.');
|
||||
captureCommitPhaseError(nextEffect, error);
|
||||
nextEffect = nextEffect.nextEffect;
|
||||
}
|
||||
}
|
||||
} while (nextEffect !== null);
|
||||
commitMutationEffects(finishedWork, root, renderPriorityLevel);
|
||||
|
||||
if (shouldFireAfterActiveInstanceBlur) {
|
||||
afterActiveInstanceBlur();
|
||||
@@ -2024,28 +2062,7 @@ function commitRootImpl(root, renderPriorityLevel) {
|
||||
// The next phase is the layout phase, where we call effects that read
|
||||
// the host tree after it's been mutated. The idiomatic use case for this is
|
||||
// layout, but class component lifecycles also fire here for legacy reasons.
|
||||
nextEffect = firstEffect;
|
||||
do {
|
||||
if (__DEV__) {
|
||||
invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes);
|
||||
if (hasCaughtError()) {
|
||||
invariant(nextEffect !== null, 'Should be working on an effect.');
|
||||
const error = clearCaughtError();
|
||||
captureCommitPhaseError(nextEffect, error);
|
||||
nextEffect = nextEffect.nextEffect;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
commitLayoutEffects(root, lanes);
|
||||
} catch (error) {
|
||||
invariant(nextEffect !== null, 'Should be working on an effect.');
|
||||
captureCommitPhaseError(nextEffect, error);
|
||||
nextEffect = nextEffect.nextEffect;
|
||||
}
|
||||
}
|
||||
} while (nextEffect !== null);
|
||||
|
||||
nextEffect = null;
|
||||
commitLayoutEffects(finishedWork, root, lanes);
|
||||
|
||||
// Tell Scheduler to yield at the end of the frame, so the browser has an
|
||||
// opportunity to paint.
|
||||
@@ -2079,18 +2096,7 @@ function commitRootImpl(root, renderPriorityLevel) {
|
||||
pendingPassiveEffectsLanes = lanes;
|
||||
pendingPassiveEffectsRenderPriority = renderPriorityLevel;
|
||||
} else {
|
||||
// We are done with the effect chain at this point so let's clear the
|
||||
// nextEffect pointers to assist with GC. If we have passive effects, we'll
|
||||
// clear this in flushPassiveEffects.
|
||||
nextEffect = firstEffect;
|
||||
while (nextEffect !== null) {
|
||||
const nextNextEffect = nextEffect.nextEffect;
|
||||
nextEffect.nextEffect = null;
|
||||
if (nextEffect.effectTag & Deletion) {
|
||||
detachFiberAfterEffects(nextEffect);
|
||||
}
|
||||
nextEffect = nextNextEffect;
|
||||
}
|
||||
// TODO (effects) Detach sibling pointers for deleted Fibers
|
||||
}
|
||||
|
||||
// Read this again, since an effect might have updated it
|
||||
@@ -2172,162 +2178,313 @@ function commitRootImpl(root, renderPriorityLevel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function commitBeforeMutationEffects() {
|
||||
while (nextEffect !== null) {
|
||||
const current = nextEffect.alternate;
|
||||
function commitBeforeMutationEffects(firstChild: Fiber) {
|
||||
let fiber = firstChild;
|
||||
while (fiber !== null) {
|
||||
if (fiber.deletions !== null) {
|
||||
commitBeforeMutationEffectsDeletions(fiber.deletions);
|
||||
}
|
||||
|
||||
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
|
||||
if ((nextEffect.effectTag & Deletion) !== NoEffect) {
|
||||
if (doesFiberContain(nextEffect, focusedInstanceHandle)) {
|
||||
shouldFireAfterActiveInstanceBlur = true;
|
||||
beforeActiveInstanceBlur();
|
||||
}
|
||||
} else {
|
||||
// TODO: Move this out of the hot path using a dedicated effect tag.
|
||||
if (
|
||||
nextEffect.tag === SuspenseComponent &&
|
||||
isSuspenseBoundaryBeingHidden(current, nextEffect) &&
|
||||
doesFiberContain(nextEffect, focusedInstanceHandle)
|
||||
) {
|
||||
shouldFireAfterActiveInstanceBlur = true;
|
||||
beforeActiveInstanceBlur();
|
||||
}
|
||||
if (fiber.child !== null) {
|
||||
const primarySubtreeTag = fiber.subtreeTag & BeforeMutation;
|
||||
if (primarySubtreeTag !== NoSubtreeTag) {
|
||||
commitBeforeMutationEffects(fiber.child);
|
||||
}
|
||||
}
|
||||
|
||||
const effectTag = nextEffect.effectTag;
|
||||
if ((effectTag & Snapshot) !== NoEffect) {
|
||||
setCurrentDebugFiberInDEV(nextEffect);
|
||||
|
||||
commitBeforeMutationEffectOnFiber(current, nextEffect);
|
||||
|
||||
if (__DEV__) {
|
||||
setCurrentDebugFiberInDEV(fiber);
|
||||
invokeGuardedCallback(null, commitBeforeMutationEffectsImpl, null, fiber);
|
||||
if (hasCaughtError()) {
|
||||
const error = clearCaughtError();
|
||||
captureCommitPhaseError(fiber, error);
|
||||
}
|
||||
resetCurrentDebugFiberInDEV();
|
||||
}
|
||||
if ((effectTag & Passive) !== NoEffect) {
|
||||
// If there are passive effects, schedule a callback to flush at
|
||||
// the earliest opportunity.
|
||||
if (!rootDoesHavePassiveEffects) {
|
||||
rootDoesHavePassiveEffects = true;
|
||||
scheduleCallback(NormalSchedulerPriority, () => {
|
||||
flushPassiveEffects();
|
||||
return null;
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
commitBeforeMutationEffectsImpl(fiber);
|
||||
} catch (error) {
|
||||
captureCommitPhaseError(fiber, error);
|
||||
}
|
||||
}
|
||||
nextEffect = nextEffect.nextEffect;
|
||||
fiber = fiber.sibling;
|
||||
}
|
||||
}
|
||||
|
||||
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
|
||||
// TODO: Should probably move the bulk of this function to commitWork.
|
||||
while (nextEffect !== null) {
|
||||
setCurrentDebugFiberInDEV(nextEffect);
|
||||
function commitBeforeMutationEffectsImpl(fiber: Fiber) {
|
||||
const current = fiber.alternate;
|
||||
const effectTag = fiber.effectTag;
|
||||
|
||||
const effectTag = nextEffect.effectTag;
|
||||
|
||||
if (effectTag & ContentReset) {
|
||||
commitResetTextContent(nextEffect);
|
||||
}
|
||||
|
||||
if (effectTag & Ref) {
|
||||
const current = nextEffect.alternate;
|
||||
if (current !== null) {
|
||||
commitDetachRef(current);
|
||||
}
|
||||
if (enableScopeAPI) {
|
||||
// TODO: This is a temporary solution that allows us to transition away
|
||||
// from React Flare on www.
|
||||
if (nextEffect.tag === ScopeComponent) {
|
||||
commitAttachRef(nextEffect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The following switch statement is only concerned about placement,
|
||||
// updates, and deletions. To avoid needing to add a case for every possible
|
||||
// bitmap value, we remove the secondary effects from the effect tag and
|
||||
// switch on that value.
|
||||
const primaryEffectTag =
|
||||
effectTag & (Placement | Update | Deletion | Hydrating);
|
||||
switch (primaryEffectTag) {
|
||||
case Placement: {
|
||||
commitPlacement(nextEffect);
|
||||
// Clear the "placement" from effect tag so that we know that this is
|
||||
// inserted, before any life-cycles like componentDidMount gets called.
|
||||
// TODO: findDOMNode doesn't rely on this any more but isMounted does
|
||||
// and isMounted is deprecated anyway so we should be able to kill this.
|
||||
nextEffect.effectTag &= ~Placement;
|
||||
break;
|
||||
}
|
||||
case PlacementAndUpdate: {
|
||||
// Placement
|
||||
commitPlacement(nextEffect);
|
||||
// Clear the "placement" from effect tag so that we know that this is
|
||||
// inserted, before any life-cycles like componentDidMount gets called.
|
||||
nextEffect.effectTag &= ~Placement;
|
||||
|
||||
// Update
|
||||
const current = nextEffect.alternate;
|
||||
commitWork(current, nextEffect);
|
||||
break;
|
||||
}
|
||||
case Hydrating: {
|
||||
nextEffect.effectTag &= ~Hydrating;
|
||||
break;
|
||||
}
|
||||
case HydratingAndUpdate: {
|
||||
nextEffect.effectTag &= ~Hydrating;
|
||||
|
||||
// Update
|
||||
const current = nextEffect.alternate;
|
||||
commitWork(current, nextEffect);
|
||||
break;
|
||||
}
|
||||
case Update: {
|
||||
const current = nextEffect.alternate;
|
||||
commitWork(current, nextEffect);
|
||||
break;
|
||||
}
|
||||
case Deletion: {
|
||||
commitDeletion(root, nextEffect, renderPriorityLevel);
|
||||
break;
|
||||
}
|
||||
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
|
||||
// Check to see if the focused element was inside of a hidden (Suspense) subtree.
|
||||
// TODO: Move this out of the hot path using a dedicated effect tag.
|
||||
if (
|
||||
fiber.tag === SuspenseComponent &&
|
||||
isSuspenseBoundaryBeingHidden(current, fiber) &&
|
||||
doesFiberContain(fiber, focusedInstanceHandle)
|
||||
) {
|
||||
shouldFireAfterActiveInstanceBlur = true;
|
||||
beforeActiveInstanceBlur();
|
||||
}
|
||||
}
|
||||
|
||||
if ((effectTag & Snapshot) !== NoEffect) {
|
||||
setCurrentDebugFiberInDEV(fiber);
|
||||
commitBeforeMutationEffectOnFiber(current, fiber);
|
||||
resetCurrentDebugFiberInDEV();
|
||||
nextEffect = nextEffect.nextEffect;
|
||||
}
|
||||
|
||||
if ((effectTag & Passive) !== NoEffect) {
|
||||
// If there are passive effects, schedule a callback to flush at
|
||||
// the earliest opportunity.
|
||||
if (!rootDoesHavePassiveEffects) {
|
||||
rootDoesHavePassiveEffects = true;
|
||||
scheduleCallback(NormalSchedulerPriority, () => {
|
||||
flushPassiveEffects();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
|
||||
// TODO: Should probably move the bulk of this function to commitWork.
|
||||
while (nextEffect !== null) {
|
||||
setCurrentDebugFiberInDEV(nextEffect);
|
||||
function commitBeforeMutationEffectsDeletions(deletions: Array<Fiber>) {
|
||||
for (let i = 0; i < deletions.length; i++) {
|
||||
const fiber = deletions[i];
|
||||
|
||||
const effectTag = nextEffect.effectTag;
|
||||
// TODO (effects) It would be nice to avoid calling doesFiberContain()
|
||||
// Maybe we can repurpose one of the subtreeTag positions for this instead?
|
||||
// Use it to store which part of the tree the focused instance is in?
|
||||
// This assumes we can safely determine that instance during the "render" phase.
|
||||
|
||||
if (effectTag & (Update | Callback)) {
|
||||
const current = nextEffect.alternate;
|
||||
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
|
||||
if (doesFiberContain(fiber, ((focusedInstanceHandle: any): Fiber))) {
|
||||
shouldFireAfterActiveInstanceBlur = true;
|
||||
beforeActiveInstanceBlur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function commitMutationEffects(
|
||||
firstChild: Fiber,
|
||||
root: FiberRoot,
|
||||
renderPriorityLevel,
|
||||
) {
|
||||
let fiber = firstChild;
|
||||
while (fiber !== null) {
|
||||
if (fiber.deletions !== null) {
|
||||
commitMutationEffectsDeletions(
|
||||
fiber.deletions,
|
||||
root,
|
||||
renderPriorityLevel,
|
||||
);
|
||||
|
||||
// TODO (effects) Don't clear this yet; we may need to cleanup passive effects
|
||||
fiber.deletions = null;
|
||||
}
|
||||
|
||||
if (fiber.child !== null) {
|
||||
const primarySubtreeTag = fiber.subtreeTag & Mutation;
|
||||
if (primarySubtreeTag !== NoSubtreeTag) {
|
||||
commitMutationEffects(fiber.child, root, renderPriorityLevel);
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
setCurrentDebugFiberInDEV(fiber);
|
||||
invokeGuardedCallback(
|
||||
null,
|
||||
commitMutationEffectsImpl,
|
||||
null,
|
||||
fiber,
|
||||
root,
|
||||
renderPriorityLevel,
|
||||
);
|
||||
if (hasCaughtError()) {
|
||||
const error = clearCaughtError();
|
||||
captureCommitPhaseError(fiber, error);
|
||||
}
|
||||
resetCurrentDebugFiberInDEV();
|
||||
} else {
|
||||
try {
|
||||
commitMutationEffectsImpl(fiber, root, renderPriorityLevel);
|
||||
} catch (error) {
|
||||
captureCommitPhaseError(fiber, error);
|
||||
}
|
||||
}
|
||||
fiber = fiber.sibling;
|
||||
}
|
||||
}
|
||||
|
||||
function commitMutationEffectsImpl(
|
||||
fiber: Fiber,
|
||||
root: FiberRoot,
|
||||
renderPriorityLevel,
|
||||
) {
|
||||
const effectTag = fiber.effectTag;
|
||||
if (effectTag & ContentReset) {
|
||||
commitResetTextContent(fiber);
|
||||
}
|
||||
|
||||
if (effectTag & Ref) {
|
||||
const current = fiber.alternate;
|
||||
if (current !== null) {
|
||||
commitDetachRef(current);
|
||||
}
|
||||
if (enableScopeAPI) {
|
||||
// TODO: This is a temporary solution that allows us to transition away
|
||||
// from React Flare on www.
|
||||
if (effectTag & Ref && nextEffect.tag !== ScopeComponent) {
|
||||
commitAttachRef(nextEffect);
|
||||
if (fiber.tag === ScopeComponent) {
|
||||
commitAttachRef(fiber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The following switch statement is only concerned about placement,
|
||||
// updates, and deletions. To avoid needing to add a case for every possible
|
||||
// bitmap value, we remove the secondary effects from the effect tag and
|
||||
// switch on that value.
|
||||
const primaryEffectTag = effectTag & (Placement | Update | Hydrating);
|
||||
switch (primaryEffectTag) {
|
||||
case Placement: {
|
||||
commitPlacement(fiber);
|
||||
// Clear the "placement" from effect tag so that we know that this is
|
||||
// inserted, before any life-cycles like componentDidMount gets called.
|
||||
// TODO: findDOMNode doesn't rely on this any more but isMounted does
|
||||
// and isMounted is deprecated anyway so we should be able to kill this.
|
||||
fiber.effectTag &= ~Placement;
|
||||
break;
|
||||
}
|
||||
case PlacementAndUpdate: {
|
||||
// Placement
|
||||
commitPlacement(fiber);
|
||||
// Clear the "placement" from effect tag so that we know that this is
|
||||
// inserted, before any life-cycles like componentDidMount gets called.
|
||||
fiber.effectTag &= ~Placement;
|
||||
|
||||
// Update
|
||||
const current = fiber.alternate;
|
||||
commitWork(current, fiber);
|
||||
break;
|
||||
}
|
||||
case Hydrating: {
|
||||
fiber.effectTag &= ~Hydrating;
|
||||
break;
|
||||
}
|
||||
case HydratingAndUpdate: {
|
||||
fiber.effectTag &= ~Hydrating;
|
||||
|
||||
// Update
|
||||
const current = fiber.alternate;
|
||||
commitWork(current, fiber);
|
||||
break;
|
||||
}
|
||||
case Update: {
|
||||
const current = fiber.alternate;
|
||||
commitWork(current, fiber);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function commitMutationEffectsDeletions(
|
||||
deletions: Array<Fiber>,
|
||||
root: FiberRoot,
|
||||
renderPriorityLevel,
|
||||
) {
|
||||
for (let i = 0; i < deletions.length; i++) {
|
||||
const childToDelete = deletions[i];
|
||||
if (__DEV__) {
|
||||
invokeGuardedCallback(
|
||||
null,
|
||||
commitDeletion,
|
||||
null,
|
||||
root,
|
||||
childToDelete,
|
||||
renderPriorityLevel,
|
||||
);
|
||||
if (hasCaughtError()) {
|
||||
const error = clearCaughtError();
|
||||
captureCommitPhaseError(childToDelete, error);
|
||||
}
|
||||
} else {
|
||||
if (effectTag & Ref) {
|
||||
commitAttachRef(nextEffect);
|
||||
try {
|
||||
commitDeletion(root, childToDelete, renderPriorityLevel);
|
||||
} catch (error) {
|
||||
captureCommitPhaseError(childToDelete, error);
|
||||
}
|
||||
}
|
||||
// Don't clear the Deletion effect yet; we also use it to know when we need to detach refs later.
|
||||
}
|
||||
}
|
||||
|
||||
function commitLayoutEffects(
|
||||
firstChild: Fiber,
|
||||
root: FiberRoot,
|
||||
committedLanes: Lanes,
|
||||
) {
|
||||
let fiber = firstChild;
|
||||
while (fiber !== null) {
|
||||
if (fiber.child !== null) {
|
||||
const primarySubtreeTag = fiber.subtreeTag & Layout;
|
||||
if (primarySubtreeTag !== NoSubtreeTag) {
|
||||
commitLayoutEffects(fiber.child, root, committedLanes);
|
||||
}
|
||||
}
|
||||
|
||||
resetCurrentDebugFiberInDEV();
|
||||
nextEffect = nextEffect.nextEffect;
|
||||
if (__DEV__) {
|
||||
setCurrentDebugFiberInDEV(fiber);
|
||||
invokeGuardedCallback(
|
||||
null,
|
||||
commitLayoutEffectsImpl,
|
||||
null,
|
||||
fiber,
|
||||
root,
|
||||
committedLanes,
|
||||
);
|
||||
if (hasCaughtError()) {
|
||||
const error = clearCaughtError();
|
||||
captureCommitPhaseError(fiber, error);
|
||||
}
|
||||
resetCurrentDebugFiberInDEV();
|
||||
} else {
|
||||
try {
|
||||
commitLayoutEffectsImpl(fiber, root, committedLanes);
|
||||
} catch (error) {
|
||||
captureCommitPhaseError(fiber, error);
|
||||
}
|
||||
}
|
||||
fiber = fiber.sibling;
|
||||
}
|
||||
}
|
||||
|
||||
function commitLayoutEffectsImpl(
|
||||
fiber: Fiber,
|
||||
root: FiberRoot,
|
||||
committedLanes: Lanes,
|
||||
) {
|
||||
const effectTag = fiber.effectTag;
|
||||
|
||||
setCurrentDebugFiberInDEV(fiber);
|
||||
|
||||
if (effectTag & (Update | Callback)) {
|
||||
const current = fiber.alternate;
|
||||
commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
|
||||
}
|
||||
|
||||
if (enableScopeAPI) {
|
||||
// TODO: This is a temporary solution that allows us to transition away
|
||||
// from React Flare on www.
|
||||
if (effectTag & Ref && fiber.tag !== ScopeComponent) {
|
||||
commitAttachRef(fiber);
|
||||
}
|
||||
} else {
|
||||
if (effectTag & Ref) {
|
||||
commitAttachRef(fiber);
|
||||
}
|
||||
}
|
||||
|
||||
resetCurrentDebugFiberInDEV();
|
||||
}
|
||||
|
||||
export function flushPassiveEffects() {
|
||||
if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {
|
||||
const priorityLevel =
|
||||
@@ -2539,19 +2696,7 @@ function flushPassiveEffectsImpl() {
|
||||
}
|
||||
}
|
||||
|
||||
// Note: This currently assumes there are no passive effects on the root fiber
|
||||
// because the root is not part of its own effect list.
|
||||
// This could change in the future.
|
||||
let effect = root.current.firstEffect;
|
||||
while (effect !== null) {
|
||||
const nextNextEffect = effect.nextEffect;
|
||||
// Remove nextEffect pointer to assist GC
|
||||
effect.nextEffect = null;
|
||||
if (effect.effectTag & Deletion) {
|
||||
detachFiberAfterEffects(effect);
|
||||
}
|
||||
effect = nextNextEffect;
|
||||
}
|
||||
// TODO (effects) Detach sibling pointers for deleted Fibers
|
||||
|
||||
if (enableProfilerTimer && enableProfilerCommitHooks) {
|
||||
const profilerEffects = pendingPassiveProfilerEffects;
|
||||
@@ -3638,7 +3783,3 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function detachFiberAfterEffects(fiber: Fiber): void {
|
||||
fiber.sibling = null;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {SuspenseInstance} from './ReactFiberHostConfig';
|
||||
import type {WorkTag} from './ReactWorkTags';
|
||||
import type {TypeOfMode} from './ReactTypeOfMode';
|
||||
import type {SideEffectTag} from './ReactSideEffectTags';
|
||||
import type {SubtreeTag} from './ReactSubtreeTags';
|
||||
import type {Lane, LanePriority, Lanes, LaneMap} from './ReactFiberLane';
|
||||
import type {HookType} from './ReactFiberHooks.old';
|
||||
import type {RootTag} from './ReactRootTags';
|
||||
@@ -126,6 +127,8 @@ export type Fiber = {|
|
||||
|
||||
// Effect
|
||||
effectTag: SideEffectTag,
|
||||
subtreeTag: SubtreeTag,
|
||||
deletions: Array<Fiber> | null,
|
||||
|
||||
// Singly linked list fast path to the next fiber with side-effects.
|
||||
nextEffect: Fiber | null,
|
||||
|
||||
@@ -38,3 +38,8 @@ export const HostEffectMask = /* */ 0b000011111111111;
|
||||
export const Incomplete = /* */ 0b000100000000000;
|
||||
export const ShouldCapture = /* */ 0b001000000000000;
|
||||
export const ForceUpdateForLegacySuspense = /* */ 0b100000000000000;
|
||||
|
||||
// Union of side effect groupings as pertains to subtreeTag
|
||||
export const BeforeMutationMask = /* */ 0b000001100001010;
|
||||
export const MutationMask = /* */ 0b000010010011110;
|
||||
export const LayoutMask = /* */ 0b000000010100100;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export type SubtreeTag = number;
|
||||
|
||||
export const NoEffect = /* */ 0b000;
|
||||
export const BeforeMutation = /* */ 0b001;
|
||||
export const Mutation = /* */ 0b010;
|
||||
export const Layout = /* */ 0b100;
|
||||
@@ -1743,7 +1743,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
await resolveText('B2');
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'B2',
|
||||
'Destroy Layout Effect [Loading...]',
|
||||
|
||||
Reference in New Issue
Block a user