Add key-based child matching algorithm (reconcileChildren) for fiber reconciliation
This commit is contained in:
@@ -2,7 +2,7 @@ import { effect } from "@preact/signals-core";
|
||||
import type { Fiber, Effect } from "./fiber.js";
|
||||
import type { HostConfig } from "./config.js";
|
||||
import type { UNode, UElement } from "../core/schema.js";
|
||||
import { isUPrimitive, isURoot } from "../core/schema.js";
|
||||
import { isUPrimitive, isURoot, isUElement } from "../core/schema.js";
|
||||
|
||||
interface PendingUpdate {
|
||||
fiber: Fiber<unknown>;
|
||||
@@ -154,4 +154,118 @@ export function wireSignalToFiber<I>(
|
||||
export function resetUpdateQueue(): void {
|
||||
pendingUpdates = [];
|
||||
flushScheduled = false;
|
||||
}
|
||||
|
||||
export interface MatchedChild<I> {
|
||||
oldFiber: Fiber<I>;
|
||||
newChild: UElement;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface ChildClassification<I> {
|
||||
matched: MatchedChild<I>[];
|
||||
added: { newChild: UElement; index: number }[];
|
||||
removed: Fiber<I>[];
|
||||
}
|
||||
|
||||
export function reconcileChildren<I>(
|
||||
oldFibers: Fiber<I>[],
|
||||
newChildren: UNode[],
|
||||
): ChildClassification<I> {
|
||||
const matched: MatchedChild<I>[] = [];
|
||||
const added: { newChild: UElement; index: number }[] = [];
|
||||
const removed: Fiber<I>[] = [];
|
||||
|
||||
const oldKeyMap = new Map<string, Fiber<I>>();
|
||||
const displacedOldFibers: Fiber<I>[] = [];
|
||||
for (const fiber of oldFibers) {
|
||||
if (fiber.key !== undefined) {
|
||||
if (oldKeyMap.has(fiber.key)) {
|
||||
console.warn(`Duplicate key "${fiber.key}" among old children; last-wins`);
|
||||
displacedOldFibers.push(oldKeyMap.get(fiber.key)!);
|
||||
}
|
||||
oldKeyMap.set(fiber.key, fiber);
|
||||
}
|
||||
}
|
||||
|
||||
const matchedOldKeys = new Set<string>();
|
||||
const unkeyedOldUsed = new Set<number>();
|
||||
let unkeyedOldCursor = 0;
|
||||
|
||||
const seenNewKeys = new Map<string, number>();
|
||||
|
||||
for (let i = 0; i < newChildren.length; i++) {
|
||||
const child = newChildren[i]!;
|
||||
if (!isUElement(child)) continue;
|
||||
|
||||
if (child.key !== undefined) {
|
||||
const prevIdx = seenNewKeys.get(child.key);
|
||||
if (prevIdx !== undefined) {
|
||||
console.warn(`Duplicate key "${child.key}" among new children; last-wins`);
|
||||
const prevMatch = matched.findIndex((m) => m.newChild.key === child.key && m.index === prevIdx);
|
||||
if (prevMatch !== -1) {
|
||||
const oldFiber = matched[prevMatch]!.oldFiber;
|
||||
removed.push(oldFiber);
|
||||
matched.splice(prevMatch, 1);
|
||||
}
|
||||
}
|
||||
seenNewKeys.set(child.key, i);
|
||||
|
||||
const oldFiber = oldKeyMap.get(child.key);
|
||||
if (oldFiber !== undefined) {
|
||||
matchedOldKeys.add(child.key);
|
||||
if (oldFiber.tag === child.type) {
|
||||
matched.push({ oldFiber, newChild: child, index: i });
|
||||
} else {
|
||||
removed.push(oldFiber);
|
||||
added.push({ newChild: child, index: i });
|
||||
}
|
||||
} else {
|
||||
added.push({ newChild: child, index: i });
|
||||
}
|
||||
} else {
|
||||
let matchedOld: Fiber<I> | null = null;
|
||||
let matchedOldIdx = -1;
|
||||
for (let j = unkeyedOldCursor; j < oldFibers.length; j++) {
|
||||
if (!unkeyedOldUsed.has(j) && oldFibers[j]!.key === undefined) {
|
||||
matchedOld = oldFibers[j]!;
|
||||
matchedOldIdx = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matchedOld !== null) {
|
||||
unkeyedOldUsed.add(matchedOldIdx);
|
||||
unkeyedOldCursor = matchedOldIdx + 1;
|
||||
if (matchedOld.tag === child.type) {
|
||||
matched.push({ oldFiber: matchedOld, newChild: child, index: i });
|
||||
} else {
|
||||
removed.push(matchedOld);
|
||||
added.push({ newChild: child, index: i });
|
||||
}
|
||||
} else {
|
||||
added.push({ newChild: child, index: i });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < oldFibers.length; i++) {
|
||||
const fiber = oldFibers[i]!;
|
||||
if (fiber.key !== undefined) {
|
||||
if (!matchedOldKeys.has(fiber.key)) {
|
||||
removed.push(fiber);
|
||||
}
|
||||
} else {
|
||||
if (!unkeyedOldUsed.has(i)) {
|
||||
removed.push(fiber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const displaced of displacedOldFibers) {
|
||||
if (matchedOldKeys.has(displaced.key!)) {
|
||||
removed.push(displaced);
|
||||
}
|
||||
}
|
||||
|
||||
return { matched, added, removed };
|
||||
}
|
||||
Reference in New Issue
Block a user