@samitouri / QOS-React-2 / commits / b641d02097

Use recursion to traverse during layout phase

This converts the layout phase to iterate over its effects recursively instead of iteratively. This makes it easier to track contextual information, like whether a fiber is inside a hidden tree. We already made this change for the mutation phase. See 481dece for more context.

Andrew Clark committed Jul 12, 2022 at 13:25 UTC b641d0209717d1127296d26b53b8dc056d9e6952
2 files changed +186 -230
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+93 -115
@@ -716,6 +716,11 @@ function commitLayoutEffectOnFiber(
716 case FunctionComponent:
717 case ForwardRef:
718 case SimpleMemoComponent: {
719 + recursivelyTraverseLayoutEffects(
720 + finishedRoot,
721 + finishedWork,
722 + committedLanes,
723 + );
724 if (flags & Update) {
725 if (!offscreenSubtreeWasHidden) {
726 // At this point layout effects have already been destroyed (during mutation phase).
@@ -752,6 +757,11 @@ function commitLayoutEffectOnFiber(
757 break;
758 }
759 case ClassComponent: {
760 + recursivelyTraverseLayoutEffects(
761 + finishedRoot,
762 + finishedWork,
763 + committedLanes,
764 + );
765 if (flags & Update) {
766 if (!offscreenSubtreeWasHidden) {
767 const instance = finishedWork.stateNode;
@@ -946,6 +956,11 @@ function commitLayoutEffectOnFiber(
956 break;
957 }
958 case HostRoot: {
959 + recursivelyTraverseLayoutEffects(
960 + finishedRoot,
961 + finishedWork,
962 + committedLanes,
963 + );
964 if (flags & Callback) {
965 // TODO: I think this is now always non-null by the time it reaches the
966 // commit phase. Consider removing the type check.
@@ -974,6 +989,11 @@ function commitLayoutEffectOnFiber(
989 break;
990 }
991 case HostComponent: {
992 + recursivelyTraverseLayoutEffects(
993 + finishedRoot,
994 + finishedWork,
995 + committedLanes,
996 + );
997 if (flags & Update) {
998 const instance: Instance = finishedWork.stateNode;
999
@@ -1003,15 +1023,12 @@ function commitLayoutEffectOnFiber(
1023 }
1024 break;
1025 }
1006 - case HostText: {
1007 - // We have no life-cycles associated with text.
1008 - break;
1009 - }
1010 - case HostPortal: {
1011 - // We have no life-cycles associated with portals.
1012 - break;
1013 - }
1026 case Profiler: {
1027 + recursivelyTraverseLayoutEffects(
1028 + finishedRoot,
1029 + finishedWork,
1030 + committedLanes,
1031 + );
1032 if (enableProfilerTimer) {
1033 if (flags & Update) {
1034 try {
@@ -1078,6 +1095,11 @@ function commitLayoutEffectOnFiber(
1095 break;
1096 }
1097 case SuspenseComponent: {
1098 + recursivelyTraverseLayoutEffects(
1099 + finishedRoot,
1100 + finishedWork,
1101 + committedLanes,
1102 + );
1103 if (flags & Update) {
1104 try {
1105 commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
@@ -1087,20 +1109,58 @@ function commitLayoutEffectOnFiber(
1109 }
1110 break;
1111 }
1090 - case SuspenseListComponent:
1091 - case IncompleteClassComponent:
1092 - case ScopeComponent:
1093 - case OffscreenComponent:
1094 - case LegacyHiddenComponent:
1095 - case TracingMarkerComponent: {
1112 + case OffscreenComponent: {
1113 + const isModernRoot = (finishedWork.mode & ConcurrentMode) !== NoMode;
1114 + if (isModernRoot) {
1115 + const isHidden = finishedWork.memoizedState !== null;
1116 + const newOffscreenSubtreeIsHidden =
1117 + isHidden || offscreenSubtreeIsHidden;
1118 + if (newOffscreenSubtreeIsHidden) {
1119 + // The Offscreen tree is hidden. Skip over its layout effects.
1120 + } else {
1121 + // The Offscreen tree is visible.
1122 +
1123 + const wasHidden = current !== null && current.memoizedState !== null;
1124 + const newOffscreenSubtreeWasHidden =
1125 + wasHidden || offscreenSubtreeWasHidden;
1126 + const prevOffscreenSubtreeIsHidden = offscreenSubtreeIsHidden;
1127 + const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden;
1128 + offscreenSubtreeIsHidden = newOffscreenSubtreeIsHidden;
1129 + offscreenSubtreeWasHidden = newOffscreenSubtreeWasHidden;
1130 +
1131 + if (offscreenSubtreeWasHidden && !prevOffscreenSubtreeWasHidden) {
1132 + // This is the root of a reappearing boundary. Turn its layout
1133 + // effects back on.
1134 + // TODO: Convert this to use recursion
1135 + nextEffect = finishedWork;
1136 + reappearLayoutEffects_begin(finishedWork);
1137 + }
1138 +
1139 + recursivelyTraverseLayoutEffects(
1140 + finishedRoot,
1141 + finishedWork,
1142 + committedLanes,
1143 + );
1144 + offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
1145 + offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
1146 + }
1147 + } else {
1148 + recursivelyTraverseLayoutEffects(
1149 + finishedRoot,
1150 + finishedWork,
1151 + committedLanes,
1152 + );
1153 + }
1154 break;
1155 }
1098 -
1099 - default:
1100 - throw new Error(
1101 - 'This unit of work tag should not have side-effects. This error is ' +
1102 - 'likely caused by a bug in React. Please file an issue.',
1156 + default: {
1157 + recursivelyTraverseLayoutEffects(
1158 + finishedRoot,
1159 + finishedWork,
1160 + committedLanes,
1161 );
1162 + break;
1163 + }
1164 }
1165 }
1166
@@ -2591,112 +2651,30 @@ export function commitLayoutEffects(
2651 ): void {
2652 inProgressLanes = committedLanes;
2653 inProgressRoot = root;
2594 - nextEffect = finishedWork;
2654
2596 - commitLayoutEffects_begin(finishedWork, root, committedLanes);
2655 + const current = finishedWork.alternate;
2656 + commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);
2657
2658 inProgressLanes = null;
2659 inProgressRoot = null;
2660 }
2661
2602 -function commitLayoutEffects_begin(
2603 - subtreeRoot: Fiber,
2662 +function recursivelyTraverseLayoutEffects(
2663 root: FiberRoot,
2605 - committedLanes: Lanes,
2606 -) {
2607 - // Suspense layout effects semantics don't change for legacy roots.
2608 - const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;
2609 -
2610 - while (nextEffect !== null) {
2611 - const fiber = nextEffect;
2612 - const firstChild = fiber.child;
2613 -
2614 - if (fiber.tag === OffscreenComponent && isModernRoot) {
2615 - // Keep track of the current Offscreen stack's state.
2616 - const isHidden = fiber.memoizedState !== null;
2617 - const newOffscreenSubtreeIsHidden = isHidden || offscreenSubtreeIsHidden;
2618 - if (newOffscreenSubtreeIsHidden) {
2619 - // The Offscreen tree is hidden. Skip over its layout effects.
2620 - commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
2621 - continue;
2622 - } else {
2623 - // TODO (Offscreen) Also check: subtreeFlags & LayoutMask
2624 - const current = fiber.alternate;
2625 - const wasHidden = current !== null && current.memoizedState !== null;
2626 - const newOffscreenSubtreeWasHidden =
2627 - wasHidden || offscreenSubtreeWasHidden;
2628 - const prevOffscreenSubtreeIsHidden = offscreenSubtreeIsHidden;
2629 - const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden;
2630 -
2631 - // Traverse the Offscreen subtree with the current Offscreen as the root.
2632 - offscreenSubtreeIsHidden = newOffscreenSubtreeIsHidden;
2633 - offscreenSubtreeWasHidden = newOffscreenSubtreeWasHidden;
2634 -
2635 - if (offscreenSubtreeWasHidden && !prevOffscreenSubtreeWasHidden) {
2636 - // This is the root of a reappearing boundary. Turn its layout effects
2637 - // back on.
2638 - nextEffect = fiber;
2639 - reappearLayoutEffects_begin(fiber);
2640 - }
2641 -
2642 - let child = firstChild;
2643 - while (child !== null) {
2644 - nextEffect = child;
2645 - commitLayoutEffects_begin(
2646 - child, // New root; bubble back up to here and stop.
2647 - root,
2648 - committedLanes,
2649 - );
2650 - child = child.sibling;
2651 - }
2652 -
2653 - // Restore Offscreen state and resume in our-progress traversal.
2654 - nextEffect = fiber;
2655 - offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
2656 - offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
2657 - commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
2658 -
2659 - continue;
2660 - }
2661 - }
2662 -
2663 - if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
2664 - firstChild.return = fiber;
2665 - nextEffect = firstChild;
2666 - } else {
2667 - commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
2668 - }
2669 - }
2670 -}
2671 -
2672 -function commitLayoutMountEffects_complete(
2673 - subtreeRoot: Fiber,
2674 - root: FiberRoot,
2675 - committedLanes: Lanes,
2664 + parentFiber: Fiber,
2665 + lanes: Lanes,
2666 ) {
2677 - while (nextEffect !== null) {
2678 - const fiber = nextEffect;
2679 - if ((fiber.flags & LayoutMask) !== NoFlags) {
2680 - const current = fiber.alternate;
2681 - setCurrentDebugFiberInDEV(fiber);
2682 - commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
2683 - resetCurrentDebugFiberInDEV();
2684 - }
2685 -
2686 - if (fiber === subtreeRoot) {
2687 - nextEffect = null;
2688 - return;
2689 - }
2690 -
2691 - const sibling = fiber.sibling;
2692 - if (sibling !== null) {
2693 - sibling.return = fiber.return;
2694 - nextEffect = sibling;
2695 - return;
2667 + const prevDebugFiber = getCurrentDebugFiberInDEV();
2668 + if (parentFiber.subtreeFlags & LayoutMask) {
2669 + let child = parentFiber.child;
2670 + while (child !== null) {
2671 + setCurrentDebugFiberInDEV(child);
2672 + const current = child.alternate;
2673 + commitLayoutEffectOnFiber(root, current, child, lanes);
2674 + child = child.sibling;
2675 }
2697 -
2698 - nextEffect = fiber.return;
2676 }
2677 + setCurrentDebugFiberInDEV(prevDebugFiber);
2678 }
2679
2680 function disappearLayoutEffects_begin(subtreeRoot: Fiber) {
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+93 -115
@@ -716,6 +716,11 @@ function commitLayoutEffectOnFiber(
716 case FunctionComponent:
717 case ForwardRef:
718 case SimpleMemoComponent: {
719 + recursivelyTraverseLayoutEffects(
720 + finishedRoot,
721 + finishedWork,
722 + committedLanes,
723 + );
724 if (flags & Update) {
725 if (!offscreenSubtreeWasHidden) {
726 // At this point layout effects have already been destroyed (during mutation phase).
@@ -752,6 +757,11 @@ function commitLayoutEffectOnFiber(
757 break;
758 }
759 case ClassComponent: {
760 + recursivelyTraverseLayoutEffects(
761 + finishedRoot,
762 + finishedWork,
763 + committedLanes,
764 + );
765 if (flags & Update) {
766 if (!offscreenSubtreeWasHidden) {
767 const instance = finishedWork.stateNode;
@@ -946,6 +956,11 @@ function commitLayoutEffectOnFiber(
956 break;
957 }
958 case HostRoot: {
959 + recursivelyTraverseLayoutEffects(
960 + finishedRoot,
961 + finishedWork,
962 + committedLanes,
963 + );
964 if (flags & Callback) {
965 // TODO: I think this is now always non-null by the time it reaches the
966 // commit phase. Consider removing the type check.
@@ -974,6 +989,11 @@ function commitLayoutEffectOnFiber(
989 break;
990 }
991 case HostComponent: {
992 + recursivelyTraverseLayoutEffects(
993 + finishedRoot,
994 + finishedWork,
995 + committedLanes,
996 + );
997 if (flags & Update) {
998 const instance: Instance = finishedWork.stateNode;
999
@@ -1003,15 +1023,12 @@ function commitLayoutEffectOnFiber(
1023 }
1024 break;
1025 }
1006 - case HostText: {
1007 - // We have no life-cycles associated with text.
1008 - break;
1009 - }
1010 - case HostPortal: {
1011 - // We have no life-cycles associated with portals.
1012 - break;
1013 - }
1026 case Profiler: {
1027 + recursivelyTraverseLayoutEffects(
1028 + finishedRoot,
1029 + finishedWork,
1030 + committedLanes,
1031 + );
1032 if (enableProfilerTimer) {
1033 if (flags & Update) {
1034 try {
@@ -1078,6 +1095,11 @@ function commitLayoutEffectOnFiber(
1095 break;
1096 }
1097 case SuspenseComponent: {
1098 + recursivelyTraverseLayoutEffects(
1099 + finishedRoot,
1100 + finishedWork,
1101 + committedLanes,
1102 + );
1103 if (flags & Update) {
1104 try {
1105 commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
@@ -1087,20 +1109,58 @@ function commitLayoutEffectOnFiber(
1109 }
1110 break;
1111 }
1090 - case SuspenseListComponent:
1091 - case IncompleteClassComponent:
1092 - case ScopeComponent:
1093 - case OffscreenComponent:
1094 - case LegacyHiddenComponent:
1095 - case TracingMarkerComponent: {
1112 + case OffscreenComponent: {
1113 + const isModernRoot = (finishedWork.mode & ConcurrentMode) !== NoMode;
1114 + if (isModernRoot) {
1115 + const isHidden = finishedWork.memoizedState !== null;
1116 + const newOffscreenSubtreeIsHidden =
1117 + isHidden || offscreenSubtreeIsHidden;
1118 + if (newOffscreenSubtreeIsHidden) {
1119 + // The Offscreen tree is hidden. Skip over its layout effects.
1120 + } else {
1121 + // The Offscreen tree is visible.
1122 +
1123 + const wasHidden = current !== null && current.memoizedState !== null;
1124 + const newOffscreenSubtreeWasHidden =
1125 + wasHidden || offscreenSubtreeWasHidden;
1126 + const prevOffscreenSubtreeIsHidden = offscreenSubtreeIsHidden;
1127 + const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden;
1128 + offscreenSubtreeIsHidden = newOffscreenSubtreeIsHidden;
1129 + offscreenSubtreeWasHidden = newOffscreenSubtreeWasHidden;
1130 +
1131 + if (offscreenSubtreeWasHidden && !prevOffscreenSubtreeWasHidden) {
1132 + // This is the root of a reappearing boundary. Turn its layout
1133 + // effects back on.
1134 + // TODO: Convert this to use recursion
1135 + nextEffect = finishedWork;
1136 + reappearLayoutEffects_begin(finishedWork);
1137 + }
1138 +
1139 + recursivelyTraverseLayoutEffects(
1140 + finishedRoot,
1141 + finishedWork,
1142 + committedLanes,
1143 + );
1144 + offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
1145 + offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
1146 + }
1147 + } else {
1148 + recursivelyTraverseLayoutEffects(
1149 + finishedRoot,
1150 + finishedWork,
1151 + committedLanes,
1152 + );
1153 + }
1154 break;
1155 }
1098 -
1099 - default:
1100 - throw new Error(
1101 - 'This unit of work tag should not have side-effects. This error is ' +
1102 - 'likely caused by a bug in React. Please file an issue.',
1156 + default: {
1157 + recursivelyTraverseLayoutEffects(
1158 + finishedRoot,
1159 + finishedWork,
1160 + committedLanes,
1161 );
1162 + break;
1163 + }
1164 }
1165 }
1166
@@ -2591,112 +2651,30 @@ export function commitLayoutEffects(
2651 ): void {
2652 inProgressLanes = committedLanes;
2653 inProgressRoot = root;
2594 - nextEffect = finishedWork;
2654
2596 - commitLayoutEffects_begin(finishedWork, root, committedLanes);
2655 + const current = finishedWork.alternate;
2656 + commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);
2657
2658 inProgressLanes = null;
2659 inProgressRoot = null;
2660 }
2661
2602 -function commitLayoutEffects_begin(
2603 - subtreeRoot: Fiber,
2662 +function recursivelyTraverseLayoutEffects(
2663 root: FiberRoot,
2605 - committedLanes: Lanes,
2606 -) {
2607 - // Suspense layout effects semantics don't change for legacy roots.
2608 - const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;
2609 -
2610 - while (nextEffect !== null) {
2611 - const fiber = nextEffect;
2612 - const firstChild = fiber.child;
2613 -
2614 - if (fiber.tag === OffscreenComponent && isModernRoot) {
2615 - // Keep track of the current Offscreen stack's state.
2616 - const isHidden = fiber.memoizedState !== null;
2617 - const newOffscreenSubtreeIsHidden = isHidden || offscreenSubtreeIsHidden;
2618 - if (newOffscreenSubtreeIsHidden) {
2619 - // The Offscreen tree is hidden. Skip over its layout effects.
2620 - commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
2621 - continue;
2622 - } else {
2623 - // TODO (Offscreen) Also check: subtreeFlags & LayoutMask
2624 - const current = fiber.alternate;
2625 - const wasHidden = current !== null && current.memoizedState !== null;
2626 - const newOffscreenSubtreeWasHidden =
2627 - wasHidden || offscreenSubtreeWasHidden;
2628 - const prevOffscreenSubtreeIsHidden = offscreenSubtreeIsHidden;
2629 - const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden;
2630 -
2631 - // Traverse the Offscreen subtree with the current Offscreen as the root.
2632 - offscreenSubtreeIsHidden = newOffscreenSubtreeIsHidden;
2633 - offscreenSubtreeWasHidden = newOffscreenSubtreeWasHidden;
2634 -
2635 - if (offscreenSubtreeWasHidden && !prevOffscreenSubtreeWasHidden) {
2636 - // This is the root of a reappearing boundary. Turn its layout effects
2637 - // back on.
2638 - nextEffect = fiber;
2639 - reappearLayoutEffects_begin(fiber);
2640 - }
2641 -
2642 - let child = firstChild;
2643 - while (child !== null) {
2644 - nextEffect = child;
2645 - commitLayoutEffects_begin(
2646 - child, // New root; bubble back up to here and stop.
2647 - root,
2648 - committedLanes,
2649 - );
2650 - child = child.sibling;
2651 - }
2652 -
2653 - // Restore Offscreen state and resume in our-progress traversal.
2654 - nextEffect = fiber;
2655 - offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
2656 - offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
2657 - commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
2658 -
2659 - continue;
2660 - }
2661 - }
2662 -
2663 - if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
2664 - firstChild.return = fiber;
2665 - nextEffect = firstChild;
2666 - } else {
2667 - commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
2668 - }
2669 - }
2670 -}
2671 -
2672 -function commitLayoutMountEffects_complete(
2673 - subtreeRoot: Fiber,
2674 - root: FiberRoot,
2675 - committedLanes: Lanes,
2664 + parentFiber: Fiber,
2665 + lanes: Lanes,
2666 ) {
2677 - while (nextEffect !== null) {
2678 - const fiber = nextEffect;
2679 - if ((fiber.flags & LayoutMask) !== NoFlags) {
2680 - const current = fiber.alternate;
2681 - setCurrentDebugFiberInDEV(fiber);
2682 - commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
2683 - resetCurrentDebugFiberInDEV();
2684 - }
2685 -
2686 - if (fiber === subtreeRoot) {
2687 - nextEffect = null;
2688 - return;
2689 - }
2690 -
2691 - const sibling = fiber.sibling;
2692 - if (sibling !== null) {
2693 - sibling.return = fiber.return;
2694 - nextEffect = sibling;
2695 - return;
2667 + const prevDebugFiber = getCurrentDebugFiberInDEV();
2668 + if (parentFiber.subtreeFlags & LayoutMask) {
2669 + let child = parentFiber.child;
2670 + while (child !== null) {
2671 + setCurrentDebugFiberInDEV(child);
2672 + const current = child.alternate;
2673 + commitLayoutEffectOnFiber(root, current, child, lanes);
2674 + child = child.sibling;
2675 }
2697 -
2698 - nextEffect = fiber.return;
2676 }
2677 + setCurrentDebugFiberInDEV(prevDebugFiber);
2678 }
2679
2680 function disappearLayoutEffects_begin(subtreeRoot: Fiber) {