271 lines
7.4 KiB
TypeScript
271 lines
7.4 KiB
TypeScript
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, isUElement } from "../core/schema.js";
|
|
|
|
interface PendingUpdate {
|
|
fiber: Fiber<unknown>;
|
|
nextNode: UNode;
|
|
}
|
|
|
|
let pendingUpdates: PendingUpdate[] = [];
|
|
let flushScheduled = false;
|
|
|
|
export function scheduleUpdate<I>(
|
|
fiber: Fiber<I>,
|
|
nextNode: UNode,
|
|
host: HostConfig<string, I, unknown>,
|
|
ctx: unknown,
|
|
): void {
|
|
pendingUpdates.push({ fiber: fiber as Fiber<unknown>, nextNode });
|
|
if (!flushScheduled) {
|
|
flushScheduled = true;
|
|
queueMicrotask(() => flushUpdates(host as HostConfig<string, unknown, unknown>, ctx));
|
|
}
|
|
}
|
|
|
|
export function flushUpdates(
|
|
host: HostConfig<string, unknown, unknown>,
|
|
ctx: unknown,
|
|
): void {
|
|
flushScheduled = false;
|
|
const updates = pendingUpdates.splice(0);
|
|
|
|
for (const { fiber, nextNode } of updates) {
|
|
reconcileProps(fiber, nextNode, host, ctx);
|
|
}
|
|
|
|
const seen = new Set<Fiber<unknown>>();
|
|
for (const { fiber } of updates) {
|
|
let root: Fiber<unknown> | null = fiber;
|
|
while (root.parent) root = root.parent;
|
|
if (!seen.has(root)) {
|
|
seen.add(root);
|
|
commitEffects(root, host, ctx);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function reconcileProps<I>(
|
|
fiber: Fiber<I>,
|
|
nextNode: UNode,
|
|
host: HostConfig<string, I, unknown>,
|
|
ctx: unknown,
|
|
): void {
|
|
if (isUPrimitive(nextNode)) {
|
|
if (fiber.tag === "#text") {
|
|
const text = nextNode === null ? "" : String(nextNode);
|
|
if (fiber.props.text !== text) {
|
|
const nextProps = { text };
|
|
const payload = host.prepareUpdate?.(
|
|
fiber.instance,
|
|
fiber.tag as never,
|
|
fiber.props,
|
|
nextProps,
|
|
ctx as never,
|
|
);
|
|
if (payload !== null && payload !== undefined) {
|
|
fiber.effect = { type: "update", payload };
|
|
fiber.prevProps = { ...fiber.props };
|
|
fiber.props = nextProps;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (isURoot(nextNode)) {
|
|
const rootChildren = nextNode.children;
|
|
const count = Math.min(fiber.children.length, rootChildren.length);
|
|
for (let i = 0; i < count; i++) {
|
|
reconcileProps(fiber.children[i]!, rootChildren[i]!, host, ctx);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const el = nextNode as UElement;
|
|
|
|
if (typeof el.type === "function") {
|
|
const component = el.type as (props: Record<string, unknown>) => UNode;
|
|
const out = component({ ...el.props, children: el.children });
|
|
reconcileProps(fiber, out, host, ctx);
|
|
return;
|
|
}
|
|
|
|
if (fiber.tag !== "#text" && fiber.tag !== el.type) return;
|
|
|
|
const nextProps = el.props as Record<string, unknown>;
|
|
const payload = host.prepareUpdate?.(
|
|
fiber.instance,
|
|
fiber.tag as never,
|
|
fiber.props,
|
|
nextProps,
|
|
ctx as never,
|
|
);
|
|
|
|
if (payload !== null && payload !== undefined) {
|
|
fiber.effect = { type: "update", payload };
|
|
fiber.prevProps = { ...fiber.props };
|
|
fiber.props = nextProps;
|
|
}
|
|
|
|
const count = Math.min(fiber.children.length, el.children.length);
|
|
for (let i = 0; i < count; i++) {
|
|
reconcileProps(fiber.children[i]!, el.children[i]!, host, ctx);
|
|
}
|
|
}
|
|
|
|
export function commitEffects<I>(
|
|
fiber: Fiber<I>,
|
|
host: HostConfig<string, I, unknown>,
|
|
ctx: unknown,
|
|
): void {
|
|
if (fiber.effect?.type === "update") {
|
|
const updateEffect = fiber.effect as Effect<I> & { type: "update"; payload: unknown };
|
|
host.commitUpdate?.(
|
|
fiber.instance,
|
|
updateEffect.payload,
|
|
fiber.tag as never,
|
|
fiber.prevProps ?? {},
|
|
fiber.props,
|
|
ctx as never,
|
|
);
|
|
}
|
|
for (const child of fiber.children) {
|
|
commitEffects(child, host, ctx);
|
|
}
|
|
fiber.effect = null;
|
|
}
|
|
|
|
export function wireSignalToFiber<I>(
|
|
fiber: Fiber<I>,
|
|
signalGetter: () => UNode,
|
|
host: HostConfig<string, I, unknown>,
|
|
ctx: unknown,
|
|
): void {
|
|
const disposer = effect(() => {
|
|
const nextNode = signalGetter();
|
|
scheduleUpdate(fiber, nextNode, host, ctx);
|
|
});
|
|
fiber.signalDisposers.push(disposer);
|
|
}
|
|
|
|
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 };
|
|
} |