Implement commitMutations for insert/move/remove effects in tree order
Adds commitMutations function to reconcile.ts that processes fiber effects in correct order: removes (reverse), inserts+moves (left-to-right with insertBefore), updates (top-down). Integrates key-based reconciliation pipeline into render() via reconcileNode, resolveUNode, and createFiberForInsert.
This commit is contained in:
@@ -4,6 +4,12 @@ import type { HostConfig } from "./config.js";
|
||||
import type { UNode, UElement } from "../core/schema.js";
|
||||
import { isUPrimitive, isURoot, isUElement } from "../core/schema.js";
|
||||
|
||||
export interface CommitContext<I> {
|
||||
host: HostConfig<string, I, unknown>;
|
||||
ctx: unknown;
|
||||
createFiber: (node: UNode, parentFiber: Fiber<I>) => Fiber<I>;
|
||||
}
|
||||
|
||||
interface PendingUpdate {
|
||||
fiber: Fiber<unknown>;
|
||||
nextNode: UNode;
|
||||
@@ -320,4 +326,125 @@ export function reconcileChildren<I>(
|
||||
}
|
||||
|
||||
return { matched, added, removed, moves };
|
||||
}
|
||||
|
||||
export function commitMutations<I>(
|
||||
parentFiber: Fiber<I>,
|
||||
classification: ChildClassification<I>,
|
||||
commitCtx: CommitContext<I>,
|
||||
): void {
|
||||
const { host, ctx, createFiber } = commitCtx;
|
||||
const parentInst = parentFiber.instance;
|
||||
|
||||
const movedSet = new Set<Fiber<I>>(classification.moves.values());
|
||||
|
||||
// Map matched index (position in classification.matched[]) for quick lookup
|
||||
const matchedArrayIndex = new Map<MatchedChild<I>, number>();
|
||||
for (let i = 0; i < classification.matched.length; i++) {
|
||||
matchedArrayIndex.set(classification.matched[i]!, i);
|
||||
}
|
||||
|
||||
// Build maps keyed by new-children position
|
||||
const matchedByNewIndex = new Map<number, MatchedChild<I>>();
|
||||
for (const m of classification.matched) {
|
||||
matchedByNewIndex.set(m.index, m);
|
||||
}
|
||||
const addedByIndex = new Map<number, UElement>();
|
||||
for (const { newChild, index } of classification.added) {
|
||||
addedByIndex.set(index, newChild);
|
||||
}
|
||||
|
||||
// Collect all positions in new-children order
|
||||
const allIndices = new Set<number>();
|
||||
for (const m of classification.matched) allIndices.add(m.index);
|
||||
for (const a of classification.added) allIndices.add(a.index);
|
||||
const sortedIndices = [...allIndices].sort((a, b) => a - b);
|
||||
|
||||
// Phase 1: Removes — reverse order (children before parents, bottom-up)
|
||||
for (let i = classification.removed.length - 1; i >= 0; i--) {
|
||||
const fiber = classification.removed[i]!;
|
||||
host.removeChild?.(parentInst as never, fiber.instance as never, ctx as never);
|
||||
}
|
||||
|
||||
// Build new children array and determine placement actions
|
||||
const newChildren: Fiber<I>[] = [];
|
||||
type Placement = { fiber: Fiber<I>; beforeFiber: Fiber<I> | null; kind: "insert" | "move" };
|
||||
const placements: Placement[] = [];
|
||||
|
||||
// Create fibers for added children first (so they're available for before-lookup)
|
||||
const addedFibers = new Map<number, Fiber<I>>();
|
||||
for (const { index } of classification.added) {
|
||||
const newChild = addedByIndex.get(index)!;
|
||||
const fiber = createFiber(newChild, parentFiber);
|
||||
addedFibers.set(index, fiber);
|
||||
}
|
||||
|
||||
// Build the final children array
|
||||
for (const idx of sortedIndices) {
|
||||
const addedChild = addedByIndex.get(idx);
|
||||
if (addedChild !== undefined) {
|
||||
const fiber = addedFibers.get(idx)!;
|
||||
newChildren.push(fiber);
|
||||
// Find before: next fiber in newChildren that is "staying" (not moved, not just inserted)
|
||||
const before = findNextStayingFiber(sortedIndices, matchedByNewIndex, addedByIndex, movedSet, idx);
|
||||
placements.push({ fiber, beforeFiber: before, kind: "insert" });
|
||||
continue;
|
||||
}
|
||||
|
||||
const match = matchedByNewIndex.get(idx);
|
||||
if (match !== undefined) {
|
||||
const fiber = match.oldFiber;
|
||||
newChildren.push(fiber);
|
||||
const mIdx = matchedArrayIndex.get(match)!;
|
||||
if (classification.moves.has(mIdx)) {
|
||||
const before = findNextStayingFiber(sortedIndices, matchedByNewIndex, addedByIndex, movedSet, idx);
|
||||
placements.push({ fiber, beforeFiber: before, kind: "move" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Inserts + Moves — left-to-right
|
||||
for (const { fiber, beforeFiber } of placements) {
|
||||
if (host.insertBefore && beforeFiber) {
|
||||
host.insertBefore(
|
||||
parentInst as never,
|
||||
fiber.instance as never,
|
||||
beforeFiber.instance as never,
|
||||
ctx as never,
|
||||
);
|
||||
} else {
|
||||
host.appendChild(parentInst as never, fiber.instance as never, ctx as never);
|
||||
}
|
||||
}
|
||||
|
||||
// Update fiber tree
|
||||
parentFiber.children = newChildren;
|
||||
for (const fiber of classification.removed) {
|
||||
fiber.parent = null;
|
||||
}
|
||||
|
||||
// Phase 3: Updates — top-down (parent before child) via commitEffects
|
||||
commitEffects(parentFiber, host as HostConfig<string, I, unknown>, ctx);
|
||||
}
|
||||
|
||||
function findNextStayingFiber<I>(
|
||||
sortedIndices: number[],
|
||||
matchedByNewIndex: Map<number, MatchedChild<I>>,
|
||||
addedByIndex: Map<number, UElement>,
|
||||
movedSet: Set<Fiber<I>>,
|
||||
currentIdx: number,
|
||||
): Fiber<I> | null {
|
||||
const posInSorted = sortedIndices.indexOf(currentIdx);
|
||||
for (let p = posInSorted + 1; p < sortedIndices.length; p++) {
|
||||
const nextIdx = sortedIndices[p]!;
|
||||
const isAdded = addedByIndex.has(nextIdx);
|
||||
const match = matchedByNewIndex.get(nextIdx);
|
||||
if (!isAdded && match !== undefined) {
|
||||
const isMoved = movedSet.has(match.oldFiber);
|
||||
if (!isMoved) {
|
||||
return match.oldFiber;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user