Wire signal-driven updates: scheduleUpdate, flushUpdates, reconcileProps, commitEffects, wireSignalToFiber, re-renderable render()
This commit is contained in:
@@ -2,6 +2,7 @@ import type { UNode, UElement, URoot, ComponentFn, UComponent } from "../core/sc
|
||||
import { isURoot, isUPrimitive } from "../core/schema.js";
|
||||
import { Context } from "../core/context.js";
|
||||
import type { Fiber } from "./fiber.js";
|
||||
import { reconcileProps, commitEffects } from "./reconcile.js";
|
||||
|
||||
export interface HostConfig<TTag extends string, Instance, RootCtx> {
|
||||
name: string;
|
||||
@@ -122,6 +123,19 @@ export function createRoot<TTag extends string, Instance, RootCtx>(
|
||||
context: rootContext,
|
||||
rootFiber: null,
|
||||
render(node: UNode) {
|
||||
if (this.rootFiber) {
|
||||
const payloadChildren = isURoot(node) ? (node as URoot).children : [node];
|
||||
for (let i = 0; i < payloadChildren.length; i++) {
|
||||
const childFiber = this.rootFiber.children[i];
|
||||
if (childFiber) {
|
||||
reconcileProps(childFiber, payloadChildren[i]!, host as HostConfig<string, Instance, unknown>, ctx as unknown);
|
||||
}
|
||||
}
|
||||
commitEffects(this.rootFiber, host as HostConfig<string, Instance, unknown>, ctx as unknown);
|
||||
host.emit?.("root.render", `root_${Date.now()}`, { childCount: payloadChildren.length });
|
||||
return;
|
||||
}
|
||||
|
||||
const root: Fiber<Instance> = {
|
||||
instance: undefined as unknown as Instance,
|
||||
tag: "#root",
|
||||
|
||||
157
src/host/reconcile.ts
Normal file
157
src/host/reconcile.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
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";
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -21,5 +21,7 @@ export type { HostConfig, Root } from "./host/config.js";
|
||||
|
||||
export type { Fiber, Effect } from "./host/fiber.js";
|
||||
|
||||
export { scheduleUpdate, flushUpdates, reconcileProps, commitEffects, wireSignalToFiber, resetUpdateQueue } from "./host/reconcile.js";
|
||||
|
||||
export { TransformRegistry, childCtx, matchesSchema, ctx as transformCtx } from "./transform/registry.js";
|
||||
export type { TransformContext, TransformFn, TransformRule } from "./transform/registry.js";
|
||||
Reference in New Issue
Block a user