Rewrite the Suspense logic
Dan committed
Apr 5, 2019 at 21:48 UTC
0f536bba5c4354838d4dc1f3fdf226d1b0e67ee9
3 files changed
+210
-101
src/backend/renderer.js
+158
-97
@@ -22,6 +22,7 @@ import {
22
TREE_OPERATION_ADD,
23
TREE_OPERATION_REMOVE,
24
TREE_OPERATION_RESET_CHILDREN,
25
+ TREE_OPERATION_RECURSIVE_REMOVE_CHILDREN,
26
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
27
} from '../constants';
28
import { getUID } from '../utils';
@@ -627,7 +628,7 @@ export function attach(
628
pendingOperations = new Uint32Array(0);
629
}
630
630
- function enqueueMount(fiber: Fiber, parentFiber: Fiber | null) {
631
+ function recordMount(fiber: Fiber, parentFiber: Fiber | null) {
632
const isRoot = fiber.tag === HostRoot;
633
const id = getFiberID(getPrimaryFiber(fiber));
634
@@ -713,7 +714,7 @@ export function attach(
714
}
715
}
716
716
- function enqueueUnmount(fiber) {
717
+ function recordUnmount(fiber) {
718
const isRoot = fiber.tag === HostRoot;
719
const primaryFiber = getPrimaryFiber(fiber);
720
if (!fiberToIDMap.has(primaryFiber)) {
@@ -753,18 +754,27 @@ export function attach(
754
}
755
}
756
756
- function mountFiber(
757
+ function recordRecursiveRemoveChildren(fiber) {
758
+ const primaryFiber = getPrimaryFiber(fiber);
759
+ const id = getFiberID(primaryFiber);
760
+ const operation = new Uint32Array(2);
761
+ operation[0] = TREE_OPERATION_RECURSIVE_REMOVE_CHILDREN;
762
+ operation[1] = id;
763
+ addOperation(operation, false);
764
+ }
765
+
766
+ function mountFiberRecursively(
767
fiber: Fiber,
768
parentFiber: Fiber | null,
769
traverseSiblings = false
770
) {
771
if (__DEBUG__) {
762
- debug('mountFiber()', fiber, parentFiber);
772
+ debug('mountFiberRecursively()', fiber, parentFiber);
773
}
774
765
- const shouldEnqueueMount = !shouldFilterFiber(fiber);
766
- if (shouldEnqueueMount) {
767
- enqueueMount(fiber, parentFiber);
775
+ const shouldInclude = !shouldFilterFiber(fiber);
776
+ if (shouldInclude) {
777
+ recordMount(fiber, parentFiber);
778
}
779
780
const isTimedOutSuspense =
@@ -779,25 +789,45 @@ export function attach(
789
const fallbackChildFragment = primaryChildFragment.sibling;
790
const fallbackChild = fallbackChildFragment.child;
791
if (fallbackChild !== null) {
782
- mountFiber(fallbackChild, shouldEnqueueMount ? fiber : parentFiber, true);
792
+ mountFiberRecursively(
793
+ fallbackChild,
794
+ shouldInclude ? fiber : parentFiber,
795
+ true
796
+ );
797
}
798
} else {
799
if (fiber.child !== null) {
786
- mountFiber(fiber.child, shouldEnqueueMount ? fiber : parentFiber, true);
800
+ mountFiberRecursively(
801
+ fiber.child,
802
+ shouldInclude ? fiber : parentFiber,
803
+ true
804
+ );
805
}
806
}
807
808
if (traverseSiblings && fiber.sibling !== null) {
791
- mountFiber(fiber.sibling, parentFiber, true);
809
+ mountFiberRecursively(fiber.sibling, parentFiber, true);
810
}
811
}
812
795
- function enqueueUpdateIfNecessary(
796
- fiber: Fiber,
797
- hasChildOrderChanged: boolean
798
- ) {
813
+ function unmountFiberRecursively(fiber, traverseSiblings = false) {
814
if (__DEBUG__) {
800
- debug('enqueueUpdateIfNecessary()', fiber);
815
+ debug('unmountFiberRecursively()', fiber, traverseSiblings);
816
+ }
817
+ if (!shouldFilterFiber(fiber)) {
818
+ recordUnmount(fiber);
819
+ }
820
+ if (fiber.child !== null) {
821
+ unmountFiberRecursively(fiber.child, true);
822
+ }
823
+ if (traverseSiblings && fiber.sibling !== null) {
824
+ unmountFiberRecursively(fiber.sibling, true);
825
+ }
826
+ }
827
+
828
+ function maybeRecordUpdate(fiber: Fiber, hasChildOrderChanged: boolean) {
829
+ if (__DEBUG__) {
830
+ debug('maybeRecordUpdate()', fiber);
831
}
832
833
const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
@@ -844,7 +874,7 @@ export function attach(
874
// We might want to revisit this if it proves to be too inefficient.
875
let child = fiber.child;
876
while (child !== null) {
847
- findReorderedChildren(child, nextChildren);
877
+ findReorderedChildrenRecursively(child, nextChildren);
878
child = child.sibling;
879
}
880
@@ -858,109 +888,140 @@ export function attach(
888
}
889
}
890
861
- function findReorderedChildren(fiber: Fiber, nextChildren: Array<number>) {
891
+ function findReorderedChildrenRecursively(
892
+ fiber: Fiber,
893
+ nextChildren: Array<number>
894
+ ) {
895
if (!shouldFilterFiber(fiber)) {
896
nextChildren.push(getFiberID(getPrimaryFiber(fiber)));
897
} else {
898
let child = fiber.child;
899
while (child !== null) {
867
- findReorderedChildren(child, nextChildren);
900
+ findReorderedChildrenRecursively(child, nextChildren);
901
child = child.sibling;
902
}
903
}
904
}
905
873
- function updateFiber(
906
+ function updateFiberRecursively(
907
nextFiber: Fiber,
908
prevFiber: Fiber,
909
parentFiber: Fiber | null
910
) {
911
if (__DEBUG__) {
879
- debug('enqueueUpdateIfNecessary()', nextFiber, parentFiber);
912
+ debug('updateFiberRecursively()', nextFiber, parentFiber);
913
}
914
882
- const shouldEnqueueUpdate = !shouldFilterFiber(nextFiber);
883
-
884
- // Suspense components only have a non-null memoizedState if they're timed-out.
885
- const isTimedOutSuspense =
886
- nextFiber.tag === SuspenseComponent && nextFiber.memoizedState !== null;
887
-
888
- if (isTimedOutSuspense) {
889
- // The behavior of timed-out Suspense trees is unique.
890
- // Rather than unmount the timed out content (and possibly lose important state),
891
- // React re-parents this content within a hidden Fragment while the fallback is showing.
892
- // This behavior doesn't need to be observable in the DevTools though.
893
- // It might even result in a bad user experience for e.g. node selection in the Elements panel.
894
- // The easiest fix is to strip out the intermediate Fragment fibers,
895
- // so the Elements panel and Profiler don't need to special case them.
896
- const primaryChildFragment = nextFiber.child;
897
- const fallbackChildFragment = primaryChildFragment.sibling;
898
- const fallbackChild = fallbackChildFragment.child;
899
-
900
- // The primary, hidden child is never actually updated in this case,
901
- // so we can skip any updates to its tree.
902
- // We only need to track updates to the Fallback UI for now.
903
- if (fallbackChild.alternate) {
904
- updateFiber(fallbackChild, fallbackChild.alternate, nextFiber);
915
+ // The behavior of timed-out Suspense trees is unique.
916
+ // Rather than unmount the timed out content (and possibly lose important state),
917
+ // React re-parents this content within a hidden Fragment while the fallback is showing.
918
+ // This behavior doesn't need to be observable in the DevTools though.
919
+ // It might even result in a bad user experience for e.g. node selection in the Elements panel.
920
+ // The easiest fix is to strip out the intermediate Fragment fibers,
921
+ // so the Elements panel and Profiler don't need to special case them.
922
+ if (nextFiber.tag === SuspenseComponent) {
923
+ // Suspense components only have a non-null memoizedState if they're timed-out.
924
+ const prevDidTimeout = prevFiber.memoizedState !== null;
925
+ const nextDidTimeOut = nextFiber.memoizedState !== null;
926
+
927
+ // The logic below is inspired by the codepaths in updateSuspenseComponent()
928
+ // inside ReactFiberBeginWork in the React source code.
929
+ if (prevDidTimeout) {
930
+ if (nextDidTimeOut) {
931
+ // Fallback -> Fallback:
932
+ // 1. Reconcile fallback set.
933
+ const nextFallbackChildSet = nextFiber.child.sibling;
934
+ // Note: We can't use nextFiber.child.sibling.alternate
935
+ // because the set is special and alternate may not exist.
936
+ const prevFallbackChildSet = prevFiber.child.sibling;
937
+ updateFiberRecursively(
938
+ nextFallbackChildSet,
939
+ prevFallbackChildSet,
940
+ nextFiber
941
+ );
942
+ return;
943
+ } else {
944
+ // Fallback -> Primary:
945
+ // 1. Unmount fallback set
946
+ // Note: don't emulate fallback unmount because React actually did it.
947
+ // 2. Mount primary set
948
+ const nextPrimaryChildSet = nextFiber.child;
949
+ mountFiberRecursively(nextPrimaryChildSet, nextFiber, true);
950
+ return;
951
+ }
952
} else {
906
- mountFiber(fallbackChild, nextFiber);
953
+ if (nextDidTimeOut) {
954
+ // Primary -> Fallback:
955
+ // 1. Hide primary set
956
+ // This is not a real unmount, so it won't get reported by React.
957
+ // By this point it's *too late* to find the previous primary child set
958
+ // so we'll just tell the store to "forget" about those children.
959
+ // They might "resurface" later when we switch to primary content,
960
+ // but from the store's point of view they will be a new tree.
961
+ recordRecursiveRemoveChildren(nextFiber);
962
+ // 2. Mount fallback set
963
+ const nextFallbackChildSet = nextFiber.child.sibling;
964
+ mountFiberRecursively(nextFallbackChildSet, nextFiber, true);
965
+ return;
966
+ } else {
967
+ // Primary -> Primary:
968
+ // 1. Reconcile primary set.
969
+ // Note: no return so we can passthrough to the logic below.
970
+ }
971
}
972
+ }
973
909
- if (shouldEnqueueUpdate) {
910
- enqueueUpdateIfNecessary(nextFiber, false);
911
- }
912
- } else {
913
- let hasChildOrderChanged = false;
914
- if (nextFiber.child !== prevFiber.child) {
915
- // If the first child is different, we need to traverse them.
916
- // Each next child will be either a new child (mount) or an alternate (update).
917
- let nextChild = nextFiber.child;
918
- let prevChildAtSameIndex = prevFiber.child;
919
- while (nextChild) {
920
- // We already know children will be referentially different because
921
- // they are either new mounts or alternates of previous children.
922
- // Schedule updates and mounts depending on whether alternates exist.
923
- // We don't track deletions here because they are reported separately.
924
- if (nextChild.alternate) {
925
- const prevChild = nextChild.alternate;
926
- updateFiber(
927
- nextChild,
928
- prevChild,
929
- shouldEnqueueUpdate ? nextFiber : parentFiber
930
- );
931
- // However we also keep track if the order of the children matches
932
- // the previous order. They are always different referentially, but
933
- // if the instances line up conceptually we'll want to know that.
934
- if (!hasChildOrderChanged && prevChild !== prevChildAtSameIndex) {
935
- hasChildOrderChanged = true;
936
- }
937
- } else {
938
- mountFiber(
939
- nextChild,
940
- shouldEnqueueUpdate ? nextFiber : parentFiber
941
- );
942
- if (!hasChildOrderChanged) {
943
- hasChildOrderChanged = true;
944
- }
974
+ const shouldInclude = !shouldFilterFiber(nextFiber);
975
+ let hasChildOrderChanged = false;
976
+ if (nextFiber.child !== prevFiber.child) {
977
+ // If the first child is different, we need to traverse them.
978
+ // Each next child will be either a new child (mount) or an alternate (update).
979
+ let nextChild = nextFiber.child;
980
+ let prevChildAtSameIndex = prevFiber.child;
981
+ while (nextChild) {
982
+ // We already know children will be referentially different because
983
+ // they are either new mounts or alternates of previous children.
984
+ // Schedule updates and mounts depending on whether alternates exist.
985
+ // We don't track deletions here because they are reported separately.
986
+ if (nextChild.alternate) {
987
+ const prevChild = nextChild.alternate;
988
+ updateFiberRecursively(
989
+ nextChild,
990
+ prevChild,
991
+ shouldInclude ? nextFiber : parentFiber
992
+ );
993
+ // However we also keep track if the order of the children matches
994
+ // the previous order. They are always different referentially, but
995
+ // if the instances line up conceptually we'll want to know that.
996
+ if (!hasChildOrderChanged && prevChild !== prevChildAtSameIndex) {
997
+ hasChildOrderChanged = true;
998
}
946
- // Try the next child.
947
- nextChild = nextChild.sibling;
948
- // Advance the pointer in the previous list so that we can
949
- // keep comparing if they line up.
950
- if (!hasChildOrderChanged && prevChildAtSameIndex != null) {
951
- prevChildAtSameIndex = prevChildAtSameIndex.sibling;
999
+ } else {
1000
+ mountFiberRecursively(
1001
+ nextChild,
1002
+ shouldInclude ? nextFiber : parentFiber
1003
+ );
1004
+ if (!hasChildOrderChanged) {
1005
+ hasChildOrderChanged = true;
1006
}
1007
}
954
- // If we have no more children, but used to, they don't line up.
1008
+ // Try the next child.
1009
+ nextChild = nextChild.sibling;
1010
+ // Advance the pointer in the previous list so that we can
1011
+ // keep comparing if they line up.
1012
if (!hasChildOrderChanged && prevChildAtSameIndex != null) {
956
- hasChildOrderChanged = true;
1013
+ prevChildAtSameIndex = prevChildAtSameIndex.sibling;
1014
}
1015
}
959
-
960
- if (shouldEnqueueUpdate) {
961
- enqueueUpdateIfNecessary(nextFiber, hasChildOrderChanged);
1016
+ // If we have no more children, but used to, they don't line up.
1017
+ if (!hasChildOrderChanged && prevChildAtSameIndex != null) {
1018
+ hasChildOrderChanged = true;
1019
}
1020
}
1021
+
1022
+ if (shouldInclude) {
1023
+ maybeRecordUpdate(nextFiber, hasChildOrderChanged);
1024
+ }
1025
}
1026
1027
function cleanup() {
@@ -1002,7 +1063,7 @@ export function attach(
1063
};
1064
}
1065
1005
- mountFiber(root.current, null);
1066
+ mountFiberRecursively(root.current, null);
1067
flushPendingEvents(root);
1068
currentRootID = -1;
1069
});
@@ -1013,7 +1074,7 @@ export function attach(
1074
// This is not recursive.
1075
// We can't traverse fibers after unmounting so instead
1076
// we rely on React telling us about each unmount.
1016
- enqueueUnmount(fiber);
1077
+ recordUnmount(fiber);
1078
}
1079
1080
function handleCommitFiberRoot(root) {
@@ -1047,17 +1108,17 @@ export function attach(
1108
current.memoizedState != null && current.memoizedState.element != null;
1109
if (!wasMounted && isMounted) {
1110
// Mount a new root.
1050
- mountFiber(current, null);
1111
+ mountFiberRecursively(current, null);
1112
} else if (wasMounted && isMounted) {
1113
// Update an existing root.
1053
- updateFiber(current, alternate, null);
1114
+ updateFiberRecursively(current, alternate, null);
1115
} else if (wasMounted && !isMounted) {
1116
// Unmount an existing root.
1056
- enqueueUnmount(current);
1117
+ recordUnmount(current);
1118
}
1119
} else {
1120
// Mount a new root.
1060
- mountFiber(current, null);
1121
+ mountFiberRecursively(current, null);
1122
}
1123
1124
if (isProfiling) {
src/constants.js
+1
@@ -4,6 +4,7 @@ export const TREE_OPERATION_ADD = 1;
4
export const TREE_OPERATION_REMOVE = 2;
5
export const TREE_OPERATION_RESET_CHILDREN = 3;
6
export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
7
+export const TREE_OPERATION_RECURSIVE_REMOVE_CHILDREN = 5;
8
9
export const LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY =
10
'React::DevTools::reloadAndProfile';
src/devtools/store.js
+51
-4
@@ -3,6 +3,7 @@
3
import EventEmitter from 'events';
4
import {
5
TREE_OPERATION_ADD,
6
+ TREE_OPERATION_RECURSIVE_REMOVE_CHILDREN,
7
TREE_OPERATION_REMOVE,
8
TREE_OPERATION_RESET_CHILDREN,
9
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
@@ -572,7 +573,52 @@ export default class Store extends EventEmitter {
573
weightDelta = 1;
574
}
575
break;
575
- case TREE_OPERATION_REMOVE:
576
+ case TREE_OPERATION_RECURSIVE_REMOVE_CHILDREN: {
577
+ id = ((operations[i + 1]: any): number);
578
+
579
+ if (!this._idToElement.has(id)) {
580
+ throw new Error(
581
+ 'Store does not contain fiber ' +
582
+ id +
583
+ '. This is a bug in React DevTools.'
584
+ );
585
+ }
586
+
587
+ i = i + 2;
588
+
589
+ let justRemovedIDs = [];
590
+ const recursivelyRemove = childID => {
591
+ justRemovedIDs.push(childID);
592
+ const child = this._idToElement.get(childID);
593
+ if (!child) {
594
+ throw new Error(
595
+ 'Store does not contain fiber ' +
596
+ childID +
597
+ '. This is a bug in React DevTools.'
598
+ );
599
+ }
600
+ this._idToElement.delete(childID);
601
+ child.children.forEach(recursivelyRemove);
602
+ };
603
+
604
+ // Track removed items so search results can be updated
605
+ const oldRemovedElementIDs = removedElementIDs;
606
+ removedElementIDs = new Uint32Array(
607
+ removedElementIDs.length + justRemovedIDs.length
608
+ );
609
+ removedElementIDs.set(oldRemovedElementIDs);
610
+ let startIndex = oldRemovedElementIDs.length;
611
+ for (let j = 0; j < justRemovedIDs.length; j++) {
612
+ removedElementIDs[startIndex + j] = oldRemovedElementIDs[j];
613
+ }
614
+
615
+ parentElement = ((this._idToElement.get(id): any): Element);
616
+ parentElement.children.forEach(recursivelyRemove);
617
+ parentElement.children = [];
618
+ weightDelta = -parentElement.weight + 1;
619
+ break;
620
+ }
621
+ case TREE_OPERATION_REMOVE: {
622
id = ((operations[i + 1]: any): number);
623
624
if (!this._idToElement.has(id)) {
@@ -614,11 +660,12 @@ export default class Store extends EventEmitter {
660
}
661
662
// Track removed items so search results can be updated
617
- const oldRemovededElementIDs = removedElementIDs;
663
+ const oldRemovedElementIDs = removedElementIDs;
664
removedElementIDs = new Uint32Array(removedElementIDs.length + 1);
619
- removedElementIDs.set(oldRemovededElementIDs);
620
- removedElementIDs[oldRemovededElementIDs.length] = id;
665
+ removedElementIDs.set(oldRemovedElementIDs);
666
+ removedElementIDs[oldRemovedElementIDs.length] = id;
667
break;
668
+ }
669
case TREE_OPERATION_RESET_CHILDREN:
670
id = ((operations[i + 1]: any): number);
671
const numChildren = ((operations[i + 2]: any): number);