added basic updates

This commit is contained in:
MasterGordon 2025-08-31 20:59:41 +02:00
parent b9fb2d5a45
commit 04e4278bab
3 changed files with 168 additions and 35 deletions

View File

@ -1,5 +1,6 @@
import { domFiber, ElementSymbol, FiberNodeSymbol } from "./symbols"; import { domFiber, domProps, ElementSymbol, FiberNodeSymbol } from "./symbols";
import { import {
debugEffectTag,
EffectTag, EffectTag,
isFunctionFiber, isFunctionFiber,
isHostFiber, isHostFiber,
@ -11,7 +12,7 @@ import {
type RootFiber, type RootFiber,
} from "./types"; } from "./types";
let unitOfWork: FiberNode | null = null; let unitOfWork: FiberNode | RootFiber | null = null;
let wipFiber: FiberNode; let wipFiber: FiberNode;
let hookIndex: number; let hookIndex: number;
let effectFiber: FiberNode | null = null; 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)) { if (isFunctionFiber(fiber)) {
updateFunctionComponent(fiber); updateFunctionComponent(fiber);
} else if (isHostFiber(fiber)) { } else if (isHostFiber(fiber)) {
updateHostComponent(fiber); updateHostComponent(fiber);
} else if (fiber.isRoot) {
reconcileChildren(fiber, fiber.pendingProps.children);
} }
if (fiber.child) { if (fiber.child) {
@ -90,8 +104,10 @@ function updateFunctionComponent(fiber: FunctionFiber) {
function updateHostComponent(fiber: HostFiber) { function updateHostComponent(fiber: HostFiber) {
wipFiber = fiber; wipFiber = fiber;
if (fiber.type !== textElementType) {
reconcileChildren(fiber, fiber.pendingProps.children); reconcileChildren(fiber, fiber.pendingProps.children);
} }
}
function pushEffect(fiber: FiberNode) { function pushEffect(fiber: FiberNode) {
if (!wipRoot) { if (!wipRoot) {
@ -112,7 +128,10 @@ function pushEffect(fiber: FiberNode) {
wipRoot.lastEffect = fiber; wipRoot.lastEffect = fiber;
} }
function reconcileChildren(wipFiber: FiberNode, children: Child[] = []) { function reconcileChildren(
wipFiber: FiberNode | RootFiber,
children: Child[] = [],
) {
let index = 0; let index = 0;
let oldFiber = wipFiber.alternate?.child; let oldFiber = wipFiber.alternate?.child;
let prevSibling: FiberNode | null = null; let prevSibling: FiberNode | null = null;
@ -202,32 +221,105 @@ function findDomParent(fiber: FiberNode) {
return null; 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() { function commitRoot() {
if (!wipRoot) return; if (!wipRoot) return;
effectFiber = wipRoot.firstEffect; effectFiber = wipRoot.firstEffect;
while (effectFiber) { while (effectFiber) {
console.log(
"commit",
debugEffectTag(effectFiber.effectTag),
typeof effectFiber.type !== "function"
? effectFiber.type
: effectFiber.type.name,
);
// Do effect // Do effect
if (effectFiber.effectTag & EffectTag.Placement) { if (
let dom: Node; effectFiber.effectTag & EffectTag.Placement &&
isHostFiber(effectFiber)
) {
if (effectFiber.type === textElementType) { if (effectFiber.type === textElementType) {
dom = document.createTextNode(effectFiber.pendingProps.nodeValue); effectFiber.dom = document.createTextNode(
effectFiber.pendingProps.nodeValue,
);
} else { } else {
dom = document.createElement(effectFiber.type as string); effectFiber.dom = document.createElement(effectFiber.type as string);
// TODO: Set props updateProps(effectFiber);
// TODO: Maybe add event section (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 // TODO: Maybe add ref in far future might consider a general on mount and on unmount event
} }
effectFiber.dom = dom;
const parent = findDomParent(effectFiber); const parent = findDomParent(effectFiber);
if (parent?.dom) { if (parent?.dom) {
console.log("Appending", dom, "into", parent.dom); parent.dom.appendChild(effectFiber.dom);
parent.dom.appendChild(dom);
} else { } else {
throw "Can't commit without parent"; throw "Can't commit without parent";
} }
} }
effectFiber = effectFiber.nextEffect; 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; wipRoot = null;
} }
@ -264,6 +356,7 @@ const getEventProp = (eventType: string) => {
}; };
function handleEvent(event: Event, root: RootFiber) { function handleEvent(event: Event, root: RootFiber) {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
if (target === root.dom) return;
const type = event.type; const type = event.type;
let node: FiberNode | RootFiber | null = target[domFiber] ?? null; let node: FiberNode | RootFiber | null = target[domFiber] ?? null;
let propagationStopped = false; let propagationStopped = false;
@ -284,7 +377,7 @@ function handleEvent(event: Event, root: RootFiber) {
} }
export function createRootFiber(root: HTMLElement): RootFiber { export function createRootFiber(root: HTMLElement): RootFiber {
const rootFiber: RootFiber = { const alternate: RootFiber = {
$$typeof: FiberNodeSymbol, $$typeof: FiberNodeSymbol,
type: undefined, type: undefined,
key: null, key: null,
@ -302,6 +395,25 @@ export function createRootFiber(root: HTMLElement): RootFiber {
firstEffect: null, firstEffect: null,
lastEffect: 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) { for (const event of events) {
rootFiber.dom.addEventListener(event, (event) => rootFiber.dom.addEventListener(event, (event) =>
handleEvent(event, rootFiber), handleEvent(event, rootFiber),
@ -312,10 +424,15 @@ export function createRootFiber(root: HTMLElement): RootFiber {
export function render(rootFiber: RootFiber, element: JSXElement) { export function render(rootFiber: RootFiber, element: JSXElement) {
wipRoot = rootFiber; wipRoot = rootFiber;
const fiber = createFiberFromElement(element); rootFiber.pendingProps = {
fiber.parent = rootFiber; children: [element],
rootFiber.child = fiber; };
wipRoot.child = fiber; rootFiber.alternate!.pendingProps = {
unitOfWork = fiber; children: [element],
console.log(fiber); };
// const fiber = createFiberFromElement(element);
// fiber.parent = rootFiber;
// rootFiber.child = fiber;
// wipRoot.child = fiber;
unitOfWork = wipRoot;
} }

View File

@ -1,21 +1,23 @@
import { createRootFiber, render } from "./cr"; import { createRootFiber, render, scheduleRerender } from "./cr";
import { domFiber } from "./symbols";
let renderCount = 0;
const App = () => { const App = () => {
console.log("render");
const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
return ( return (
<main> <div
<div>Hello world</div> id="foo"
<ul> onClick={(e) => {
{items.map((item) => ( const fiber = (e.target as HTMLElement)[domFiber];
<li key={item}>{String(item)}</li> scheduleRerender(fiber!);
))} }}
</ul> >
</main> Hello world {String(++renderCount)}
</div>
); );
}; };
console.log("foo");
const root = createRootFiber(document.getElementById("app")!); const root = createRootFiber(document.getElementById("app")!);
console.log(<div>Hello World</div>);
render(root, <App />); render(root, <App />);

View File

@ -25,6 +25,18 @@ export enum EffectTag {
Layout = 0x20, 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 StateHook = Symbol.for("cr.state-hook");
export const EffectHook = Symbol.for("cr.effect-hook"); export const EffectHook = Symbol.for("cr.effect-hook");
type Hook = type Hook =
@ -63,16 +75,18 @@ export type FunctionFiber = FiberNode & {
type: FC; type: FC;
}; };
export type HostFiber = FiberNode & { export type HostFiber = Omit<FiberNode, "dom"> & {
type: string; type: string;
dom: Node | null;
}; };
export type RootFiber = Omit<FiberNode, "type"> & { export type RootFiber = Omit<FiberNode, "type" | "dom" | "alternate"> & {
type: undefined; type: undefined;
dom: HTMLElement; dom: Node;
isRoot: true; isRoot: true;
firstEffect: FiberNode | null; firstEffect: FiberNode | null;
lastEffect: FiberNode | null; lastEffect: FiberNode | null;
alternate: RootFiber | null;
}; };
export const isFiberNode = (fiber: any): fiber is FiberNode => export const isFiberNode = (fiber: any): fiber is FiberNode =>