Merge feat/value-hash-detection: add Value.Hash O(1) change detection with commitHashes (clean merge, no Value.Diff overlap)
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,4 +7,4 @@ dist/
|
|||||||
.idea/
|
.idea/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
coverage/
|
coverage/.worktrees/
|
||||||
|
|||||||
@@ -81,15 +81,15 @@ describe("Root.render() re-renderable", () => {
|
|||||||
expect(divFiber.props.class).toBe("b");
|
expect(divFiber.props.class).toBe("b");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prepareUpdate is called for changed props on re-render", () => {
|
it("commitUpdate is called for changed props on re-render", () => {
|
||||||
const { host, prepareUpdateCalls } = makeHost();
|
const { host, commitUpdateCalls } = makeHost();
|
||||||
const root = createHostRoot(host, {});
|
const root = createHostRoot(host, {});
|
||||||
root.render(h("div", { class: "old" }, "hello"));
|
root.render(h("div", { class: "old" }, "hello"));
|
||||||
prepareUpdateCalls.length = 0;
|
commitUpdateCalls.length = 0;
|
||||||
|
|
||||||
root.render(h("div", { class: "new" }, "hello"));
|
root.render(h("div", { class: "new" }, "hello"));
|
||||||
expect(prepareUpdateCalls.length).toBeGreaterThanOrEqual(1);
|
expect(commitUpdateCalls.length).toBeGreaterThanOrEqual(1);
|
||||||
const divCall = prepareUpdateCalls.find((c) => c.tag === "div");
|
const divCall = commitUpdateCalls.find((c) => c.tag === "div");
|
||||||
expect(divCall).toBeDefined();
|
expect(divCall).toBeDefined();
|
||||||
expect(divCall!.prevProps.class).toBe("old");
|
expect(divCall!.prevProps.class).toBe("old");
|
||||||
expect(divCall!.nextProps.class).toBe("new");
|
expect(divCall!.nextProps.class).toBe("new");
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ describe("signal-driven-updates", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("wireSignalToFiber", () => {
|
describe("wireSignalToFiber", () => {
|
||||||
it("signal change triggers prepareUpdate + commitUpdate", async () => {
|
it("signal change triggers commitUpdate", async () => {
|
||||||
const { host, prepareUpdateCalls, commitUpdateCalls } = makeTrackingHost();
|
const { host, commitUpdateCalls } = makeTrackingHost();
|
||||||
const color = signal("red");
|
const color = signal("red");
|
||||||
|
|
||||||
const root = createHostRoot(host, {});
|
const root = createHostRoot(host, {});
|
||||||
@@ -77,8 +77,7 @@ describe("signal-driven-updates", () => {
|
|||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(prepareUpdateCalls.length).toBe(0);
|
commitUpdateCalls.length = 0;
|
||||||
expect(commitUpdateCalls.length).toBe(0);
|
|
||||||
|
|
||||||
color.value = "blue";
|
color.value = "blue";
|
||||||
|
|
||||||
@@ -86,16 +85,9 @@ describe("signal-driven-updates", () => {
|
|||||||
queueMicrotask(() => resolve());
|
queueMicrotask(() => resolve());
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(prepareUpdateCalls.length).toBeGreaterThanOrEqual(1);
|
|
||||||
const lastPrepare = prepareUpdateCalls[prepareUpdateCalls.length - 1]!;
|
|
||||||
expect(lastPrepare.tag).toBe("div");
|
|
||||||
expect(lastPrepare.prevProps.color).toBe("red");
|
|
||||||
expect(lastPrepare.nextProps.color).toBe("blue");
|
|
||||||
|
|
||||||
expect(commitUpdateCalls.length).toBeGreaterThanOrEqual(1);
|
expect(commitUpdateCalls.length).toBeGreaterThanOrEqual(1);
|
||||||
const lastCommit = commitUpdateCalls[commitUpdateCalls.length - 1]!;
|
const lastCommit = commitUpdateCalls[commitUpdateCalls.length - 1]!;
|
||||||
expect(lastCommit.instance).toBe(divFiber.instance);
|
expect(lastCommit.instance).toBe(divFiber.instance);
|
||||||
expect(lastCommit.payload).toEqual({ color: "blue" });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("signal effect disposer stored in fiber.signalDisposers", () => {
|
it("signal effect disposer stored in fiber.signalDisposers", () => {
|
||||||
@@ -120,7 +112,7 @@ describe("signal-driven-updates", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("disposing signal via signalDisposers stops updates", async () => {
|
it("disposing signal via signalDisposers stops updates", async () => {
|
||||||
const { host, prepareUpdateCalls } = makeTrackingHost();
|
const { host, commitUpdateCalls } = makeTrackingHost();
|
||||||
const color = signal("red");
|
const color = signal("red");
|
||||||
|
|
||||||
const root = createHostRoot(host, {});
|
const root = createHostRoot(host, {});
|
||||||
@@ -142,20 +134,20 @@ describe("signal-driven-updates", () => {
|
|||||||
const disposer = divFiber.signalDisposers.pop()!;
|
const disposer = divFiber.signalDisposers.pop()!;
|
||||||
disposer();
|
disposer();
|
||||||
|
|
||||||
const callCountBefore = prepareUpdateCalls.filter((c) => c.tag === "div").length;
|
const callCountBefore = commitUpdateCalls.filter((c) => c.tag === "div").length;
|
||||||
color.value = "green";
|
color.value = "green";
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
queueMicrotask(() => resolve());
|
queueMicrotask(() => resolve());
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(prepareUpdateCalls.filter((c) => c.tag === "div").length).toBe(callCountBefore);
|
expect(commitUpdateCalls.filter((c) => c.tag === "div").length).toBe(callCountBefore);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("batch of signal changes", () => {
|
describe("batch of signal changes", () => {
|
||||||
it("batch of signal changes results in single reconciliation pass", async () => {
|
it("batch of signal changes results in single reconciliation pass", async () => {
|
||||||
const { host, prepareUpdateCalls, commitUpdateCalls } = makeTrackingHost();
|
const { host, commitUpdateCalls } = makeTrackingHost();
|
||||||
const color = signal("red");
|
const color = signal("red");
|
||||||
const size = signal("small");
|
const size = signal("small");
|
||||||
|
|
||||||
@@ -175,7 +167,6 @@ describe("signal-driven-updates", () => {
|
|||||||
queueMicrotask(() => resolve());
|
queueMicrotask(() => resolve());
|
||||||
});
|
});
|
||||||
resetUpdateQueue();
|
resetUpdateQueue();
|
||||||
prepareUpdateCalls.length = 0;
|
|
||||||
commitUpdateCalls.length = 0;
|
commitUpdateCalls.length = 0;
|
||||||
|
|
||||||
batch(() => {
|
batch(() => {
|
||||||
@@ -187,14 +178,13 @@ describe("signal-driven-updates", () => {
|
|||||||
queueMicrotask(() => resolve());
|
queueMicrotask(() => resolve());
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(prepareUpdateCalls.filter((c) => c.tag === "div").length).toBe(1);
|
|
||||||
expect(commitUpdateCalls.filter((c) => c.tag === "div").length).toBe(1);
|
expect(commitUpdateCalls.filter((c) => c.tag === "div").length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("render() re-renderable", () => {
|
describe("render() re-renderable", () => {
|
||||||
it("second render reconciles props against existing fiber tree", () => {
|
it("second render reconciles props against existing fiber tree", () => {
|
||||||
const { host, prepareUpdateCalls, commitUpdateCalls, instances } = makeTrackingHost();
|
const { host, commitUpdateCalls, instances } = makeTrackingHost();
|
||||||
const root = createHostRoot(host, {});
|
const root = createHostRoot(host, {});
|
||||||
root.render(h("div", { color: "red" }, "hello"));
|
root.render(h("div", { color: "red" }, "hello"));
|
||||||
|
|
||||||
@@ -205,14 +195,10 @@ describe("signal-driven-updates", () => {
|
|||||||
|
|
||||||
expect(instances.length).toBe(instanceCountBefore);
|
expect(instances.length).toBe(instanceCountBefore);
|
||||||
|
|
||||||
const divPrepareCalls = prepareUpdateCalls.filter((c) => c.tag === "div");
|
|
||||||
expect(divPrepareCalls.length).toBe(1);
|
|
||||||
expect(divPrepareCalls[0]!.prevProps.color).toBe("red");
|
|
||||||
expect(divPrepareCalls[0]!.nextProps.color).toBe("blue");
|
|
||||||
|
|
||||||
const divCommitCalls = commitUpdateCalls.filter((c) => c.tag === "div");
|
const divCommitCalls = commitUpdateCalls.filter((c) => c.tag === "div");
|
||||||
expect(divCommitCalls.length).toBe(1);
|
expect(divCommitCalls.length).toBe(1);
|
||||||
expect(divCommitCalls[0]!.payload).toEqual({ color: "blue" });
|
expect(divCommitCalls[0]!.prevProps.color).toBe("red");
|
||||||
|
expect(divCommitCalls[0]!.nextProps.color).toBe("blue");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("second render does not create duplicate instances", () => {
|
it("second render does not create duplicate instances", () => {
|
||||||
@@ -225,16 +211,16 @@ describe("signal-driven-updates", () => {
|
|||||||
expect(instances.length).toBe(1);
|
expect(instances.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prepareUpdate is called for changed props on re-render", () => {
|
it("commitUpdate is called for changed props on re-render", () => {
|
||||||
const { host, prepareUpdateCalls } = makeTrackingHost();
|
const { host, commitUpdateCalls } = makeTrackingHost();
|
||||||
const root = createHostRoot(host, {});
|
const root = createHostRoot(host, {});
|
||||||
root.render(h("span", { label: "a" }));
|
root.render(h("span", { label: "a" }));
|
||||||
root.render(h("span", { label: "b" }));
|
root.render(h("span", { label: "b" }));
|
||||||
|
|
||||||
const spanPrepares = prepareUpdateCalls.filter((c) => c.tag === "span");
|
const spanCommits = commitUpdateCalls.filter((c) => c.tag === "span");
|
||||||
expect(spanPrepares.length).toBe(1);
|
expect(spanCommits.length).toBe(1);
|
||||||
expect(spanPrepares[0]!.prevProps.label).toBe("a");
|
expect(spanCommits[0]!.prevProps.label).toBe("a");
|
||||||
expect(spanPrepares[0]!.nextProps.label).toBe("b");
|
expect(spanCommits[0]!.nextProps.label).toBe("b");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -88,33 +88,33 @@ describe("Value.Equal bail-out", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("changed prop still triggers prepareUpdate", () => {
|
describe("changed prop still triggers update", () => {
|
||||||
it("changed prop on parent triggers prepareUpdate", () => {
|
it("changed prop on parent triggers commitUpdate", () => {
|
||||||
const { host, prepareUpdateCalls } = makeTrackingHost();
|
const { host, commitUpdateCalls } = makeTrackingHost();
|
||||||
const root = createHostRoot(host, {});
|
const root = createHostRoot(host, {});
|
||||||
root.render(h("div", { color: "red" }));
|
root.render(h("div", { color: "red" }));
|
||||||
|
|
||||||
prepareUpdateCalls.length = 0;
|
commitUpdateCalls.length = 0;
|
||||||
|
|
||||||
root.render(h("div", { color: "blue" }));
|
root.render(h("div", { color: "blue" }));
|
||||||
|
|
||||||
expect(prepareUpdateCalls.length).toBeGreaterThanOrEqual(1);
|
expect(commitUpdateCalls.length).toBeGreaterThanOrEqual(1);
|
||||||
const divCall = prepareUpdateCalls.find((c) => c.tag === "div");
|
const divCall = commitUpdateCalls.find((c) => c.tag === "div");
|
||||||
expect(divCall).toBeDefined();
|
expect(divCall).toBeDefined();
|
||||||
expect(divCall!.prevProps.color).toBe("red");
|
expect(divCall!.prevProps.color).toBe("red");
|
||||||
expect(divCall!.nextProps.color).toBe("blue");
|
expect(divCall!.nextProps.color).toBe("blue");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("changed child prop triggers prepareUpdate only for child", () => {
|
it("changed child prop triggers commitUpdate only for child", () => {
|
||||||
const { host, prepareUpdateCalls } = makeTrackingHost();
|
const { host, commitUpdateCalls } = makeTrackingHost();
|
||||||
const root = createHostRoot(host, {});
|
const root = createHostRoot(host, {});
|
||||||
root.render(h("div", { color: "red" }, h("span", { label: "a" })));
|
root.render(h("div", { color: "red" }, h("span", { label: "a" })));
|
||||||
|
|
||||||
prepareUpdateCalls.length = 0;
|
commitUpdateCalls.length = 0;
|
||||||
|
|
||||||
root.render(h("div", { color: "red" }, h("span", { label: "b" })));
|
root.render(h("div", { color: "red" }, h("span", { label: "b" })));
|
||||||
|
|
||||||
const spanCalls = prepareUpdateCalls.filter((c) => c.tag === "span");
|
const spanCalls = commitUpdateCalls.filter((c) => c.tag === "span");
|
||||||
expect(spanCalls.length).toBe(1);
|
expect(spanCalls.length).toBe(1);
|
||||||
expect(spanCalls[0]!.prevProps.label).toBe("a");
|
expect(spanCalls[0]!.prevProps.label).toBe("a");
|
||||||
expect(spanCalls[0]!.nextProps.label).toBe("b");
|
expect(spanCalls[0]!.nextProps.label).toBe("b");
|
||||||
@@ -214,17 +214,17 @@ describe("Value.Equal bail-out", () => {
|
|||||||
expect(divFiber.effect).toBeNull();
|
expect(divFiber.effect).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reconcileProps with changed node calls prepareUpdate", () => {
|
it("reconcileProps with changed node produces an update effect", () => {
|
||||||
const { host, prepareUpdateCalls } = makeTrackingHost();
|
const { host } = makeTrackingHost();
|
||||||
const root = createHostRoot(host, {});
|
const root = createHostRoot(host, {});
|
||||||
root.render(h("div", { color: "red" }));
|
root.render(h("div", { color: "red" }));
|
||||||
|
|
||||||
const divFiber = root.rootFiber!.children[0]!;
|
const divFiber = root.rootFiber!.children[0]!;
|
||||||
prepareUpdateCalls.length = 0;
|
|
||||||
|
|
||||||
reconcileProps(divFiber, h("div", { color: "blue" }), host as HostConfig<string, string, unknown>, {});
|
reconcileProps(divFiber, h("div", { color: "blue" }), host as HostConfig<string, string, unknown>, {});
|
||||||
|
|
||||||
expect(prepareUpdateCalls.length).toBe(1);
|
expect(divFiber.effect).not.toBeNull();
|
||||||
|
expect(divFiber.props.color).toBe("blue");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user