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:
Brian Vaughn
2020-07-16 09:10:00 -04:00
committed by GitHub
co-authored by Luna
parent fed4ae0247
commit a226b9b445
11 changed files with 488 additions and 272 deletions
@@ -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;
}
+3
View File
@@ -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,
+5
View File
@@ -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;
+15
View File
@@ -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...]',