@samitouri / QOS-React / commits / 0f536bba5c

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);