Skip to content

Commit de75315

Browse files
committedDec 7, 2020
Track deletions using an array on the parent
Adds back the `deletions` array and uses it in the commit phase. We use a trick where the first time we hit a deletion effect, we commit all the deletion effects that belong to that parent. This is an incremental step away from using the effect list and toward a DFS + subtreeFlags traversal. This will help determine whether the regression is caused by, say, pushing the same fiber into the deletions array multiple times.
1 parent 1377e46 commit de75315

9 files changed

+203
-39
lines changed
 

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

+18-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type {Fiber} from './ReactInternalTypes';
1313
import type {Lanes} from './ReactFiberLane.new';
1414

1515
import getComponentName from 'shared/getComponentName';
16-
import {Placement, Deletion} from './ReactFiberFlags';
16+
import {Deletion, ChildDeletion, Placement} from './ReactFiberFlags';
1717
import {
1818
getIteratorFn,
1919
REACT_ELEMENT_TYPE,
@@ -276,6 +276,23 @@ function ChildReconciler(shouldTrackSideEffects) {
276276
}
277277
childToDelete.nextEffect = null;
278278
childToDelete.flags = Deletion;
279+
280+
let deletions = returnFiber.deletions;
281+
if (deletions === null) {
282+
deletions = returnFiber.deletions = [childToDelete];
283+
returnFiber.flags |= ChildDeletion;
284+
} else {
285+
deletions.push(childToDelete);
286+
}
287+
// Stash a reference to the return fiber's deletion array on each of the
288+
// deleted children. This is really weird, but it's a temporary workaround
289+
// while we're still using the effect list to traverse effect fibers. A
290+
// better workaround would be to follow the `.return` pointer in the commit
291+
// phase, but unfortunately we can't assume that `.return` points to the
292+
// correct fiber, even in the commit phase, because `findDOMNode` might
293+
// mutate it.
294+
// TODO: Remove this line.
295+
childToDelete.deletions = deletions;
279296
}
280297

281298
function deleteRemainingChildren(

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

+19-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type {Fiber} from './ReactInternalTypes';
1313
import type {Lanes} from './ReactFiberLane.old';
1414

1515
import getComponentName from 'shared/getComponentName';
16-
import {Placement, Deletion} from './ReactFiberFlags';
16+
import {Deletion, ChildDeletion, Placement} from './ReactFiberFlags';
1717
import {
1818
getIteratorFn,
1919
REACT_ELEMENT_TYPE,
@@ -276,6 +276,23 @@ function ChildReconciler(shouldTrackSideEffects) {
276276
}
277277
childToDelete.nextEffect = null;
278278
childToDelete.flags = Deletion;
279+
280+
let deletions = returnFiber.deletions;
281+
if (deletions === null) {
282+
deletions = returnFiber.deletions = [childToDelete];
283+
returnFiber.flags |= ChildDeletion;
284+
} else {
285+
deletions.push(childToDelete);
286+
}
287+
// Stash a reference to the return fiber's deletion array on each of the
288+
// deleted children. This is really weird, but it's a temporary workaround
289+
// while we're still using the effect list to traverse effect fibers. A
290+
// better workaround would be to follow the `.return` pointer in the commit
291+
// phase, but unfortunately we can't assume that `.return` points to the
292+
// correct fiber, even in the commit phase, because `findDOMNode` might
293+
// mutate it.
294+
// TODO: Remove this line.
295+
childToDelete.deletions = deletions;
279296
}
280297

281298
function deleteRemainingChildren(
@@ -1125,6 +1142,7 @@ function ChildReconciler(shouldTrackSideEffects) {
11251142
} else {
11261143
if (
11271144
child.elementType === elementType ||
1145+
// Keep this check inline so it only runs on the false path:
11281146
(__DEV__
11291147
? isCompatibleFamilyForHotReloading(child, element)
11301148
: false) ||

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

+34-14
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {
6161
Update,
6262
Ref,
6363
Deletion,
64+
ChildDeletion,
6465
ForceUpdateForLegacySuspense,
6566
} from './ReactFiberFlags';
6667
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -2007,6 +2008,14 @@ function updateSuspensePrimaryChildren(
20072008
currentFallbackChildFragment.nextEffect = null;
20082009
currentFallbackChildFragment.flags = Deletion;
20092010
workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChildFragment;
2011+
let deletions = workInProgress.deletions;
2012+
if (deletions === null) {
2013+
deletions = workInProgress.deletions = [currentFallbackChildFragment];
2014+
workInProgress.flags |= ChildDeletion;
2015+
} else {
2016+
deletions.push(currentFallbackChildFragment);
2017+
}
2018+
currentFallbackChildFragment.deletions = deletions;
20102019
}
20112020

20122021
workInProgress.child = primaryChildFragment;
@@ -2061,21 +2070,23 @@ function updateSuspenseFallbackChildren(
20612070
currentPrimaryChildFragment.treeBaseDuration;
20622071
}
20632072

2064-
// The fallback fiber was added as a deletion effect during the first pass.
2065-
// However, since we're going to remain on the fallback, we no longer want
2066-
// to delete it. So we need to remove it from the list. Deletions are stored
2067-
// on the same list as effects. We want to keep the effects from the primary
2068-
// tree. So we copy the primary child fragment's effect list, which does not
2069-
// include the fallback deletion effect.
2070-
const progressedLastEffect = primaryChildFragment.lastEffect;
2071-
if (progressedLastEffect !== null) {
2072-
workInProgress.firstEffect = primaryChildFragment.firstEffect;
2073-
workInProgress.lastEffect = progressedLastEffect;
2074-
progressedLastEffect.nextEffect = null;
2075-
} else {
2076-
// TODO: Reset this somewhere else? Lol legacy mode is so weird.
2077-
workInProgress.firstEffect = workInProgress.lastEffect = null;
2073+
if (currentFallbackChildFragment !== null) {
2074+
// The fallback fiber was added as a deletion effect during the first
2075+
// pass. However, since we're going to remain on the fallback, we no
2076+
// longer want to delete it. So we need to remove it from the list.
2077+
// Deletions are stored on the same list as effects, and are always added
2078+
// to the front. So we know that the first effect must be the fallback
2079+
// deletion effect, and everything after that is from the primary free.
2080+
const firstPrimaryTreeEffect = currentFallbackChildFragment.nextEffect;
2081+
if (firstPrimaryTreeEffect !== null) {
2082+
workInProgress.firstEffect = firstPrimaryTreeEffect;
2083+
} else {
2084+
// TODO: Reset this somewhere else? Lol legacy mode is so weird.
2085+
workInProgress.firstEffect = workInProgress.lastEffect = null;
2086+
}
20782087
}
2088+
2089+
workInProgress.deletions = null;
20792090
} else {
20802091
primaryChildFragment = createWorkInProgressOffscreenFiber(
20812092
currentPrimaryChildFragment,
@@ -2982,6 +2993,15 @@ function remountFiber(
29822993
current.nextEffect = null;
29832994
current.flags = Deletion;
29842995

2996+
let deletions = returnFiber.deletions;
2997+
if (deletions === null) {
2998+
deletions = returnFiber.deletions = [current];
2999+
returnFiber.flags |= ChildDeletion;
3000+
} else {
3001+
deletions.push(current);
3002+
}
3003+
current.deletions = deletions;
3004+
29853005
newWorkInProgress.flags |= Placement;
29863006

29873007
// Restart work from the new fiber.

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

+34-14
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {
6161
Update,
6262
Ref,
6363
Deletion,
64+
ChildDeletion,
6465
ForceUpdateForLegacySuspense,
6566
} from './ReactFiberFlags';
6667
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -2007,6 +2008,14 @@ function updateSuspensePrimaryChildren(
20072008
currentFallbackChildFragment.nextEffect = null;
20082009
currentFallbackChildFragment.flags = Deletion;
20092010
workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChildFragment;
2011+
let deletions = workInProgress.deletions;
2012+
if (deletions === null) {
2013+
deletions = workInProgress.deletions = [currentFallbackChildFragment];
2014+
workInProgress.flags |= ChildDeletion;
2015+
} else {
2016+
deletions.push(currentFallbackChildFragment);
2017+
}
2018+
currentFallbackChildFragment.deletions = deletions;
20102019
}
20112020

20122021
workInProgress.child = primaryChildFragment;
@@ -2061,21 +2070,23 @@ function updateSuspenseFallbackChildren(
20612070
currentPrimaryChildFragment.treeBaseDuration;
20622071
}
20632072

2064-
// The fallback fiber was added as a deletion effect during the first pass.
2065-
// However, since we're going to remain on the fallback, we no longer want
2066-
// to delete it. So we need to remove it from the list. Deletions are stored
2067-
// on the same list as effects. We want to keep the effects from the primary
2068-
// tree. So we copy the primary child fragment's effect list, which does not
2069-
// include the fallback deletion effect.
2070-
const progressedLastEffect = primaryChildFragment.lastEffect;
2071-
if (progressedLastEffect !== null) {
2072-
workInProgress.firstEffect = primaryChildFragment.firstEffect;
2073-
workInProgress.lastEffect = progressedLastEffect;
2074-
progressedLastEffect.nextEffect = null;
2075-
} else {
2076-
// TODO: Reset this somewhere else? Lol legacy mode is so weird.
2077-
workInProgress.firstEffect = workInProgress.lastEffect = null;
2073+
if (currentFallbackChildFragment !== null) {
2074+
// The fallback fiber was added as a deletion effect during the first
2075+
// pass. However, since we're going to remain on the fallback, we no
2076+
// longer want to delete it. So we need to remove it from the list.
2077+
// Deletions are stored on the same list as effects, and are always added
2078+
// to the front. So we know that the first effect must be the fallback
2079+
// deletion effect, and everything after that is from the primary free.
2080+
const firstPrimaryTreeEffect = currentFallbackChildFragment.nextEffect;
2081+
if (firstPrimaryTreeEffect !== null) {
2082+
workInProgress.firstEffect = firstPrimaryTreeEffect;
2083+
} else {
2084+
// TODO: Reset this somewhere else? Lol legacy mode is so weird.
2085+
workInProgress.firstEffect = workInProgress.lastEffect = null;
2086+
}
20782087
}
2088+
2089+
workInProgress.deletions = null;
20792090
} else {
20802091
primaryChildFragment = createWorkInProgressOffscreenFiber(
20812092
currentPrimaryChildFragment,
@@ -2982,6 +2993,15 @@ function remountFiber(
29822993
current.nextEffect = null;
29832994
current.flags = Deletion;
29842995

2996+
let deletions = returnFiber.deletions;
2997+
if (deletions === null) {
2998+
deletions = returnFiber.deletions = [current];
2999+
returnFiber.flags |= ChildDeletion;
3000+
} else {
3001+
deletions.push(current);
3002+
}
3003+
current.deletions = deletions;
3004+
29853005
newWorkInProgress.flags |= Placement;
29863006

29873007
// Restart work from the new fiber.

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

-3
Original file line numberDiff line numberDiff line change
@@ -1864,9 +1864,6 @@ const HooksDispatcherOnMount: Dispatcher = {
18641864

18651865
unstable_isNewReconciler: enableNewReconciler,
18661866
};
1867-
if (enableCache) {
1868-
(HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
1869-
}
18701867

18711868
const HooksDispatcherOnUpdate: Dispatcher = {
18721869
readContext,

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
HostRoot,
2525
SuspenseComponent,
2626
} from './ReactWorkTags';
27-
import {Deletion, Placement, Hydrating} from './ReactFiberFlags';
27+
import {Deletion, ChildDeletion, Placement, Hydrating} from './ReactFiberFlags';
2828
import invariant from 'shared/invariant';
2929

3030
import {
@@ -137,6 +137,15 @@ function deleteHydratableInstance(
137137
} else {
138138
returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
139139
}
140+
141+
let deletions = returnFiber.deletions;
142+
if (deletions === null) {
143+
deletions = returnFiber.deletions = [childToDelete];
144+
returnFiber.flags |= ChildDeletion;
145+
} else {
146+
deletions.push(childToDelete);
147+
}
148+
childToDelete.deletions = deletions;
140149
}
141150

142151
function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
HostRoot,
2525
SuspenseComponent,
2626
} from './ReactWorkTags';
27-
import {Deletion, Placement, Hydrating} from './ReactFiberFlags';
27+
import {Deletion, ChildDeletion, Placement, Hydrating} from './ReactFiberFlags';
2828
import invariant from 'shared/invariant';
2929

3030
import {
@@ -137,6 +137,15 @@ function deleteHydratableInstance(
137137
} else {
138138
returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
139139
}
140+
141+
let deletions = returnFiber.deletions;
142+
if (deletions === null) {
143+
deletions = returnFiber.deletions = [childToDelete];
144+
returnFiber.flags |= ChildDeletion;
145+
} else {
146+
deletions.push(childToDelete);
147+
}
148+
childToDelete.deletions = deletions;
140149
}
141150

142151
function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {

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

+31-2
Original file line numberDiff line numberDiff line change
@@ -1821,6 +1821,7 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
18211821
// Mark the parent fiber as incomplete and clear its effect list.
18221822
returnFiber.firstEffect = returnFiber.lastEffect = null;
18231823
returnFiber.flags |= Incomplete;
1824+
returnFiber.deletions = null;
18241825
}
18251826
}
18261827

@@ -2384,7 +2385,7 @@ function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
23842385
// bitmap value, we remove the secondary effects from the effect tag and
23852386
// switch on that value.
23862387
const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
2387-
switch (primaryFlags) {
2388+
outer: switch (primaryFlags) {
23882389
case Placement: {
23892390
commitPlacement(nextEffect);
23902391
// Clear the "placement" from effect tag so that we know that this is
@@ -2424,7 +2425,35 @@ function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
24242425
break;
24252426
}
24262427
case Deletion: {
2427-
commitDeletion(root, nextEffect, renderPriorityLevel);
2428+
// Reached a deletion effect. Instead of commit this effect like we
2429+
// normally do, we're going to use the `deletions` array of the parent.
2430+
// However, because the effect list is sorted in depth-first order, we
2431+
// can't wait until we reach the parent node, because the child effects
2432+
// will have run in the meantime.
2433+
//
2434+
// So instead, we use a trick where the first time we hit a deletion
2435+
// effect, we commit all the deletion effects that belong to that parent.
2436+
//
2437+
// This is an incremental step away from using the effect list and
2438+
// toward a DFS + subtreeFlags traversal.
2439+
//
2440+
// A reference to the deletion array of the parent is also stored on
2441+
// each of the deletions. This is really weird. It would be better to
2442+
// follow the `.return` pointer, but unfortunately we can't assume that
2443+
// `.return` points to the correct fiber, even in the commit phase,
2444+
// because `findDOMNode` might mutate it.
2445+
const deletedChild = nextEffect;
2446+
const deletions = deletedChild.deletions;
2447+
if (deletions !== null) {
2448+
for (let i = 0; i < deletions.length; i++) {
2449+
const deletion = deletions[i];
2450+
// Clear the deletion effect so that we don't delete this node more
2451+
// than once.
2452+
deletion.flags &= ~Deletion;
2453+
deletion.deletions = null;
2454+
commitDeletion(root, deletion, renderPriorityLevel);
2455+
}
2456+
}
24282457
break;
24292458
}
24302459
}

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

+47-2
Original file line numberDiff line numberDiff line change
@@ -1821,6 +1821,7 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
18211821
// Mark the parent fiber as incomplete and clear its effect list.
18221822
returnFiber.firstEffect = returnFiber.lastEffect = null;
18231823
returnFiber.flags |= Incomplete;
1824+
returnFiber.deletions = null;
18241825
}
18251826
}
18261827

@@ -2384,7 +2385,7 @@ function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
23842385
// bitmap value, we remove the secondary effects from the effect tag and
23852386
// switch on that value.
23862387
const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
2387-
switch (primaryFlags) {
2388+
outer: switch (primaryFlags) {
23882389
case Placement: {
23892390
commitPlacement(nextEffect);
23902391
// Clear the "placement" from effect tag so that we know that this is
@@ -2424,7 +2425,35 @@ function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
24242425
break;
24252426
}
24262427
case Deletion: {
2427-
commitDeletion(root, nextEffect, renderPriorityLevel);
2428+
// Reached a deletion effect. Instead of commit this effect like we
2429+
// normally do, we're going to use the `deletions` array of the parent.
2430+
// However, because the effect list is sorted in depth-first order, we
2431+
// can't wait until we reach the parent node, because the child effects
2432+
// will have run in the meantime.
2433+
//
2434+
// So instead, we use a trick where the first time we hit a deletion
2435+
// effect, we commit all the deletion effects that belong to that parent.
2436+
//
2437+
// This is an incremental step away from using the effect list and
2438+
// toward a DFS + subtreeFlags traversal.
2439+
//
2440+
// A reference to the deletion array of the parent is also stored on
2441+
// each of the deletions. This is really weird. It would be better to
2442+
// follow the `.return` pointer, but unfortunately we can't assume that
2443+
// `.return` points to the correct fiber, even in the commit phase,
2444+
// because `findDOMNode` might mutate it.
2445+
const deletedChild = nextEffect;
2446+
const deletions = deletedChild.deletions;
2447+
if (deletions !== null) {
2448+
for (let i = 0; i < deletions.length; i++) {
2449+
const deletion = deletions[i];
2450+
// Clear the deletion effect so that we don't delete this node more
2451+
// than once.
2452+
deletion.flags &= ~Deletion;
2453+
deletion.deletions = null;
2454+
commitDeletion(root, deletion, renderPriorityLevel);
2455+
}
2456+
}
24282457
break;
24292458
}
24302459
}
@@ -2844,6 +2873,22 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
28442873
}
28452874
fiber = fiber.return;
28462875
}
2876+
2877+
if (__DEV__) {
2878+
// TODO: Until we re-land skipUnmountedBoundaries (see #20147), this warning
2879+
// will fire for errors that are thrown by destroy functions inside deleted
2880+
// trees. What it should instead do is propagate the error to the parent of
2881+
// the deleted tree. In the meantime, do not add this warning to the
2882+
// allowlist; this is only for our internal use.
2883+
console.error(
2884+
'Internal React error: Attempted to capture a commit phase error ' +
2885+
'inside a detached tree. This indicates a bug in React. Likely ' +
2886+
'causes include deleting the same fiber more than once, committing an ' +
2887+
'already-finished tree, or an inconsistent return pointer.\n\n' +
2888+
'Error message:\n\n%s',
2889+
error,
2890+
);
2891+
}
28472892
}
28482893

28492894
export function pingSuspendedRoot(

0 commit comments

Comments
 (0)
Please sign in to comment.