From 04e4278babd4e3141537272758158cc0518f6ec3 Mon Sep 17 00:00:00 2001 From: MasterGordon Date: Sun, 31 Aug 2025 20:59:41 +0200 Subject: [PATCH] added basic updates --- examples/client-renderer/src/cr.ts | 159 ++++++++++++++++++++++---- examples/client-renderer/src/main.tsx | 24 ++-- examples/client-renderer/src/types.ts | 20 +++- 3 files changed, 168 insertions(+), 35 deletions(-) diff --git a/examples/client-renderer/src/cr.ts b/examples/client-renderer/src/cr.ts index 9a40d95..8ae100c 100644 --- a/examples/client-renderer/src/cr.ts +++ b/examples/client-renderer/src/cr.ts @@ -1,5 +1,6 @@ -import { domFiber, ElementSymbol, FiberNodeSymbol } from "./symbols"; +import { domFiber, domProps, ElementSymbol, FiberNodeSymbol } from "./symbols"; import { + debugEffectTag, EffectTag, isFunctionFiber, isHostFiber, @@ -11,7 +12,7 @@ import { type RootFiber, } from "./types"; -let unitOfWork: FiberNode | null = null; +let unitOfWork: FiberNode | RootFiber | null = null; let wipFiber: FiberNode; let hookIndex: number; let effectFiber: FiberNode | null = null; @@ -27,11 +28,24 @@ const createTextElement = (text: string): JSXElement => ({ }, }); -function performUnitOfWork(fiber: FiberNode) { +const getRootFiber = (fiber: FiberNode): RootFiber => { + let root = fiber.parent; + while (root) { + if (root.isRoot) { + return root as RootFiber; + } + root = root.parent; + } + throw "Can't find root"; +}; + +function performUnitOfWork(fiber: FiberNode | RootFiber) { if (isFunctionFiber(fiber)) { updateFunctionComponent(fiber); } else if (isHostFiber(fiber)) { updateHostComponent(fiber); + } else if (fiber.isRoot) { + reconcileChildren(fiber, fiber.pendingProps.children); } if (fiber.child) { @@ -90,7 +104,9 @@ function updateFunctionComponent(fiber: FunctionFiber) { function updateHostComponent(fiber: HostFiber) { wipFiber = fiber; - reconcileChildren(fiber, fiber.pendingProps.children); + if (fiber.type !== textElementType) { + reconcileChildren(fiber, fiber.pendingProps.children); + } } function pushEffect(fiber: FiberNode) { @@ -112,7 +128,10 @@ function pushEffect(fiber: FiberNode) { wipRoot.lastEffect = fiber; } -function reconcileChildren(wipFiber: FiberNode, children: Child[] = []) { +function reconcileChildren( + wipFiber: FiberNode | RootFiber, + children: Child[] = [], +) { let index = 0; let oldFiber = wipFiber.alternate?.child; let prevSibling: FiberNode | null = null; @@ -202,32 +221,105 @@ function findDomParent(fiber: FiberNode) { return null; } +function updateProps(fiber: HostFiber) { + const dom = fiber.dom; + if (dom instanceof Text) { + dom.nodeValue = fiber.pendingProps.nodeValue; + return; + } + if (!dom || !(dom instanceof HTMLElement)) { + return; + } + const prevProps = fiber.alternate?.pendingProps ?? {}; + const nextProps = fiber.pendingProps ?? {}; + let propNames = new Set([ + ...Object.keys(prevProps), + ...Object.keys(nextProps), + ]); + for (const propName of propNames) { + if ( + propName === "children" || + propName === "ref" || + propName.startsWith("on") + ) { + continue; + } + const prevProp = prevProps[propName]; + const nextProp = nextProps[propName]; + if (nextProp === undefined) { + dom.removeAttribute(propName); + continue; + } + if (prevProp !== nextProp) { + // Handle special cases + if (propName === "className") { + dom.className = nextProp; + } else if (propName === "htmlFor") { + (dom as HTMLLabelElement).htmlFor = nextProp; + } else if (propName === "style" && typeof nextProp === "object") { + Object.assign(dom.style, nextProp); + } else { + dom.setAttribute(propName, nextProp); + } + } + } +} + +export function scheduleRerender(fiber: FiberNode) { + const root = getRootFiber(fiber); + wipRoot = root.alternate; + unitOfWork = root.alternate; + requestIdleCallback(workLoop); +} + function commitRoot() { if (!wipRoot) return; effectFiber = wipRoot.firstEffect; while (effectFiber) { + console.log( + "commit", + debugEffectTag(effectFiber.effectTag), + typeof effectFiber.type !== "function" + ? effectFiber.type + : effectFiber.type.name, + ); // Do effect - if (effectFiber.effectTag & EffectTag.Placement) { - let dom: Node; + if ( + effectFiber.effectTag & EffectTag.Placement && + isHostFiber(effectFiber) + ) { if (effectFiber.type === textElementType) { - dom = document.createTextNode(effectFiber.pendingProps.nodeValue); + effectFiber.dom = document.createTextNode( + effectFiber.pendingProps.nodeValue, + ); } else { - dom = document.createElement(effectFiber.type as string); - // TODO: Set props - // TODO: Maybe add event section + effectFiber.dom = document.createElement(effectFiber.type as string); + updateProps(effectFiber); + (effectFiber.dom as HTMLElement)[domFiber] = effectFiber; + (effectFiber.dom as HTMLElement)[domProps] = effectFiber.pendingProps; // TODO: Maybe add ref in far future might consider a general on mount and on unmount event } - effectFiber.dom = dom; const parent = findDomParent(effectFiber); if (parent?.dom) { - console.log("Appending", dom, "into", parent.dom); - parent.dom.appendChild(dom); + parent.dom.appendChild(effectFiber.dom); } else { throw "Can't commit without parent"; } } + if (effectFiber.effectTag & EffectTag.Deletion) { + if (effectFiber.dom && effectFiber.dom instanceof HTMLElement) { + effectFiber.dom.remove(); + } + } + if (effectFiber.effectTag & EffectTag.Update && isHostFiber(effectFiber)) { + updateProps(effectFiber); + } effectFiber = effectFiber.nextEffect; + wipRoot.lastEffect = effectFiber; } + + wipRoot.firstEffect = null; + wipRoot.lastEffect = null; wipRoot = null; } @@ -264,6 +356,7 @@ const getEventProp = (eventType: string) => { }; function handleEvent(event: Event, root: RootFiber) { const target = event.target as HTMLElement; + if (target === root.dom) return; const type = event.type; let node: FiberNode | RootFiber | null = target[domFiber] ?? null; let propagationStopped = false; @@ -284,7 +377,7 @@ function handleEvent(event: Event, root: RootFiber) { } export function createRootFiber(root: HTMLElement): RootFiber { - const rootFiber: RootFiber = { + const alternate: RootFiber = { $$typeof: FiberNodeSymbol, type: undefined, key: null, @@ -302,6 +395,25 @@ export function createRootFiber(root: HTMLElement): RootFiber { firstEffect: null, lastEffect: null, }; + const rootFiber: RootFiber = { + $$typeof: FiberNodeSymbol, + type: undefined, + key: null, + child: null, + sibling: null, + parent: null, + index: 0, + dom: root, + hooks: [], + pendingProps: null, + effectTag: EffectTag.NoEffect, + alternate, + nextEffect: null, + isRoot: true, + firstEffect: null, + lastEffect: null, + }; + alternate.alternate = rootFiber; for (const event of events) { rootFiber.dom.addEventListener(event, (event) => handleEvent(event, rootFiber), @@ -312,10 +424,15 @@ export function createRootFiber(root: HTMLElement): RootFiber { export function render(rootFiber: RootFiber, element: JSXElement) { wipRoot = rootFiber; - const fiber = createFiberFromElement(element); - fiber.parent = rootFiber; - rootFiber.child = fiber; - wipRoot.child = fiber; - unitOfWork = fiber; - console.log(fiber); + rootFiber.pendingProps = { + children: [element], + }; + rootFiber.alternate!.pendingProps = { + children: [element], + }; + // const fiber = createFiberFromElement(element); + // fiber.parent = rootFiber; + // rootFiber.child = fiber; + // wipRoot.child = fiber; + unitOfWork = wipRoot; } diff --git a/examples/client-renderer/src/main.tsx b/examples/client-renderer/src/main.tsx index 2cb2026..6434902 100644 --- a/examples/client-renderer/src/main.tsx +++ b/examples/client-renderer/src/main.tsx @@ -1,21 +1,23 @@ -import { createRootFiber, render } from "./cr"; +import { createRootFiber, render, scheduleRerender } from "./cr"; +import { domFiber } from "./symbols"; +let renderCount = 0; const App = () => { + console.log("render"); const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; return ( -
-
Hello world
- -
+
{ + const fiber = (e.target as HTMLElement)[domFiber]; + scheduleRerender(fiber!); + }} + > + Hello world {String(++renderCount)} +
); }; -console.log("foo"); const root = createRootFiber(document.getElementById("app")!); -console.log(
Hello World
); render(root, ); diff --git a/examples/client-renderer/src/types.ts b/examples/client-renderer/src/types.ts index ca4a576..848aba4 100644 --- a/examples/client-renderer/src/types.ts +++ b/examples/client-renderer/src/types.ts @@ -25,6 +25,18 @@ export enum EffectTag { Layout = 0x20, } +export const debugEffectTag = (tag: EffectTag): string => { + const effects = []; + if (tag & EffectTag.Placement) effects.push("Placement"); + if (tag & EffectTag.Update) effects.push("Update"); + if (tag & EffectTag.Deletion) effects.push("Deletion"); + if (tag & EffectTag.Hydration) effects.push("Hydration"); + if (tag & EffectTag.Passive) effects.push("Passive"); + if (tag & EffectTag.Layout) effects.push("Layout"); + if (effects.length === 0) effects.push("NoEffect"); + return effects.join(" | "); +}; + export const StateHook = Symbol.for("cr.state-hook"); export const EffectHook = Symbol.for("cr.effect-hook"); type Hook = @@ -63,16 +75,18 @@ export type FunctionFiber = FiberNode & { type: FC; }; -export type HostFiber = FiberNode & { +export type HostFiber = Omit & { type: string; + dom: Node | null; }; -export type RootFiber = Omit & { +export type RootFiber = Omit & { type: undefined; - dom: HTMLElement; + dom: Node; isRoot: true; firstEffect: FiberNode | null; lastEffect: FiberNode | null; + alternate: RootFiber | null; }; export const isFiberNode = (fiber: any): fiber is FiberNode =>