Combine deletion phase into single recursive function
Similar to the previous step, this converts the deletion phase into a single recursive function. Although there's less code, this one is a bit trickier because it's already contains some stack-like logic for tracking the nearest host parent. But instead of using the actual stack, it repeatedly searches up the fiber return path to find the nearest host parent. Instead, I've changed it to track the nearest host parent on the JS stack. (We still search up the return path once, to set the initial host parent right before entering a deleted tree. As a follow up, we can instead push this to the stack as we traverse during the main mutation phase.)
Andrew Clark committed
Apr 6, 2022 at 23:25 UTC
46db4e996d686ffbc8ec4cc7d559d6897288b780
2 files changed
+550
-538
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+275
-269
@@ -1222,148 +1222,6 @@ function commitDetachRef(current: Fiber) {
1222
}
1223
}
1224
1225
-// User-originating errors (lifecycles and refs) should not interrupt
1226
-// deletion, so don't let them throw. Host-originating errors should
1227
-// interrupt deletion, so it's okay
1228
-function commitUnmount(
1229
- finishedRoot: FiberRoot,
1230
- current: Fiber,
1231
- nearestMountedAncestor: Fiber,
1232
-): void {
1233
- onCommitUnmount(current);
1234
-
1235
- switch (current.tag) {
1236
- case FunctionComponent:
1237
- case ForwardRef:
1238
- case MemoComponent:
1239
- case SimpleMemoComponent: {
1240
- const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
1241
- if (updateQueue !== null) {
1242
- const lastEffect = updateQueue.lastEffect;
1243
- if (lastEffect !== null) {
1244
- const firstEffect = lastEffect.next;
1245
-
1246
- let effect = firstEffect;
1247
- do {
1248
- const {destroy, tag} = effect;
1249
- if (destroy !== undefined) {
1250
- if ((tag & HookInsertion) !== NoHookEffect) {
1251
- safelyCallDestroy(current, nearestMountedAncestor, destroy);
1252
- } else if ((tag & HookLayout) !== NoHookEffect) {
1253
- if (enableSchedulingProfiler) {
1254
- markComponentLayoutEffectUnmountStarted(current);
1255
- }
1256
-
1257
- if (
1258
- enableProfilerTimer &&
1259
- enableProfilerCommitHooks &&
1260
- current.mode & ProfileMode
1261
- ) {
1262
- startLayoutEffectTimer();
1263
- safelyCallDestroy(current, nearestMountedAncestor, destroy);
1264
- recordLayoutEffectDuration(current);
1265
- } else {
1266
- safelyCallDestroy(current, nearestMountedAncestor, destroy);
1267
- }
1268
-
1269
- if (enableSchedulingProfiler) {
1270
- markComponentLayoutEffectUnmountStopped();
1271
- }
1272
- }
1273
- }
1274
- effect = effect.next;
1275
- } while (effect !== firstEffect);
1276
- }
1277
- }
1278
- return;
1279
- }
1280
- case ClassComponent: {
1281
- safelyDetachRef(current, nearestMountedAncestor);
1282
- const instance = current.stateNode;
1283
- if (typeof instance.componentWillUnmount === 'function') {
1284
- safelyCallComponentWillUnmount(
1285
- current,
1286
- nearestMountedAncestor,
1287
- instance,
1288
- );
1289
- }
1290
- return;
1291
- }
1292
- case HostComponent: {
1293
- safelyDetachRef(current, nearestMountedAncestor);
1294
- return;
1295
- }
1296
- case HostPortal: {
1297
- // TODO: this is recursive.
1298
- // We are also not using this parent because
1299
- // the portal will get pushed immediately.
1300
- if (supportsMutation) {
1301
- unmountHostComponents(finishedRoot, current, nearestMountedAncestor);
1302
- } else if (supportsPersistence) {
1303
- emptyPortalContainer(current);
1304
- }
1305
- return;
1306
- }
1307
- case DehydratedFragment: {
1308
- if (enableSuspenseCallback) {
1309
- const hydrationCallbacks = finishedRoot.hydrationCallbacks;
1310
- if (hydrationCallbacks !== null) {
1311
- const onDeleted = hydrationCallbacks.onDeleted;
1312
- if (onDeleted) {
1313
- onDeleted((current.stateNode: SuspenseInstance));
1314
- }
1315
- }
1316
- }
1317
- return;
1318
- }
1319
- case ScopeComponent: {
1320
- if (enableScopeAPI) {
1321
- safelyDetachRef(current, nearestMountedAncestor);
1322
- }
1323
- return;
1324
- }
1325
- }
1326
-}
1327
-
1328
-function commitNestedUnmounts(
1329
- finishedRoot: FiberRoot,
1330
- root: Fiber,
1331
- nearestMountedAncestor: Fiber,
1332
-): void {
1333
- // While we're inside a removed host node we don't want to call
1334
- // removeChild on the inner nodes because they're removed by the top
1335
- // call anyway. We also want to call componentWillUnmount on all
1336
- // composites before this host node is removed from the tree. Therefore
1337
- // we do an inner loop while we're still inside the host node.
1338
- let node: Fiber = root;
1339
- while (true) {
1340
- commitUnmount(finishedRoot, node, nearestMountedAncestor);
1341
- // Visit children because they may contain more composite or host nodes.
1342
- // Skip portals because commitUnmount() currently visits them recursively.
1343
- if (
1344
- node.child !== null &&
1345
- // If we use mutation we drill down into portals using commitUnmount above.
1346
- // If we don't use mutation we drill down into portals here instead.
1347
- (!supportsMutation || node.tag !== HostPortal)
1348
- ) {
1349
- node.child.return = node;
1350
- node = node.child;
1351
- continue;
1352
- }
1353
- if (node === root) {
1354
- return;
1355
- }
1356
- while (node.sibling === null) {
1357
- if (node.return === null || node.return === root) {
1358
- return;
1359
- }
1360
- node = node.return;
1361
- }
1362
- node.sibling.return = node.return;
1363
- node = node.sibling;
1364
- }
1365
-}
1366
-
1225
function detachFiberMutation(fiber: Fiber) {
1226
// Cut off the return pointer to disconnect it from the tree.
1227
// This enables us to detect and warn against state updates on an unmounted component.
@@ -1652,152 +1510,302 @@ function insertOrAppendPlacementNode(
1510
}
1511
}
1512
1655
-function unmountHostComponents(
1656
- finishedRoot: FiberRoot,
1657
- current: Fiber,
1658
- nearestMountedAncestor: Fiber,
1659
-): void {
1660
- // We only have the top Fiber that was deleted but we need to recurse down its
1661
- // children to find all the terminal nodes.
1662
- let node: Fiber = current;
1513
+// These are tracked on the stack as we recursively traverse a
1514
+// deleted subtree.
1515
+// TODO: Update these during the whole mutation phase, not just during
1516
+// a deletion.
1517
+let hostParent: Instance | Container | null = null;
1518
+let hostParentIsContainer: boolean = false;
1519
1664
- // Each iteration, currentParent is populated with node's host parent if not
1665
- // currentParentIsValid.
1666
- let currentParentIsValid = false;
1520
+function commitDeletionEffects(
1521
+ root: FiberRoot,
1522
+ returnFiber: Fiber,
1523
+ deletedFiber: Fiber,
1524
+) {
1525
+ if (supportsMutation) {
1526
+ // We only have the top Fiber that was deleted but we need to recurse down its
1527
+ // children to find all the terminal nodes.
1528
1668
- // Note: these two variables *must* always be updated together.
1669
- let currentParent;
1670
- let currentParentIsContainer;
1671
-
1672
- while (true) {
1673
- if (!currentParentIsValid) {
1674
- let parent = node.return;
1675
- findParent: while (true) {
1676
- if (parent === null) {
1677
- throw new Error(
1678
- 'Expected to find a host parent. This error is likely caused by ' +
1679
- 'a bug in React. Please file an issue.',
1680
- );
1681
- }
1529
+ // Recursively delete all host nodes from the parent, detach refs, clean
1530
+ // up mounted layout effects, and call componentWillUnmount.
1531
+
1532
+ // We only need to remove the topmost host child in each branch. But then we
1533
+ // still need to keep traversing to unmount effects, refs, and cWU. TODO: We
1534
+ // could split this into two separate traversals functions, where the second
1535
+ // one doesn't include any removeChild logic. This is maybe the same
1536
+ // function as "disappearLayoutEffects" (or whatever that turns into after
1537
+ // the layout phase is refactored to use recursion).
1538
+
1539
+ // Before starting, find the nearest host parent on the stack so we know
1540
+ // which instance/container to remove the children from.
1541
+ // TODO: Instead of searching up the fiber return path on every deletion, we
1542
+ // can track the nearest host component on the JS stack as we traverse the
1543
+ // tree during the commit phase. This would make insertions faster, too.
1544
+ let parent = returnFiber;
1545
+ findParent: while (parent !== null) {
1546
+ switch (parent.tag) {
1547
+ case HostComponent: {
1548
+ hostParent = parent.stateNode;
1549
+ hostParentIsContainer = false;
1550
+ break findParent;
1551
+ }
1552
+ case HostRoot: {
1553
+ hostParent = parent.stateNode.containerInfo;
1554
+ hostParentIsContainer = true;
1555
+ break findParent;
1556
+ }
1557
+ case HostPortal: {
1558
+ hostParent = parent.stateNode.containerInfo;
1559
+ hostParentIsContainer = true;
1560
+ break findParent;
1561
+ }
1562
+ }
1563
+ parent = parent.return;
1564
+ }
1565
+ if (hostParent === null) {
1566
+ throw new Error(
1567
+ 'Expected to find a host parent. This error is likely caused by ' +
1568
+ 'a bug in React. Please file an issue.',
1569
+ );
1570
+ }
1571
+ commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
1572
+ hostParent = null;
1573
+ hostParentIsContainer = false;
1574
+ } else {
1575
+ // Detach refs and call componentWillUnmount() on the whole subtree.
1576
+ commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
1577
+ }
1578
1683
- const parentStateNode = parent.stateNode;
1684
- switch (parent.tag) {
1685
- case HostComponent:
1686
- currentParent = parentStateNode;
1687
- currentParentIsContainer = false;
1688
- break findParent;
1689
- case HostRoot:
1690
- currentParent = parentStateNode.containerInfo;
1691
- currentParentIsContainer = true;
1692
- break findParent;
1693
- case HostPortal:
1694
- currentParent = parentStateNode.containerInfo;
1695
- currentParentIsContainer = true;
1696
- break findParent;
1697
- }
1698
- parent = parent.return;
1699
- }
1700
- currentParentIsValid = true;
1701
- }
1702
-
1703
- if (node.tag === HostComponent || node.tag === HostText) {
1704
- commitNestedUnmounts(finishedRoot, node, nearestMountedAncestor);
1705
- // After all the children have unmounted, it is now safe to remove the
1706
- // node from the tree.
1707
- if (currentParentIsContainer) {
1708
- removeChildFromContainer(
1709
- ((currentParent: any): Container),
1710
- (node.stateNode: Instance | TextInstance),
1579
+ detachFiberMutation(deletedFiber);
1580
+}
1581
+
1582
+function recursivelyTraverseDeletionEffects(
1583
+ finishedRoot,
1584
+ nearestMountedAncestor,
1585
+ parent,
1586
+) {
1587
+ // TODO: Use a static flag to skip trees that don't have unmount effects
1588
+ let child = parent.child;
1589
+ while (child !== null) {
1590
+ commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, child);
1591
+ child = child.sibling;
1592
+ }
1593
+}
1594
+
1595
+function commitDeletionEffectsOnFiber(
1596
+ finishedRoot: FiberRoot,
1597
+ nearestMountedAncestor: Fiber,
1598
+ deletedFiber: Fiber,
1599
+) {
1600
+ onCommitUnmount(deletedFiber);
1601
+
1602
+ // The cases in this outer switch modify the stack before they traverse
1603
+ // into their subtree. There are simpler cases in the inner switch
1604
+ // that don't modify the stack.
1605
+ switch (deletedFiber.tag) {
1606
+ case HostComponent: {
1607
+ safelyDetachRef(deletedFiber, nearestMountedAncestor);
1608
+ // Intentional fallthrough to next branch
1609
+ }
1610
+ // eslint-disable-next-line-no-fallthrough
1611
+ case HostText: {
1612
+ // We only need to remove the nearest host child. Set the host parent
1613
+ // to `null` on the stack to indicate that nested children don't
1614
+ // need to be removed.
1615
+ if (supportsMutation) {
1616
+ const prevHostParent = hostParent;
1617
+ const prevHostParentIsContainer = hostParentIsContainer;
1618
+ hostParent = null;
1619
+ recursivelyTraverseDeletionEffects(
1620
+ finishedRoot,
1621
+ nearestMountedAncestor,
1622
+ deletedFiber,
1623
);
1624
+ hostParent = prevHostParent;
1625
+ hostParentIsContainer = prevHostParentIsContainer;
1626
+
1627
+ if (hostParent !== null) {
1628
+ // Now that all the child effects have unmounted, we can remove the
1629
+ // node from the tree.
1630
+ if (hostParentIsContainer) {
1631
+ removeChildFromContainer(
1632
+ ((hostParent: any): Container),
1633
+ (deletedFiber.stateNode: Instance | TextInstance),
1634
+ );
1635
+ } else {
1636
+ removeChild(
1637
+ ((hostParent: any): Instance),
1638
+ (deletedFiber.stateNode: Instance | TextInstance),
1639
+ );
1640
+ }
1641
+ }
1642
} else {
1713
- removeChild(
1714
- ((currentParent: any): Instance),
1715
- (node.stateNode: Instance | TextInstance),
1643
+ recursivelyTraverseDeletionEffects(
1644
+ finishedRoot,
1645
+ nearestMountedAncestor,
1646
+ deletedFiber,
1647
);
1648
}
1718
- // Don't visit children because we already visited them.
1719
- } else if (
1720
- enableSuspenseServerRenderer &&
1721
- node.tag === DehydratedFragment
1722
- ) {
1723
- if (enableSuspenseCallback) {
1724
- const hydrationCallbacks = finishedRoot.hydrationCallbacks;
1725
- if (hydrationCallbacks !== null) {
1726
- const onDeleted = hydrationCallbacks.onDeleted;
1727
- if (onDeleted) {
1728
- onDeleted((node.stateNode: SuspenseInstance));
1649
+ return;
1650
+ }
1651
+ case DehydratedFragment: {
1652
+ if (enableSuspenseServerRenderer) {
1653
+ if (enableSuspenseCallback) {
1654
+ const hydrationCallbacks = finishedRoot.hydrationCallbacks;
1655
+ if (hydrationCallbacks !== null) {
1656
+ const onDeleted = hydrationCallbacks.onDeleted;
1657
+ if (onDeleted) {
1658
+ onDeleted((deletedFiber.stateNode: SuspenseInstance));
1659
+ }
1660
}
1661
}
1731
- }
1662
1733
- // Delete the dehydrated suspense boundary and all of its content.
1734
- if (currentParentIsContainer) {
1735
- clearSuspenseBoundaryFromContainer(
1736
- ((currentParent: any): Container),
1737
- (node.stateNode: SuspenseInstance),
1663
+ // Dehydrated fragments don't have any children
1664
+
1665
+ // Delete the dehydrated suspense boundary and all of its content.
1666
+ if (supportsMutation) {
1667
+ if (hostParent !== null) {
1668
+ if (hostParentIsContainer) {
1669
+ clearSuspenseBoundaryFromContainer(
1670
+ ((hostParent: any): Container),
1671
+ (deletedFiber.stateNode: SuspenseInstance),
1672
+ );
1673
+ } else {
1674
+ clearSuspenseBoundary(
1675
+ ((hostParent: any): Instance),
1676
+ (deletedFiber.stateNode: SuspenseInstance),
1677
+ );
1678
+ }
1679
+ }
1680
+ }
1681
+ }
1682
+ return;
1683
+ }
1684
+ case HostPortal: {
1685
+ if (supportsMutation) {
1686
+ // When we go into a portal, it becomes the parent to remove from.
1687
+ const prevHostParent = hostParent;
1688
+ const prevHostParentIsContainer = hostParentIsContainer;
1689
+ hostParent = deletedFiber.stateNode.containerInfo;
1690
+ hostParentIsContainer = true;
1691
+ recursivelyTraverseDeletionEffects(
1692
+ finishedRoot,
1693
+ nearestMountedAncestor,
1694
+ deletedFiber,
1695
);
1696
+ hostParent = prevHostParent;
1697
+ hostParentIsContainer = prevHostParentIsContainer;
1698
} else {
1740
- clearSuspenseBoundary(
1741
- ((currentParent: any): Instance),
1742
- (node.stateNode: SuspenseInstance),
1699
+ emptyPortalContainer(deletedFiber);
1700
+
1701
+ recursivelyTraverseDeletionEffects(
1702
+ finishedRoot,
1703
+ nearestMountedAncestor,
1704
+ deletedFiber,
1705
);
1706
}
1745
- } else if (node.tag === HostPortal) {
1746
- if (node.child !== null) {
1747
- // When we go into a portal, it becomes the parent to remove from.
1748
- // We will reassign it back when we pop the portal on the way up.
1749
- currentParent = node.stateNode.containerInfo;
1750
- currentParentIsContainer = true;
1751
- // Visit children because portals might contain host components.
1752
- node.child.return = node;
1753
- node = node.child;
1754
- continue;
1755
- }
1756
- } else {
1757
- commitUnmount(finishedRoot, node, nearestMountedAncestor);
1758
- // Visit children because we may find more host components below.
1759
- if (node.child !== null) {
1760
- node.child.return = node;
1761
- node = node.child;
1762
- continue;
1763
- }
1707
+ return;
1708
}
1765
- if (node === current) {
1709
+ case FunctionComponent:
1710
+ case ForwardRef:
1711
+ case MemoComponent:
1712
+ case SimpleMemoComponent: {
1713
+ const updateQueue: FunctionComponentUpdateQueue | null = (deletedFiber.updateQueue: any);
1714
+ if (updateQueue !== null) {
1715
+ const lastEffect = updateQueue.lastEffect;
1716
+ if (lastEffect !== null) {
1717
+ const firstEffect = lastEffect.next;
1718
+
1719
+ let effect = firstEffect;
1720
+ do {
1721
+ const {destroy, tag} = effect;
1722
+ if (destroy !== undefined) {
1723
+ if ((tag & HookInsertion) !== NoHookEffect) {
1724
+ safelyCallDestroy(
1725
+ deletedFiber,
1726
+ nearestMountedAncestor,
1727
+ destroy,
1728
+ );
1729
+ } else if ((tag & HookLayout) !== NoHookEffect) {
1730
+ if (enableSchedulingProfiler) {
1731
+ markComponentLayoutEffectUnmountStarted(deletedFiber);
1732
+ }
1733
+
1734
+ if (
1735
+ enableProfilerTimer &&
1736
+ enableProfilerCommitHooks &&
1737
+ deletedFiber.mode & ProfileMode
1738
+ ) {
1739
+ startLayoutEffectTimer();
1740
+ safelyCallDestroy(
1741
+ deletedFiber,
1742
+ nearestMountedAncestor,
1743
+ destroy,
1744
+ );
1745
+ recordLayoutEffectDuration(deletedFiber);
1746
+ } else {
1747
+ safelyCallDestroy(
1748
+ deletedFiber,
1749
+ nearestMountedAncestor,
1750
+ destroy,
1751
+ );
1752
+ }
1753
+
1754
+ if (enableSchedulingProfiler) {
1755
+ markComponentLayoutEffectUnmountStopped();
1756
+ }
1757
+ }
1758
+ }
1759
+ effect = effect.next;
1760
+ } while (effect !== firstEffect);
1761
+ }
1762
+ }
1763
+
1764
+ recursivelyTraverseDeletionEffects(
1765
+ finishedRoot,
1766
+ nearestMountedAncestor,
1767
+ deletedFiber,
1768
+ );
1769
return;
1770
}
1768
- while (node.sibling === null) {
1769
- if (node.return === null || node.return === current) {
1770
- return;
1771
+ case ClassComponent: {
1772
+ safelyDetachRef(deletedFiber, nearestMountedAncestor);
1773
+ const instance = deletedFiber.stateNode;
1774
+ if (typeof instance.componentWillUnmount === 'function') {
1775
+ safelyCallComponentWillUnmount(
1776
+ deletedFiber,
1777
+ nearestMountedAncestor,
1778
+ instance,
1779
+ );
1780
}
1772
- node = node.return;
1773
- if (node.tag === HostPortal) {
1774
- // When we go out of the portal, we need to restore the parent.
1775
- // Since we don't keep a stack of them, we will search for it.
1776
- currentParentIsValid = false;
1781
+ recursivelyTraverseDeletionEffects(
1782
+ finishedRoot,
1783
+ nearestMountedAncestor,
1784
+ deletedFiber,
1785
+ );
1786
+ return;
1787
+ }
1788
+ case ScopeComponent: {
1789
+ if (enableScopeAPI) {
1790
+ safelyDetachRef(deletedFiber, nearestMountedAncestor);
1791
}
1792
+ recursivelyTraverseDeletionEffects(
1793
+ finishedRoot,
1794
+ nearestMountedAncestor,
1795
+ deletedFiber,
1796
+ );
1797
+ return;
1798
+ }
1799
+ default: {
1800
+ recursivelyTraverseDeletionEffects(
1801
+ finishedRoot,
1802
+ nearestMountedAncestor,
1803
+ deletedFiber,
1804
+ );
1805
+ return;
1806
}
1779
- node.sibling.return = node.return;
1780
- node = node.sibling;
1781
- }
1782
-}
1783
-
1784
-function commitDeletion(
1785
- finishedRoot: FiberRoot,
1786
- current: Fiber,
1787
- nearestMountedAncestor: Fiber,
1788
-): void {
1789
- if (supportsMutation) {
1790
- // Recursively delete all host nodes from the parent.
1791
- // Detach refs and call componentWillUnmount() on the whole subtree.
1792
- unmountHostComponents(finishedRoot, current, nearestMountedAncestor);
1793
- } else {
1794
- // Detach refs and call componentWillUnmount() on the whole subtree.
1795
- commitNestedUnmounts(finishedRoot, current, nearestMountedAncestor);
1807
}
1797
-
1798
- detachFiberMutation(current);
1808
}
1800
-
1809
function commitSuspenseCallback(finishedWork: Fiber) {
1810
// TODO: Move this to passive phase
1811
const newState: SuspenseState | null = finishedWork.memoizedState;
@@ -1907,7 +1915,6 @@ export function commitMutationEffects(
1915
) {
1916
inProgressLanes = committedLanes;
1917
inProgressRoot = root;
1910
- nextEffect = finishedWork;
1918
1919
setCurrentDebugFiberInDEV(finishedWork);
1920
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
@@ -1929,7 +1936,7 @@ function recursivelyTraverseMutationEffects(
1936
for (let i = 0; i < deletions.length; i++) {
1937
const childToDelete = deletions[i];
1938
try {
1932
- commitDeletion(root, childToDelete, parentFiber);
1939
+ commitDeletionEffects(root, parentFiber, childToDelete);
1940
} catch (error) {
1941
captureCommitPhaseError(childToDelete, parentFiber, error);
1942
}
@@ -3086,7 +3093,6 @@ function invokePassiveEffectUnmountInDEV(fiber: Fiber): void {
3093
3094
export {
3095
commitPlacement,
3089
- commitDeletion,
3096
commitAttachRef,
3097
commitDetachRef,
3098
invokeLayoutEffectMountInDEV,
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+275
-269
@@ -1222,148 +1222,6 @@ function commitDetachRef(current: Fiber) {
1222
}
1223
}
1224
1225
-// User-originating errors (lifecycles and refs) should not interrupt
1226
-// deletion, so don't let them throw. Host-originating errors should
1227
-// interrupt deletion, so it's okay
1228
-function commitUnmount(
1229
- finishedRoot: FiberRoot,
1230
- current: Fiber,
1231
- nearestMountedAncestor: Fiber,
1232
-): void {
1233
- onCommitUnmount(current);
1234
-
1235
- switch (current.tag) {
1236
- case FunctionComponent:
1237
- case ForwardRef:
1238
- case MemoComponent:
1239
- case SimpleMemoComponent: {
1240
- const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
1241
- if (updateQueue !== null) {
1242
- const lastEffect = updateQueue.lastEffect;
1243
- if (lastEffect !== null) {
1244
- const firstEffect = lastEffect.next;
1245
-
1246
- let effect = firstEffect;
1247
- do {
1248
- const {destroy, tag} = effect;
1249
- if (destroy !== undefined) {
1250
- if ((tag & HookInsertion) !== NoHookEffect) {
1251
- safelyCallDestroy(current, nearestMountedAncestor, destroy);
1252
- } else if ((tag & HookLayout) !== NoHookEffect) {
1253
- if (enableSchedulingProfiler) {
1254
- markComponentLayoutEffectUnmountStarted(current);
1255
- }
1256
-
1257
- if (
1258
- enableProfilerTimer &&
1259
- enableProfilerCommitHooks &&
1260
- current.mode & ProfileMode
1261
- ) {
1262
- startLayoutEffectTimer();
1263
- safelyCallDestroy(current, nearestMountedAncestor, destroy);
1264
- recordLayoutEffectDuration(current);
1265
- } else {
1266
- safelyCallDestroy(current, nearestMountedAncestor, destroy);
1267
- }
1268
-
1269
- if (enableSchedulingProfiler) {
1270
- markComponentLayoutEffectUnmountStopped();
1271
- }
1272
- }
1273
- }
1274
- effect = effect.next;
1275
- } while (effect !== firstEffect);
1276
- }
1277
- }
1278
- return;
1279
- }
1280
- case ClassComponent: {
1281
- safelyDetachRef(current, nearestMountedAncestor);
1282
- const instance = current.stateNode;
1283
- if (typeof instance.componentWillUnmount === 'function') {
1284
- safelyCallComponentWillUnmount(
1285
- current,
1286
- nearestMountedAncestor,
1287
- instance,
1288
- );
1289
- }
1290
- return;
1291
- }
1292
- case HostComponent: {
1293
- safelyDetachRef(current, nearestMountedAncestor);
1294
- return;
1295
- }
1296
- case HostPortal: {
1297
- // TODO: this is recursive.
1298
- // We are also not using this parent because
1299
- // the portal will get pushed immediately.
1300
- if (supportsMutation) {
1301
- unmountHostComponents(finishedRoot, current, nearestMountedAncestor);
1302
- } else if (supportsPersistence) {
1303
- emptyPortalContainer(current);
1304
- }
1305
- return;
1306
- }
1307
- case DehydratedFragment: {
1308
- if (enableSuspenseCallback) {
1309
- const hydrationCallbacks = finishedRoot.hydrationCallbacks;
1310
- if (hydrationCallbacks !== null) {
1311
- const onDeleted = hydrationCallbacks.onDeleted;
1312
- if (onDeleted) {
1313
- onDeleted((current.stateNode: SuspenseInstance));
1314
- }
1315
- }
1316
- }
1317
- return;
1318
- }
1319
- case ScopeComponent: {
1320
- if (enableScopeAPI) {
1321
- safelyDetachRef(current, nearestMountedAncestor);
1322
- }
1323
- return;
1324
- }
1325
- }
1326
-}
1327
-
1328
-function commitNestedUnmounts(
1329
- finishedRoot: FiberRoot,
1330
- root: Fiber,
1331
- nearestMountedAncestor: Fiber,
1332
-): void {
1333
- // While we're inside a removed host node we don't want to call
1334
- // removeChild on the inner nodes because they're removed by the top
1335
- // call anyway. We also want to call componentWillUnmount on all
1336
- // composites before this host node is removed from the tree. Therefore
1337
- // we do an inner loop while we're still inside the host node.
1338
- let node: Fiber = root;
1339
- while (true) {
1340
- commitUnmount(finishedRoot, node, nearestMountedAncestor);
1341
- // Visit children because they may contain more composite or host nodes.
1342
- // Skip portals because commitUnmount() currently visits them recursively.
1343
- if (
1344
- node.child !== null &&
1345
- // If we use mutation we drill down into portals using commitUnmount above.
1346
- // If we don't use mutation we drill down into portals here instead.
1347
- (!supportsMutation || node.tag !== HostPortal)
1348
- ) {
1349
- node.child.return = node;
1350
- node = node.child;
1351
- continue;
1352
- }
1353
- if (node === root) {
1354
- return;
1355
- }
1356
- while (node.sibling === null) {
1357
- if (node.return === null || node.return === root) {
1358
- return;
1359
- }
1360
- node = node.return;
1361
- }
1362
- node.sibling.return = node.return;
1363
- node = node.sibling;
1364
- }
1365
-}
1366
-
1225
function detachFiberMutation(fiber: Fiber) {
1226
// Cut off the return pointer to disconnect it from the tree.
1227
// This enables us to detect and warn against state updates on an unmounted component.
@@ -1652,152 +1510,302 @@ function insertOrAppendPlacementNode(
1510
}
1511
}
1512
1655
-function unmountHostComponents(
1656
- finishedRoot: FiberRoot,
1657
- current: Fiber,
1658
- nearestMountedAncestor: Fiber,
1659
-): void {
1660
- // We only have the top Fiber that was deleted but we need to recurse down its
1661
- // children to find all the terminal nodes.
1662
- let node: Fiber = current;
1513
+// These are tracked on the stack as we recursively traverse a
1514
+// deleted subtree.
1515
+// TODO: Update these during the whole mutation phase, not just during
1516
+// a deletion.
1517
+let hostParent: Instance | Container | null = null;
1518
+let hostParentIsContainer: boolean = false;
1519
1664
- // Each iteration, currentParent is populated with node's host parent if not
1665
- // currentParentIsValid.
1666
- let currentParentIsValid = false;
1520
+function commitDeletionEffects(
1521
+ root: FiberRoot,
1522
+ returnFiber: Fiber,
1523
+ deletedFiber: Fiber,
1524
+) {
1525
+ if (supportsMutation) {
1526
+ // We only have the top Fiber that was deleted but we need to recurse down its
1527
+ // children to find all the terminal nodes.
1528
1668
- // Note: these two variables *must* always be updated together.
1669
- let currentParent;
1670
- let currentParentIsContainer;
1671
-
1672
- while (true) {
1673
- if (!currentParentIsValid) {
1674
- let parent = node.return;
1675
- findParent: while (true) {
1676
- if (parent === null) {
1677
- throw new Error(
1678
- 'Expected to find a host parent. This error is likely caused by ' +
1679
- 'a bug in React. Please file an issue.',
1680
- );
1681
- }
1529
+ // Recursively delete all host nodes from the parent, detach refs, clean
1530
+ // up mounted layout effects, and call componentWillUnmount.
1531
+
1532
+ // We only need to remove the topmost host child in each branch. But then we
1533
+ // still need to keep traversing to unmount effects, refs, and cWU. TODO: We
1534
+ // could split this into two separate traversals functions, where the second
1535
+ // one doesn't include any removeChild logic. This is maybe the same
1536
+ // function as "disappearLayoutEffects" (or whatever that turns into after
1537
+ // the layout phase is refactored to use recursion).
1538
+
1539
+ // Before starting, find the nearest host parent on the stack so we know
1540
+ // which instance/container to remove the children from.
1541
+ // TODO: Instead of searching up the fiber return path on every deletion, we
1542
+ // can track the nearest host component on the JS stack as we traverse the
1543
+ // tree during the commit phase. This would make insertions faster, too.
1544
+ let parent = returnFiber;
1545
+ findParent: while (parent !== null) {
1546
+ switch (parent.tag) {
1547
+ case HostComponent: {
1548
+ hostParent = parent.stateNode;
1549
+ hostParentIsContainer = false;
1550
+ break findParent;
1551
+ }
1552
+ case HostRoot: {
1553
+ hostParent = parent.stateNode.containerInfo;
1554
+ hostParentIsContainer = true;
1555
+ break findParent;
1556
+ }
1557
+ case HostPortal: {
1558
+ hostParent = parent.stateNode.containerInfo;
1559
+ hostParentIsContainer = true;
1560
+ break findParent;
1561
+ }
1562
+ }
1563
+ parent = parent.return;
1564
+ }
1565
+ if (hostParent === null) {
1566
+ throw new Error(
1567
+ 'Expected to find a host parent. This error is likely caused by ' +
1568
+ 'a bug in React. Please file an issue.',
1569
+ );
1570
+ }
1571
+ commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
1572
+ hostParent = null;
1573
+ hostParentIsContainer = false;
1574
+ } else {
1575
+ // Detach refs and call componentWillUnmount() on the whole subtree.
1576
+ commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
1577
+ }
1578
1683
- const parentStateNode = parent.stateNode;
1684
- switch (parent.tag) {
1685
- case HostComponent:
1686
- currentParent = parentStateNode;
1687
- currentParentIsContainer = false;
1688
- break findParent;
1689
- case HostRoot:
1690
- currentParent = parentStateNode.containerInfo;
1691
- currentParentIsContainer = true;
1692
- break findParent;
1693
- case HostPortal:
1694
- currentParent = parentStateNode.containerInfo;
1695
- currentParentIsContainer = true;
1696
- break findParent;
1697
- }
1698
- parent = parent.return;
1699
- }
1700
- currentParentIsValid = true;
1701
- }
1702
-
1703
- if (node.tag === HostComponent || node.tag === HostText) {
1704
- commitNestedUnmounts(finishedRoot, node, nearestMountedAncestor);
1705
- // After all the children have unmounted, it is now safe to remove the
1706
- // node from the tree.
1707
- if (currentParentIsContainer) {
1708
- removeChildFromContainer(
1709
- ((currentParent: any): Container),
1710
- (node.stateNode: Instance | TextInstance),
1579
+ detachFiberMutation(deletedFiber);
1580
+}
1581
+
1582
+function recursivelyTraverseDeletionEffects(
1583
+ finishedRoot,
1584
+ nearestMountedAncestor,
1585
+ parent,
1586
+) {
1587
+ // TODO: Use a static flag to skip trees that don't have unmount effects
1588
+ let child = parent.child;
1589
+ while (child !== null) {
1590
+ commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, child);
1591
+ child = child.sibling;
1592
+ }
1593
+}
1594
+
1595
+function commitDeletionEffectsOnFiber(
1596
+ finishedRoot: FiberRoot,
1597
+ nearestMountedAncestor: Fiber,
1598
+ deletedFiber: Fiber,
1599
+) {
1600
+ onCommitUnmount(deletedFiber);
1601
+
1602
+ // The cases in this outer switch modify the stack before they traverse
1603
+ // into their subtree. There are simpler cases in the inner switch
1604
+ // that don't modify the stack.
1605
+ switch (deletedFiber.tag) {
1606
+ case HostComponent: {
1607
+ safelyDetachRef(deletedFiber, nearestMountedAncestor);
1608
+ // Intentional fallthrough to next branch
1609
+ }
1610
+ // eslint-disable-next-line-no-fallthrough
1611
+ case HostText: {
1612
+ // We only need to remove the nearest host child. Set the host parent
1613
+ // to `null` on the stack to indicate that nested children don't
1614
+ // need to be removed.
1615
+ if (supportsMutation) {
1616
+ const prevHostParent = hostParent;
1617
+ const prevHostParentIsContainer = hostParentIsContainer;
1618
+ hostParent = null;
1619
+ recursivelyTraverseDeletionEffects(
1620
+ finishedRoot,
1621
+ nearestMountedAncestor,
1622
+ deletedFiber,
1623
);
1624
+ hostParent = prevHostParent;
1625
+ hostParentIsContainer = prevHostParentIsContainer;
1626
+
1627
+ if (hostParent !== null) {
1628
+ // Now that all the child effects have unmounted, we can remove the
1629
+ // node from the tree.
1630
+ if (hostParentIsContainer) {
1631
+ removeChildFromContainer(
1632
+ ((hostParent: any): Container),
1633
+ (deletedFiber.stateNode: Instance | TextInstance),
1634
+ );
1635
+ } else {
1636
+ removeChild(
1637
+ ((hostParent: any): Instance),
1638
+ (deletedFiber.stateNode: Instance | TextInstance),
1639
+ );
1640
+ }
1641
+ }
1642
} else {
1713
- removeChild(
1714
- ((currentParent: any): Instance),
1715
- (node.stateNode: Instance | TextInstance),
1643
+ recursivelyTraverseDeletionEffects(
1644
+ finishedRoot,
1645
+ nearestMountedAncestor,
1646
+ deletedFiber,
1647
);
1648
}
1718
- // Don't visit children because we already visited them.
1719
- } else if (
1720
- enableSuspenseServerRenderer &&
1721
- node.tag === DehydratedFragment
1722
- ) {
1723
- if (enableSuspenseCallback) {
1724
- const hydrationCallbacks = finishedRoot.hydrationCallbacks;
1725
- if (hydrationCallbacks !== null) {
1726
- const onDeleted = hydrationCallbacks.onDeleted;
1727
- if (onDeleted) {
1728
- onDeleted((node.stateNode: SuspenseInstance));
1649
+ return;
1650
+ }
1651
+ case DehydratedFragment: {
1652
+ if (enableSuspenseServerRenderer) {
1653
+ if (enableSuspenseCallback) {
1654
+ const hydrationCallbacks = finishedRoot.hydrationCallbacks;
1655
+ if (hydrationCallbacks !== null) {
1656
+ const onDeleted = hydrationCallbacks.onDeleted;
1657
+ if (onDeleted) {
1658
+ onDeleted((deletedFiber.stateNode: SuspenseInstance));
1659
+ }
1660
}
1661
}
1731
- }
1662
1733
- // Delete the dehydrated suspense boundary and all of its content.
1734
- if (currentParentIsContainer) {
1735
- clearSuspenseBoundaryFromContainer(
1736
- ((currentParent: any): Container),
1737
- (node.stateNode: SuspenseInstance),
1663
+ // Dehydrated fragments don't have any children
1664
+
1665
+ // Delete the dehydrated suspense boundary and all of its content.
1666
+ if (supportsMutation) {
1667
+ if (hostParent !== null) {
1668
+ if (hostParentIsContainer) {
1669
+ clearSuspenseBoundaryFromContainer(
1670
+ ((hostParent: any): Container),
1671
+ (deletedFiber.stateNode: SuspenseInstance),
1672
+ );
1673
+ } else {
1674
+ clearSuspenseBoundary(
1675
+ ((hostParent: any): Instance),
1676
+ (deletedFiber.stateNode: SuspenseInstance),
1677
+ );
1678
+ }
1679
+ }
1680
+ }
1681
+ }
1682
+ return;
1683
+ }
1684
+ case HostPortal: {
1685
+ if (supportsMutation) {
1686
+ // When we go into a portal, it becomes the parent to remove from.
1687
+ const prevHostParent = hostParent;
1688
+ const prevHostParentIsContainer = hostParentIsContainer;
1689
+ hostParent = deletedFiber.stateNode.containerInfo;
1690
+ hostParentIsContainer = true;
1691
+ recursivelyTraverseDeletionEffects(
1692
+ finishedRoot,
1693
+ nearestMountedAncestor,
1694
+ deletedFiber,
1695
);
1696
+ hostParent = prevHostParent;
1697
+ hostParentIsContainer = prevHostParentIsContainer;
1698
} else {
1740
- clearSuspenseBoundary(
1741
- ((currentParent: any): Instance),
1742
- (node.stateNode: SuspenseInstance),
1699
+ emptyPortalContainer(deletedFiber);
1700
+
1701
+ recursivelyTraverseDeletionEffects(
1702
+ finishedRoot,
1703
+ nearestMountedAncestor,
1704
+ deletedFiber,
1705
);
1706
}
1745
- } else if (node.tag === HostPortal) {
1746
- if (node.child !== null) {
1747
- // When we go into a portal, it becomes the parent to remove from.
1748
- // We will reassign it back when we pop the portal on the way up.
1749
- currentParent = node.stateNode.containerInfo;
1750
- currentParentIsContainer = true;
1751
- // Visit children because portals might contain host components.
1752
- node.child.return = node;
1753
- node = node.child;
1754
- continue;
1755
- }
1756
- } else {
1757
- commitUnmount(finishedRoot, node, nearestMountedAncestor);
1758
- // Visit children because we may find more host components below.
1759
- if (node.child !== null) {
1760
- node.child.return = node;
1761
- node = node.child;
1762
- continue;
1763
- }
1707
+ return;
1708
}
1765
- if (node === current) {
1709
+ case FunctionComponent:
1710
+ case ForwardRef:
1711
+ case MemoComponent:
1712
+ case SimpleMemoComponent: {
1713
+ const updateQueue: FunctionComponentUpdateQueue | null = (deletedFiber.updateQueue: any);
1714
+ if (updateQueue !== null) {
1715
+ const lastEffect = updateQueue.lastEffect;
1716
+ if (lastEffect !== null) {
1717
+ const firstEffect = lastEffect.next;
1718
+
1719
+ let effect = firstEffect;
1720
+ do {
1721
+ const {destroy, tag} = effect;
1722
+ if (destroy !== undefined) {
1723
+ if ((tag & HookInsertion) !== NoHookEffect) {
1724
+ safelyCallDestroy(
1725
+ deletedFiber,
1726
+ nearestMountedAncestor,
1727
+ destroy,
1728
+ );
1729
+ } else if ((tag & HookLayout) !== NoHookEffect) {
1730
+ if (enableSchedulingProfiler) {
1731
+ markComponentLayoutEffectUnmountStarted(deletedFiber);
1732
+ }
1733
+
1734
+ if (
1735
+ enableProfilerTimer &&
1736
+ enableProfilerCommitHooks &&
1737
+ deletedFiber.mode & ProfileMode
1738
+ ) {
1739
+ startLayoutEffectTimer();
1740
+ safelyCallDestroy(
1741
+ deletedFiber,
1742
+ nearestMountedAncestor,
1743
+ destroy,
1744
+ );
1745
+ recordLayoutEffectDuration(deletedFiber);
1746
+ } else {
1747
+ safelyCallDestroy(
1748
+ deletedFiber,
1749
+ nearestMountedAncestor,
1750
+ destroy,
1751
+ );
1752
+ }
1753
+
1754
+ if (enableSchedulingProfiler) {
1755
+ markComponentLayoutEffectUnmountStopped();
1756
+ }
1757
+ }
1758
+ }
1759
+ effect = effect.next;
1760
+ } while (effect !== firstEffect);
1761
+ }
1762
+ }
1763
+
1764
+ recursivelyTraverseDeletionEffects(
1765
+ finishedRoot,
1766
+ nearestMountedAncestor,
1767
+ deletedFiber,
1768
+ );
1769
return;
1770
}
1768
- while (node.sibling === null) {
1769
- if (node.return === null || node.return === current) {
1770
- return;
1771
+ case ClassComponent: {
1772
+ safelyDetachRef(deletedFiber, nearestMountedAncestor);
1773
+ const instance = deletedFiber.stateNode;
1774
+ if (typeof instance.componentWillUnmount === 'function') {
1775
+ safelyCallComponentWillUnmount(
1776
+ deletedFiber,
1777
+ nearestMountedAncestor,
1778
+ instance,
1779
+ );
1780
}
1772
- node = node.return;
1773
- if (node.tag === HostPortal) {
1774
- // When we go out of the portal, we need to restore the parent.
1775
- // Since we don't keep a stack of them, we will search for it.
1776
- currentParentIsValid = false;
1781
+ recursivelyTraverseDeletionEffects(
1782
+ finishedRoot,
1783
+ nearestMountedAncestor,
1784
+ deletedFiber,
1785
+ );
1786
+ return;
1787
+ }
1788
+ case ScopeComponent: {
1789
+ if (enableScopeAPI) {
1790
+ safelyDetachRef(deletedFiber, nearestMountedAncestor);
1791
}
1792
+ recursivelyTraverseDeletionEffects(
1793
+ finishedRoot,
1794
+ nearestMountedAncestor,
1795
+ deletedFiber,
1796
+ );
1797
+ return;
1798
+ }
1799
+ default: {
1800
+ recursivelyTraverseDeletionEffects(
1801
+ finishedRoot,
1802
+ nearestMountedAncestor,
1803
+ deletedFiber,
1804
+ );
1805
+ return;
1806
}
1779
- node.sibling.return = node.return;
1780
- node = node.sibling;
1781
- }
1782
-}
1783
-
1784
-function commitDeletion(
1785
- finishedRoot: FiberRoot,
1786
- current: Fiber,
1787
- nearestMountedAncestor: Fiber,
1788
-): void {
1789
- if (supportsMutation) {
1790
- // Recursively delete all host nodes from the parent.
1791
- // Detach refs and call componentWillUnmount() on the whole subtree.
1792
- unmountHostComponents(finishedRoot, current, nearestMountedAncestor);
1793
- } else {
1794
- // Detach refs and call componentWillUnmount() on the whole subtree.
1795
- commitNestedUnmounts(finishedRoot, current, nearestMountedAncestor);
1807
}
1797
-
1798
- detachFiberMutation(current);
1808
}
1800
-
1809
function commitSuspenseCallback(finishedWork: Fiber) {
1810
// TODO: Move this to passive phase
1811
const newState: SuspenseState | null = finishedWork.memoizedState;
@@ -1907,7 +1915,6 @@ export function commitMutationEffects(
1915
) {
1916
inProgressLanes = committedLanes;
1917
inProgressRoot = root;
1910
- nextEffect = finishedWork;
1918
1919
setCurrentDebugFiberInDEV(finishedWork);
1920
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
@@ -1929,7 +1936,7 @@ function recursivelyTraverseMutationEffects(
1936
for (let i = 0; i < deletions.length; i++) {
1937
const childToDelete = deletions[i];
1938
try {
1932
- commitDeletion(root, childToDelete, parentFiber);
1939
+ commitDeletionEffects(root, parentFiber, childToDelete);
1940
} catch (error) {
1941
captureCommitPhaseError(childToDelete, parentFiber, error);
1942
}
@@ -3086,7 +3093,6 @@ function invokePassiveEffectUnmountInDEV(fiber: Fiber): void {
3093
3094
export {
3095
commitPlacement,
3089
- commitDeletion,
3096
commitAttachRef,
3097
commitDetachRef,
3098
invokeLayoutEffectMountInDEV,