Add TypeBox Value.Equal deep-comparison as first optimization layer in reconcileProps. When a fiber's cached node is deep-equal to the next node, skip prepareUpdate, commitUpdate, and children reconciliation entirely. New cachedNode field on Fiber stores the last reconciled node for comparison.
179 lines
5.3 KiB
TypeScript
179 lines
5.3 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import type { Fiber } from "../src/host/fiber.js";
|
|
import type { UNode } from "../src/core/schema.js";
|
|
import { longestIncreasingSubsequence, reconcileChildren } from "../src/host/reconcile.js";
|
|
|
|
function makeFiber(key: string | undefined, tag: string): Fiber<string> {
|
|
return {
|
|
instance: `inst-${tag}-${key ?? "nokey"}`,
|
|
tag,
|
|
props: {},
|
|
key,
|
|
children: [],
|
|
parent: null,
|
|
effect: null,
|
|
signalDisposers: [],
|
|
prevProps: null,
|
|
cachedNode: null,
|
|
};
|
|
}
|
|
|
|
function makeElement(type: string, key?: string): UNode {
|
|
return { type, props: {}, children: [], ...(key !== undefined ? { key } : {}) };
|
|
}
|
|
|
|
describe("longestIncreasingSubsequence", () => {
|
|
it("empty array → empty result", () => {
|
|
expect(longestIncreasingSubsequence([])).toEqual([]);
|
|
});
|
|
|
|
it("single element → [0]", () => {
|
|
expect(longestIncreasingSubsequence([5])).toEqual([0]);
|
|
});
|
|
|
|
it("already ordered [0, 1, 2] → all stay", () => {
|
|
const result = longestIncreasingSubsequence([0, 1, 2]);
|
|
expect(result).toEqual([0, 1, 2]);
|
|
});
|
|
|
|
it("[2, 0, 1] → LIS positions: [1, 2] (values 0, 1 stay)", () => {
|
|
const result = longestIncreasingSubsequence([2, 0, 1]);
|
|
expect(result).toEqual([1, 2]);
|
|
const moved = [0, 1, 2].filter((i) => !result.includes(i));
|
|
expect(moved).toEqual([0]);
|
|
});
|
|
|
|
it("reversed [3, 2, 1, 0] → LIS length 1, three moves", () => {
|
|
const result = longestIncreasingSubsequence([3, 2, 1, 0]);
|
|
expect(result).toHaveLength(1);
|
|
const moved = [0, 1, 2, 3].filter((i) => !result.includes(i));
|
|
expect(moved).toHaveLength(3);
|
|
});
|
|
|
|
it("[0, 4, 2, 3, 1] → LIS covers 0, 2, 3", () => {
|
|
const result = longestIncreasingSubsequence([0, 4, 2, 3, 1]);
|
|
expect(result.length).toBeGreaterThanOrEqual(3);
|
|
const vals = result.map((i) => [0, 4, 2, 3, 1][i]!);
|
|
for (let i = 1; i < vals.length; i++) {
|
|
expect(vals[i]).toBeGreaterThan(vals[i - 1]!);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("reconcileChildren LIS move detection", () => {
|
|
it("already ordered children → no moves", () => {
|
|
const oldFibers = [
|
|
makeFiber("a", "div"),
|
|
makeFiber("b", "span"),
|
|
makeFiber("c", "p"),
|
|
];
|
|
const newChildren = [
|
|
makeElement("div", "a"),
|
|
makeElement("span", "b"),
|
|
makeElement("p", "c"),
|
|
];
|
|
const result = reconcileChildren(oldFibers, newChildren);
|
|
expect(result.moves.size).toBe(0);
|
|
});
|
|
|
|
it("reordered children → moves for out-of-order", () => {
|
|
const oldFibers = [
|
|
makeFiber("a", "div"),
|
|
makeFiber("b", "span"),
|
|
makeFiber("c", "p"),
|
|
];
|
|
const newChildren = [
|
|
makeElement("p", "c"),
|
|
makeElement("div", "a"),
|
|
makeElement("span", "b"),
|
|
];
|
|
const result = reconcileChildren(oldFibers, newChildren);
|
|
expect(result.moves.size).toBeGreaterThan(0);
|
|
expect(result.matched).toHaveLength(3);
|
|
});
|
|
|
|
it("adding child at start doesn't move existing ones (LIS covers all old)", () => {
|
|
const oldFibers = [
|
|
makeFiber("b", "span"),
|
|
makeFiber("c", "p"),
|
|
];
|
|
const newChildren = [
|
|
makeElement("div", "a"),
|
|
makeElement("span", "b"),
|
|
makeElement("p", "c"),
|
|
];
|
|
const result = reconcileChildren(oldFibers, newChildren);
|
|
|
|
expect(result.matched).toHaveLength(2);
|
|
expect(result.added).toHaveLength(1);
|
|
expect(result.moves.size).toBe(0);
|
|
});
|
|
|
|
it("reversed list → LIS of length 1, three moves", () => {
|
|
const oldFibers = [
|
|
makeFiber("a", "div"),
|
|
makeFiber("b", "span"),
|
|
makeFiber("c", "p"),
|
|
makeFiber("d", "section"),
|
|
];
|
|
const newChildren = [
|
|
makeElement("section", "d"),
|
|
makeElement("p", "c"),
|
|
makeElement("span", "b"),
|
|
makeElement("div", "a"),
|
|
];
|
|
const result = reconcileChildren(oldFibers, newChildren);
|
|
expect(result.matched).toHaveLength(4);
|
|
expect(result.moves.size).toBe(3);
|
|
});
|
|
|
|
it("[2, 0, 1] pattern → index 0 stays, indices 2 and 1 move", () => {
|
|
const oldFibers = [
|
|
makeFiber("a", "div"),
|
|
makeFiber("b", "span"),
|
|
makeFiber("c", "p"),
|
|
];
|
|
const newChildren = [
|
|
makeElement("p", "c"),
|
|
makeElement("div", "a"),
|
|
makeElement("span", "b"),
|
|
];
|
|
const result = reconcileChildren(oldFibers, newChildren);
|
|
|
|
const oldIndices = result.matched.map((m) => oldFibers.indexOf(m.oldFiber));
|
|
const lisPositions = longestIncreasingSubsequence(oldIndices);
|
|
const lisSet = new Set(lisPositions);
|
|
|
|
const movedIndices = result.matched
|
|
.map((_, i) => i)
|
|
.filter((i) => !lisSet.has(i));
|
|
expect(movedIndices.length).toBeGreaterThan(0);
|
|
|
|
const stayedIndices = result.matched
|
|
.map((_, i) => i)
|
|
.filter((i) => lisSet.has(i));
|
|
expect(stayedIndices.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("moves map contains old fibers that need moving", () => {
|
|
const oldFibers = [
|
|
makeFiber("a", "div"),
|
|
makeFiber("b", "span"),
|
|
];
|
|
const newChildren = [
|
|
makeElement("span", "b"),
|
|
makeElement("div", "a"),
|
|
];
|
|
const result = reconcileChildren(oldFibers, newChildren);
|
|
|
|
expect(result.moves.size).toBe(1);
|
|
for (const fiber of result.moves.values()) {
|
|
expect(oldFibers).toContain(fiber);
|
|
}
|
|
});
|
|
|
|
it("no matched children → empty moves", () => {
|
|
const result = reconcileChildren([], [makeElement("div", "a")]);
|
|
expect(result.moves.size).toBe(0);
|
|
});
|
|
}); |