Match Fibers to saved selection as they mount
This implements matching Fibers against the tracked selection path. The algorithm is optimized to do as little checks as possible: * When not trying to restore selection, we don't do anything * When restoring selection, we only check .return pointers of new mounts * Only when .return pointers match our current deepest match, we compare the frames
Dan Abramov committed
Apr 23, 2019 at 16:56 UTC
7eb7c5a350f10a29c3629a72d08e2183ea2d9920
1 file changed
+126
-6
src/backend/renderer.js
+126
-6
@@ -710,7 +710,6 @@ export function attach(
710
const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner');
711
712
if (isRoot) {
713
- rootInsertionOrder.set(id, nextRootIndex++);
713
pushOperation(TREE_OPERATION_ADD);
714
pushOperation(id);
715
pushOperation(ElementTypeRoot);
@@ -784,6 +783,18 @@ export function attach(
783
}
784
785
function recordUnmount(fiber: Fiber, isSimulated: boolean) {
786
+ if (trackedPathMatchFiber !== null) {
787
+ // We're in the process of trying to restore previous selection.
788
+ // If this fiber matched but is being unmounted, there's no use trying.
789
+ // Reset the state so we don't keep holding onto it.
790
+ if (
791
+ fiber === trackedPathMatchFiber ||
792
+ fiber === trackedPathMatchFiber.alternate
793
+ ) {
794
+ setTrackedPath(null);
795
+ }
796
+ }
797
+
798
const isRoot = fiber.tag === HostRoot;
799
const primaryFiber = getPrimaryFiber(fiber);
800
if (!fiberToIDMap.has(primaryFiber)) {
@@ -798,7 +809,6 @@ export function attach(
809
}
810
const id = getFiberID(primaryFiber);
811
if (isRoot) {
801
- rootInsertionOrder.delete(id);
812
// Removing a root needs to happen at the end
813
// so we don't batch it with other unmounts.
814
pushOperation(TREE_OPERATION_REMOVE);
@@ -834,6 +844,12 @@ export function attach(
844
debug('mountFiberRecursively()', fiber, parentFiber);
845
}
846
847
+ // If we have the tree selection from previous reload, try to match this Fiber.
848
+ // Also remember whether to do the same for siblings.
849
+ const mightSiblingsBeOnTrackedPath = updateTrackedPathStateBeforeMount(
850
+ fiber
851
+ );
852
+
853
const shouldIncludeInTree = !shouldFilterFiber(fiber);
854
if (shouldIncludeInTree) {
855
recordMount(fiber, parentFiber);
@@ -867,6 +883,10 @@ export function attach(
883
}
884
}
885
886
+ // We're exiting this Fiber now, and entering its siblings.
887
+ // If we have selection to restore, we might need to re-activate tracking.
888
+ updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath);
889
+
890
if (traverseSiblings && fiber.sibling !== null) {
891
mountFiberRecursively(fiber.sibling, parentFiber, true);
892
}
@@ -1141,9 +1161,15 @@ export function attach(
1161
});
1162
});
1163
} else {
1164
+ // Before the traversals, remember to start tracking
1165
+ // our path in case we have selection to restore.
1166
+ if (trackedPath !== null) {
1167
+ mightBeOnTrackedPath = true;
1168
+ }
1169
// If we have not been profiling, then we can just walk the tree and build up its current state as-is.
1170
hook.getFiberRoots(rendererID).forEach(root => {
1171
currentRootID = getFiberID(getPrimaryFiber(root.current));
1172
+ rootInsertionOrder.set(currentRootID, nextRootIndex++);
1173
1174
if (isProfiling) {
1175
// If profiling is active, store commit time and duration, and the current interactions.
@@ -1181,6 +1207,12 @@ export function attach(
1207
1208
currentRootID = getFiberID(getPrimaryFiber(current));
1209
1210
+ // Before the traversals, remember to start tracking
1211
+ // our path in case we have selection to restore.
1212
+ if (trackedPath !== null) {
1213
+ mightBeOnTrackedPath = true;
1214
+ }
1215
+
1216
if (isProfiling) {
1217
// If profiling is active, store commit time and duration, and the current interactions.
1218
// The frontend may request this information after profiling has stopped.
@@ -1206,16 +1238,19 @@ export function attach(
1238
current.memoizedState != null && current.memoizedState.element != null;
1239
if (!wasMounted && isMounted) {
1240
// Mount a new root.
1241
+ rootInsertionOrder.set(currentRootID, nextRootIndex++);
1242
mountFiberRecursively(current, null);
1243
} else if (wasMounted && isMounted) {
1244
// Update an existing root.
1245
updateFiberRecursively(current, alternate, null);
1246
} else if (wasMounted && !isMounted) {
1247
// Unmount an existing root.
1248
+ rootInsertionOrder.delete(currentRootID);
1249
recordUnmount(current, false);
1250
}
1251
} else {
1252
// Mount a new root.
1253
+ rootInsertionOrder.set(currentRootID, nextRootIndex++);
1254
mountFiberRecursively(current, null);
1255
}
1256
@@ -2009,12 +2044,82 @@ export function attach(
2044
scheduleUpdate(fiber);
2045
}
2046
2047
+ // Remember if we're trying to restore the selection after reload.
2048
+ // In that case, we'll do some extra checks for matching mounts.
2049
let trackedPath: Array<PathFrame> | null = null;
2050
+ let trackedPathMatchFiber: Fiber | null = null;
2051
+ let trackedPathMatchDepth = -1;
2052
+ let mightBeOnTrackedPath = false;
2053
2054
function setTrackedPath(path: Array<PathFrame> | null) {
2055
+ if (path === null) {
2056
+ trackedPathMatchFiber = null;
2057
+ trackedPathMatchDepth = -1;
2058
+ mightBeOnTrackedPath = false;
2059
+ } else if (trackedPath !== null) {
2060
+ throw new Error('Tracked path can only be set once.');
2061
+ }
2062
trackedPath = path;
2063
}
2064
2065
+ // We call this before traversing a new mount.
2066
+ // It remembers whether this Fiber is the next best match for tracked path.
2067
+ // The return value signals whether we should keep matching siblings or not.
2068
+ function updateTrackedPathStateBeforeMount(fiber: Fiber): boolean {
2069
+ if (trackedPath === null || !mightBeOnTrackedPath) {
2070
+ // Fast path: there's nothing to track so do nothing and ignore siblings.
2071
+ return false;
2072
+ }
2073
+ const returnFiber = fiber.return;
2074
+ const returnAlternate = returnFiber !== null ? returnFiber.alternate : null;
2075
+ // By now we know there's some selection to restore, and this is a new Fiber.
2076
+ // Is this newly mounted Fiber a direct child of the current best match?
2077
+ // (This will also be true for new roots if we haven't matched anything yet.)
2078
+ if (
2079
+ trackedPathMatchFiber === returnFiber ||
2080
+ (trackedPathMatchFiber === returnAlternate && returnAlternate !== null)
2081
+ ) {
2082
+ // Is this the next Fiber we should select? Let's compare the frames.
2083
+ const actualFrame = getPathFrame(fiber);
2084
+ const expectedFrame = trackedPath[trackedPathMatchDepth + 1];
2085
+ if (expectedFrame === undefined) {
2086
+ throw new Error('Expected to see a frame at the next depth.');
2087
+ }
2088
+ if (
2089
+ actualFrame.index === expectedFrame.index &&
2090
+ actualFrame.key === expectedFrame.key &&
2091
+ actualFrame.displayName === expectedFrame.displayName
2092
+ ) {
2093
+ // We have our next match.
2094
+ trackedPathMatchFiber = fiber;
2095
+ trackedPathMatchDepth++;
2096
+ // Are we out of frames to match?
2097
+ if (trackedPathMatchDepth === trackedPath.length - 1) {
2098
+ // There's nothing that can possibly match afterwards.
2099
+ // Don't check the children.
2100
+ mightBeOnTrackedPath = false;
2101
+ } else {
2102
+ // Check the children, as they might reveal the next match.
2103
+ mightBeOnTrackedPath = true;
2104
+ }
2105
+ // In either case, since we have a match, we don't need
2106
+ // to check the siblings. They'll never match.
2107
+ return false;
2108
+ }
2109
+ }
2110
+ // This Fiber's parent is on the path, but this Fiber itself isn't.
2111
+ // There's no need to check its children--they won't be on the path either.
2112
+ mightBeOnTrackedPath = false;
2113
+ // However, one of its siblings may be on the path so keep searching.
2114
+ return true;
2115
+ }
2116
+
2117
+ function updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath) {
2118
+ // updateTrackedPathStateBeforeMount() told us whether to match siblings.
2119
+ // Now that we're entering siblings, let's use that information.
2120
+ mightBeOnTrackedPath = mightSiblingsBeOnTrackedPath;
2121
+ }
2122
+
2123
function getPathFrame(fiber: Fiber): PathFrame {
2124
let { displayName, key, index } = getDataForFiber(fiber);
2125
if (fiber.tag === HostRoot) {
@@ -2053,11 +2158,26 @@ export function attach(
2158
}
2159
2160
function getBestMatchForTrackedPath(): PathMatch | null {
2056
- if (trackedPath !== null) {
2057
- console.log('Looking for match for path: ', trackedPath);
2161
+ if (trackedPath === null) {
2162
+ // Nothing to match.
2163
+ return null;
2164
}
2059
- // TODO: implement this.
2060
- return null;
2165
+ if (trackedPathMatchFiber === null) {
2166
+ // We didn't find anything.
2167
+ return null;
2168
+ }
2169
+ // Find the closest Fiber store is aware of.
2170
+ let fiber = trackedPathMatchFiber;
2171
+ while (fiber !== null && shouldFilterFiber(fiber)) {
2172
+ fiber = fiber.return;
2173
+ }
2174
+ if (fiber === null) {
2175
+ return null;
2176
+ }
2177
+ return {
2178
+ id: getFiberID(getPrimaryFiber(fiber)),
2179
+ isFullMatch: trackedPathMatchDepth === trackedPath.length - 1,
2180
+ };
2181
}
2182
2183
return {