Skip to content

Commit f90a6bc

Browse files
authored
[DevTools] Reconcile Fibers Against Previous Children Instances (#30822)
This loops over the remainingReconcilingChildren to find existing FiberInstances that match the updated Fiber. This is the same thing we already do for virtual instances. This avoids the need for a `fiberToFiberInstanceMap`. This loop is fast but there is a downside when the children set is very large and gets reordered with keys since we might have to loop over the set multiple times to get to the instances in the bottom. If that becomes a problem we can optimize it the same way ReactChildFiber does which is to create a temporary Map only when the children don't line up properly. That way everything except the first pass can use the Map but there's no need to create it eagerly. Now that we have the loop we don't need the previousSibling field so we can save some memory there.
1 parent 9690b9a commit f90a6bc

File tree

1 file changed

+89
-77
lines changed
  • packages/react-devtools-shared/src/backend/fiber

1 file changed

+89
-77
lines changed

packages/react-devtools-shared/src/backend/fiber/renderer.js

+89-77
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,6 @@ type FiberInstance = {
158158
id: number,
159159
parent: null | DevToolsInstance, // filtered parent, including virtual
160160
firstChild: null | DevToolsInstance, // filtered first child, including virtual
161-
previousSibling: null | DevToolsInstance, // filtered next sibling, including virtual
162161
nextSibling: null | DevToolsInstance, // filtered next sibling, including virtual
163162
flags: number, // Force Error/Suspense
164163
source: null | string | Error | Source, // source location of this component function, or owned child stack
@@ -174,7 +173,6 @@ function createFiberInstance(fiber: Fiber): FiberInstance {
174173
id: getUID(),
175174
parent: null,
176175
firstChild: null,
177-
previousSibling: null,
178176
nextSibling: null,
179177
flags: 0,
180178
source: null,
@@ -195,7 +193,6 @@ type VirtualInstance = {
195193
id: number,
196194
parent: null | DevToolsInstance, // filtered parent, including virtual
197195
firstChild: null | DevToolsInstance, // filtered first child, including virtual
198-
previousSibling: null | DevToolsInstance, // filtered next sibling, including virtual
199196
nextSibling: null | DevToolsInstance, // filtered next sibling, including virtual
200197
flags: number,
201198
source: null | string | Error | Source, // source location of this server component, or owned child stack
@@ -218,7 +215,6 @@ function createVirtualInstance(
218215
id: getUID(),
219216
parent: null,
220217
firstChild: null,
221-
previousSibling: null,
222218
nextSibling: null,
223219
flags: 0,
224220
source: null,
@@ -1088,8 +1084,6 @@ export function attach(
10881084
' '.repeat(indent) + '- ' + instance.id + ' (' + name + ')',
10891085
'parent',
10901086
instance.parent === null ? ' ' : instance.parent.id,
1091-
'prev',
1092-
instance.previousSibling === null ? ' ' : instance.previousSibling.id,
10931087
'next',
10941088
instance.nextSibling === null ? ' ' : instance.nextSibling.id,
10951089
);
@@ -2321,30 +2315,32 @@ export function attach(
23212315
if (previouslyReconciledSibling === null) {
23222316
previouslyReconciledSibling = instance;
23232317
parentInstance.firstChild = instance;
2324-
instance.previousSibling = null;
23252318
} else {
23262319
previouslyReconciledSibling.nextSibling = instance;
2327-
instance.previousSibling = previouslyReconciledSibling;
23282320
previouslyReconciledSibling = instance;
23292321
}
23302322
instance.nextSibling = null;
23312323
}
23322324

2333-
function moveChild(instance: DevToolsInstance): void {
2334-
removeChild(instance);
2325+
function moveChild(
2326+
instance: DevToolsInstance,
2327+
previousSibling: null | DevToolsInstance,
2328+
): void {
2329+
removeChild(instance, previousSibling);
23352330
insertChild(instance);
23362331
}
23372332

2338-
function removeChild(instance: DevToolsInstance): void {
2333+
function removeChild(
2334+
instance: DevToolsInstance,
2335+
previousSibling: null | DevToolsInstance,
2336+
): void {
23392337
if (instance.parent === null) {
23402338
if (remainingReconcilingChildren === instance) {
23412339
throw new Error(
23422340
'Remaining children should not have items with no parent',
23432341
);
23442342
} else if (instance.nextSibling !== null) {
23452343
throw new Error('A deleted instance should not have next siblings');
2346-
} else if (instance.previousSibling !== null) {
2347-
throw new Error('A deleted instance should not have previous siblings');
23482344
}
23492345
// Already deleted.
23502346
return;
@@ -2360,7 +2356,7 @@ export function attach(
23602356
}
23612357
// Remove an existing child from its current position, which we assume is in the
23622358
// remainingReconcilingChildren set.
2363-
if (instance.previousSibling === null) {
2359+
if (previousSibling === null) {
23642360
// We're first in the remaining set. Remove us.
23652361
if (remainingReconcilingChildren !== instance) {
23662362
throw new Error(
@@ -2369,13 +2365,9 @@ export function attach(
23692365
}
23702366
remainingReconcilingChildren = instance.nextSibling;
23712367
} else {
2372-
instance.previousSibling.nextSibling = instance.nextSibling;
2373-
}
2374-
if (instance.nextSibling !== null) {
2375-
instance.nextSibling.previousSibling = instance.previousSibling;
2368+
previousSibling.nextSibling = instance.nextSibling;
23762369
}
23772370
instance.nextSibling = null;
2378-
instance.previousSibling = null;
23792371
instance.parent = null;
23802372
}
23812373

@@ -2655,7 +2647,7 @@ export function attach(
26552647
} else {
26562648
recordVirtualUnmount(instance);
26572649
}
2658-
removeChild(instance);
2650+
removeChild(instance, null);
26592651
}
26602652

26612653
function recordProfilingDurations(fiberInstance: FiberInstance) {
@@ -2889,8 +2881,7 @@ export function attach(
28892881
);
28902882
}
28912883
}
2892-
// TODO: Find the best matching existing child based on the key if defined.
2893-
2884+
let previousSiblingOfBestMatch = null;
28942885
let bestMatch = remainingReconcilingChildren;
28952886
if (componentInfo.key != null) {
28962887
// If there is a key try to find a matching key in the set.
@@ -2902,6 +2893,7 @@ export function attach(
29022893
) {
29032894
break;
29042895
}
2896+
previousSiblingOfBestMatch = bestMatch;
29052897
bestMatch = bestMatch.nextSibling;
29062898
}
29072899
}
@@ -2916,7 +2908,7 @@ export function attach(
29162908
// with the same name, then we claim it and reuse it for this update.
29172909
// Update it with the latest entry.
29182910
bestMatch.data = componentInfo;
2919-
moveChild(bestMatch);
2911+
moveChild(bestMatch, previousSiblingOfBestMatch);
29202912
previousVirtualInstance = bestMatch;
29212913
previousVirtualInstanceWasMount = false;
29222914
} else {
@@ -2965,42 +2957,93 @@ export function attach(
29652957
}
29662958
previousVirtualInstance = null;
29672959
}
2960+
29682961
// We've reached the end of the virtual levels, but not beyond,
29692962
// and now continue with the regular fiber.
2963+
2964+
// Do a fast pass over the remaining children to find the previous instance.
2965+
// TODO: This doesn't have the best O(n) for a large set of children that are
2966+
// reordered. Consider using a temporary map if it's not the very next one.
2967+
let prevChild;
29702968
if (prevChildAtSameIndex === nextChild) {
29712969
// This set is unchanged. We're just going through it to place all the
29722970
// children again.
2971+
prevChild = nextChild;
2972+
} else {
2973+
// We don't actually need to rely on the alternate here. We could also
2974+
// reconcile against stateNode, key or whatever. Doesn't have to be same
2975+
// Fiber pair.
2976+
prevChild = nextChild.alternate;
2977+
}
2978+
let previousSiblingOfExistingInstance = null;
2979+
let existingInstance = null;
2980+
if (prevChild !== null) {
2981+
existingInstance = remainingReconcilingChildren;
2982+
while (existingInstance !== null) {
2983+
if (existingInstance.data === prevChild) {
2984+
break;
2985+
}
2986+
previousSiblingOfExistingInstance = existingInstance;
2987+
existingInstance = existingInstance.nextSibling;
2988+
}
2989+
}
2990+
if (existingInstance !== null) {
2991+
// Common case. Match in the same parent.
2992+
const fiberInstance: FiberInstance = (existingInstance: any); // Only matches if it's a Fiber.
2993+
2994+
// We keep track if the order of the children matches the previous order.
2995+
// They are always different referentially, but if the instances line up
2996+
// conceptually we'll want to know that.
2997+
if (prevChild !== prevChildAtSameIndex) {
2998+
shouldResetChildren = true;
2999+
}
3000+
3001+
// Register the new alternate in case it's not already in.
3002+
fiberToFiberInstanceMap.set(nextChild, fiberInstance);
3003+
3004+
// Update the Fiber so we that we always keep the current Fiber on the data.
3005+
fiberInstance.data = nextChild;
3006+
moveChild(fiberInstance, previousSiblingOfExistingInstance);
3007+
29733008
if (
29743009
updateFiberRecursively(
3010+
fiberInstance,
29753011
nextChild,
2976-
nextChild,
3012+
(prevChild: any),
29773013
traceNearestHostComponentUpdate,
29783014
)
29793015
) {
2980-
throw new Error('Updating the same fiber should not cause reorder');
3016+
// If a nested tree child order changed but it can't handle its own
3017+
// child order invalidation (e.g. because it's filtered out like host nodes),
3018+
// propagate the need to reset child order upwards to this Fiber.
3019+
shouldResetChildren = true;
29813020
}
2982-
} else if (nextChild.alternate) {
2983-
const prevChild = nextChild.alternate;
3021+
} else if (prevChild !== null && shouldFilterFiber(nextChild)) {
3022+
// If this Fiber should be filtered, we need to still update its children.
3023+
// This relies on an alternate since we don't have an Instance with the previous
3024+
// child on it. Ideally, the reconciliation wouldn't need previous Fibers that
3025+
// are filtered from the tree.
29843026
if (
29853027
updateFiberRecursively(
3028+
null,
29863029
nextChild,
29873030
prevChild,
29883031
traceNearestHostComponentUpdate,
29893032
)
29903033
) {
2991-
// If a nested tree child order changed but it can't handle its own
2992-
// child order invalidation (e.g. because it's filtered out like host nodes),
2993-
// propagate the need to reset child order upwards to this Fiber.
2994-
shouldResetChildren = true;
2995-
}
2996-
// However we also keep track if the order of the children matches
2997-
// the previous order. They are always different referentially, but
2998-
// if the instances line up conceptually we'll want to know that.
2999-
if (prevChild !== prevChildAtSameIndex) {
30003034
shouldResetChildren = true;
30013035
}
30023036
} else {
3037+
// It's possible for a FiberInstance to be reparented when virtual parents
3038+
// get their sequence split or change structure with the same render result.
3039+
// In this case we unmount the and remount the FiberInstances.
3040+
// This might cause us to lose the selection but it's an edge case.
3041+
3042+
// We let the previous instance remain in the "remaining queue" it is
3043+
// in to be deleted at the end since it'll have no match.
3044+
30033045
mountFiberRecursively(nextChild, traceNearestHostComponentUpdate);
3046+
// Need to mark the parent set to remount the new instance.
30043047
shouldResetChildren = true;
30053048
}
30063049
}
@@ -3059,6 +3102,7 @@ export function attach(
30593102

30603103
// Returns whether closest unfiltered fiber parent needs to reset its child list.
30613104
function updateFiberRecursively(
3105+
fiberInstance: null | FiberInstance, // null if this should be filtered
30623106
nextFiber: Fiber,
30633107
prevFiber: Fiber,
30643108
traceNearestHostComponentUpdate: boolean,
@@ -3092,34 +3136,10 @@ export function attach(
30923136
}
30933137
}
30943138

3095-
let fiberInstance: null | FiberInstance = null;
3096-
const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
3097-
if (shouldIncludeInTree) {
3098-
const entry = fiberToFiberInstanceMap.get(prevFiber);
3099-
if (entry !== undefined && entry.parent === reconcilingParent) {
3100-
// Common case. Match in the same parent.
3101-
fiberInstance = entry;
3102-
// Register the new alternate in case it's not already in.
3103-
fiberToFiberInstanceMap.set(nextFiber, fiberInstance);
3104-
3105-
// Update the Fiber so we that we always keep the current Fiber on the data.
3106-
fiberInstance.data = nextFiber;
3107-
moveChild(fiberInstance);
3108-
} else {
3109-
// It's possible for a FiberInstance to be reparented when virtual parents
3110-
// get their sequence split or change structure with the same render result.
3111-
// In this case we unmount the and remount the FiberInstances.
3112-
// This might cause us to lose the selection but it's an edge case.
3113-
3114-
// We let the previous instance remain in the "remaining queue" it is
3115-
// in to be deleted at the end since it'll have no match.
3116-
3117-
mountFiberRecursively(nextFiber, traceNearestHostComponentUpdate);
3118-
3119-
// Need to mark the parent set to remount the new instance.
3120-
return true;
3121-
}
3122-
3139+
const stashedParent = reconcilingParent;
3140+
const stashedPrevious = previouslyReconciledSibling;
3141+
const stashedRemaining = remainingReconcilingChildren;
3142+
if (fiberInstance !== null) {
31233143
if (
31243144
mostRecentlyInspectedElement !== null &&
31253145
mostRecentlyInspectedElement.id === fiberInstance.id &&
@@ -3129,12 +3149,6 @@ export function attach(
31293149
// If it is inspected again, it may need to be re-run to obtain updated hooks values.
31303150
hasElementUpdatedSinceLastInspected = true;
31313151
}
3132-
}
3133-
3134-
const stashedParent = reconcilingParent;
3135-
const stashedPrevious = previouslyReconciledSibling;
3136-
const stashedRemaining = remainingReconcilingChildren;
3137-
if (fiberInstance !== null) {
31383152
// Push a new DevTools instance parent while reconciling this subtree.
31393153
reconcilingParent = fiberInstance;
31403154
previouslyReconciledSibling = null;
@@ -3189,7 +3203,7 @@ export function attach(
31893203
if (
31903204
nextFallbackChildSet != null &&
31913205
prevFallbackChildSet != null &&
3192-
updateFiberRecursively(
3206+
updateChildrenRecursively(
31933207
nextFallbackChildSet,
31943208
prevFallbackChildSet,
31953209
traceNearestHostComponentUpdate,
@@ -3284,10 +3298,8 @@ export function attach(
32843298
if (shouldResetChildren) {
32853299
// We need to crawl the subtree for closest non-filtered Fibers
32863300
// so that we can display them in a flat children set.
3287-
if (shouldIncludeInTree) {
3288-
if (reconcilingParent !== null) {
3289-
recordResetChildren(reconcilingParent);
3290-
}
3301+
if (fiberInstance !== null) {
3302+
recordResetChildren(fiberInstance);
32913303
// We've handled the child order change for this Fiber.
32923304
// Since it's included, there's no need to invalidate parent child order.
32933305
return false;
@@ -3299,7 +3311,7 @@ export function attach(
32993311
return false;
33003312
}
33013313
} finally {
3302-
if (shouldIncludeInTree) {
3314+
if (fiberInstance !== null) {
33033315
unmountRemainingChildren();
33043316
reconcilingParent = stashedParent;
33053317
previouslyReconciledSibling = stashedPrevious;
@@ -3489,7 +3501,7 @@ export function attach(
34893501
mountFiberRecursively(current, false);
34903502
} else if (wasMounted && isMounted) {
34913503
// Update an existing root.
3492-
updateFiberRecursively(current, alternate, false);
3504+
updateFiberRecursively(rootInstance, current, alternate, false);
34933505
} else if (wasMounted && !isMounted) {
34943506
// Unmount an existing root.
34953507
removeRootPseudoKey(currentRootID);

0 commit comments

Comments
 (0)