improved cr by very basic placement commit

This commit is contained in:
MasterGordon 2025-08-29 00:43:54 +02:00
parent 98b0752441
commit f4427f7b3f
9 changed files with 616 additions and 16 deletions

View File

@ -10,6 +10,18 @@
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
<style>
body {
background-color: black;
color: white;
}
button {
background-color: black;
color: white;
border: 1px solid white;
}
</style>
</body> </body>
</html> </html>

View File

@ -1,19 +1,44 @@
interface FiberNode { import { domFiber, ElementSymbol, FiberNodeSymbol } from "./symbols";
child?: FiberNode; import {
sibling?: FiberNode; EffectTag,
parent?: FiberNode; isFunctionFiber,
} isHostFiber,
type Child,
type FiberNode,
type FunctionFiber,
type HostFiber,
type JSXElement,
type RootFiber,
} from "./types";
let unitOfWork: FiberNode | null = null; let unitOfWork: FiberNode | null = null;
let wipFiber: FiberNode;
let hookIndex: number;
let effectFiber: FiberNode | null = null;
let wipRoot: RootFiber | null = null;
const textElementType = "$$TEXT_ELEMENT$$";
const createTextElement = (text: string): JSXElement => ({
$$typeof: ElementSymbol,
type: textElementType,
props: {
nodeValue: text,
children: [],
},
});
function performUnitOfWork(fiber: FiberNode) { function performUnitOfWork(fiber: FiberNode) {
console.log("performUnitOfWork", fiber); if (isFunctionFiber(fiber)) {
updateFunctionComponent(fiber);
} else if (isHostFiber(fiber)) {
updateHostComponent(fiber);
}
if (fiber.child) { if (fiber.child) {
return fiber.child; return fiber.child;
} }
let nextFiber: FiberNode | undefined = fiber; let nextFiber: FiberNode | RootFiber | null = fiber;
while (nextFiber) { while (nextFiber) {
if (nextFiber.sibling) { if (nextFiber.sibling) {
return nextFiber.sibling; return nextFiber.sibling;
@ -31,19 +56,266 @@ function workLoop(deadline: IdleDeadline) {
if (unitOfWork) { if (unitOfWork) {
requestIdleCallback(workLoop); requestIdleCallback(workLoop);
} else { } else if (wipRoot) {
// Commit all the changes that have accumulated. commitRoot();
} }
} }
function linkParents(fiber: FiberNode, parent?: FiberNode) { function createFiberFromElement(element: JSXElement): FiberNode {
fiber.parent = parent; return {
if (fiber.child) { $$typeof: FiberNodeSymbol,
linkParents(fiber.child, fiber); type: element.type,
key: element.key ?? null,
child: null,
sibling: null,
parent: null,
index: 0,
dom: null,
hooks: [],
pendingProps: element.props,
effectTag: EffectTag.NoEffect,
alternate: null,
nextEffect: null,
};
} }
if (fiber.sibling) {
linkParents(fiber.sibling, fiber); function updateFunctionComponent(fiber: FunctionFiber) {
wipFiber = fiber;
hookIndex = 0;
wipFiber.hooks = [];
wipFiber.effectTag = EffectTag.NoEffect;
const children = [fiber.type(fiber.pendingProps)];
reconcileChildren(fiber, children);
}
function updateHostComponent(fiber: HostFiber) {
wipFiber = fiber;
reconcileChildren(fiber, fiber.pendingProps.children);
}
function pushEffect(fiber: FiberNode) {
if (!wipRoot) {
throw "pushEffect() must be called inside a root";
}
if (effectFiber) {
effectFiber.nextEffect = fiber;
effectFiber = fiber;
} else {
effectFiber = fiber;
// @ts-ignore // HACK: Only for debugging
window.effectFiber = fiber;
unitOfWork = fiber;
}
if (!wipRoot.firstEffect) {
wipRoot.firstEffect = fiber;
}
wipRoot.lastEffect = fiber;
}
function reconcileChildren(wipFiber: FiberNode, children: Child[] = []) {
let index = 0;
let oldFiber = wipFiber.alternate?.child;
let prevSibling: FiberNode | null = null;
const elements = children
.filter((child) => child !== null && child !== undefined)
.map((child) => {
if (typeof child === "string") {
return createTextElement(child);
}
return child;
});
while (index < elements.length || oldFiber) {
const element = elements[index];
let newFiber: FiberNode | null = null;
const isSameType = oldFiber && element && element.type === oldFiber.type;
if (isSameType && oldFiber) {
newFiber = {
$$typeof: FiberNodeSymbol,
type: oldFiber.type,
key: element.key ?? null,
child: null,
sibling: null,
parent: wipFiber,
index: 0,
dom: oldFiber.dom,
hooks: [],
pendingProps: element.props,
effectTag: EffectTag.Update,
alternate: oldFiber,
nextEffect: null,
};
pushEffect(newFiber);
}
if (element && !isSameType) {
newFiber = {
$$typeof: FiberNodeSymbol,
type: element.type,
key: element.key ?? null,
child: null,
sibling: null,
parent: wipFiber,
index: 0,
dom: null,
hooks: [],
pendingProps: element.props,
effectTag: EffectTag.Placement,
alternate: null,
nextEffect: null,
};
pushEffect(newFiber);
}
if (oldFiber && !isSameType) {
oldFiber.effectTag = EffectTag.Deletion;
pushEffect(oldFiber);
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index === 0) {
wipFiber.child = newFiber;
} else if (element && prevSibling) {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
} }
} }
requestIdleCallback(workLoop); requestIdleCallback(workLoop);
// TODO: Maybe store next parent HostFiber
function findDomParent(fiber: FiberNode) {
let parent = fiber.parent;
while (parent) {
if (parent.dom) {
return parent;
}
parent = parent.parent;
}
return null;
}
function commitRoot() {
if (!wipRoot) return;
effectFiber = wipRoot.firstEffect;
while (effectFiber) {
// Do effect
if (effectFiber.effectTag & EffectTag.Placement) {
let dom: Node;
if (effectFiber.type === textElementType) {
dom = document.createTextNode(effectFiber.pendingProps.nodeValue);
} else {
dom = document.createElement(effectFiber.type as string);
// TODO: Set props
// TODO: Maybe add event section
// 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);
} else {
throw "Can't commit without parent";
}
}
effectFiber = effectFiber.nextEffect;
}
wipRoot = null;
}
const events: (keyof GlobalEventHandlersEventMap)[] = [
"click",
"mousemove",
"mouseenter",
"mouseleave",
"mouseover",
"mouseout",
"mousedown",
"mouseup",
"pointermove",
"pointerenter",
"pointerleave",
"pointerover",
"pointerout",
"blur",
"focus",
"drag",
"dragend",
"dragenter",
"dragleave",
"dragover",
"dragstart",
"drop",
"touchcancel",
"touchend",
"touchmove",
"touchstart",
];
const getEventProp = (eventType: string) => {
return `on${eventType.substring(0, 1).toUpperCase()}${eventType.substring(1)}`;
};
function handleEvent(event: Event, root: RootFiber) {
const target = event.target as HTMLElement;
const type = event.type;
let node: FiberNode | RootFiber | null = target[domFiber] ?? null;
let propagationStopped = false;
const ogStopPropagation = event.stopPropagation;
event.stopPropagation = () => {
propagationStopped = true;
ogStopPropagation.call(event);
};
do {
node?.pendingProps[getEventProp(type)]?.(event);
if (propagationStopped) {
break;
}
if (event.bubbles) {
node = node?.parent ?? null;
}
} while (node?.dom !== root.dom);
}
export function createRootFiber(root: HTMLElement): RootFiber {
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: null,
nextEffect: null,
isRoot: true,
firstEffect: null,
lastEffect: null,
};
for (const event of events) {
rootFiber.dom.addEventListener(event, (event) =>
handleEvent(event, rootFiber),
);
}
return 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);
}

158
examples/client-renderer/src/jsx.d.ts vendored Normal file
View File

@ -0,0 +1,158 @@
namespace JSX {
interface FC {
(props: any): JSX.Element | string | null;
}
type Children =
| JSX.Element
| (string | JSX.Element | JSX.Element[])[]
| string
| null
| undefined;
type BaseElementPropsWithoutChildren = {
className?: string;
id?: string;
tabindex?: number | string;
style?: import("./types").Styles;
[`data-${string}`]?: string | boolean;
onClick?: (event: MouseEvent) => void;
} & Partial<ARIAMixin>;
type BaseElementProps = BaseElementPropsWithoutChildren & {
children?: JSX.Children;
};
type SvgProps = BaseElementProps & {
["xml:lang"]?: string;
["xml:space"]?: string;
xmlns?: string;
// XLink attributes
["xlink:hrefDeprecated"]?: string;
["xlink:type"]?: string;
["xlink:role"]?: string;
["xlink:arcrole"]?: string;
["xlink:title"]?: string;
["xlink:show"]?: string;
["xlink:actuate"]?: string;
// Presentation attributes
["alignment-baseline"]?: string;
["baseline-shift"]?: string;
["clip"]?: string;
["clipPath"]?: string;
["clipRule"]?: string;
["color"]?: string;
["colorInterpolation"]?: string;
["colorInterpolationFilters"]?: string;
["cursor"]?: string;
["cx"]?: string;
["cy"]?: string;
["d"]?: string;
["direction"]?: string;
["display"]?: string;
["dominantBaseline"]?: string;
["fill"]?: string;
["fillOpacity"]?: string;
["fillRule"]?: string;
["filter"]?: string;
["floodColor"]?: string;
["floodOpacity"]?: string;
["fontFamily"]?: string;
["fontSize"]?: string;
["fontSize-adjust"]?: string;
["fontStretch"]?: string;
["fontStyle"]?: string;
["fontVariant"]?: string;
["fontWeight"]?: string;
["glyphOrientation-horizontal"]?: string;
["glyphOrientation-vertical"]?: string;
["height"]?: string;
["imageRendering"]?: string;
["letterSpacing"]?: string;
["lightingColor"]?: string;
["markerEnd"]?: string;
["markerMid"]?: string;
["markerStart"]?: string;
["mask"]?: string;
["maskType"]?: string;
["opacity"]?: string;
["overflow"]?: string;
["pointerEvents"]?: string;
["r"]?: string;
["rx"]?: string;
["ry"]?: string;
["shapeRendering"]?: string;
["stopColor"]?: string;
["stopOpacity"]?: string;
["stroke"]?: string;
["strokeDasharray"]?: string;
["strokeDashoffset"]?: string;
["strokeLinecap"]?: string;
["strokeLinejoin"]?: string;
["strokeMiterlimit"]?: string;
["strokeOpacity"]?: string;
["strokeWidth"]?: string;
["textAnchor"]?: string;
["textDecoration"]?: string;
["textOverflow"]?: string;
["textRendering"]?: string;
["transform"]?: string;
["transformOrigin"]?: string;
["unicodeBidi"]?: string;
["vectorEffect"]?: string;
["visibility"]?: string;
["whiteSpace"]?: string;
["width"]?: string;
["wordSpacing"]?: string;
["writingMode"]?: string;
["x"]?: string;
["y"]?: string;
viewbox?: string;
};
interface IntrinsicElements {
a: BaseElementProps & {
href?: string;
target?: HTMLAnchorElement["target"];
};
img: BaseElementProps & {
src: string;
alt?: string;
width?: number;
height?: number;
};
// Void IntrinsicElements
// https://github.com/wooorm/html-void-elements
area: BaseElementPropsWithoutChildren;
base: BaseElementPropsWithoutChildren;
basefont: BaseElementPropsWithoutChildren;
bgsound: BaseElementPropsWithoutChildren;
br: BaseElementPropsWithoutChildren;
col: BaseElementPropsWithoutChildren;
command: BaseElementPropsWithoutChildren;
embed: BaseElementPropsWithoutChildren;
frame: BaseElementPropsWithoutChildren;
hr: BaseElementPropsWithoutChildren;
image: BaseElementPropsWithoutChildren;
img: BaseElementPropsWithoutChildren;
input: BaseElementPropsWithoutChildren;
keygen: BaseElementPropsWithoutChildren;
link: BaseElementPropsWithoutChildren;
meta: BaseElementPropsWithoutChildren;
param: BaseElementPropsWithoutChildren;
source: BaseElementPropsWithoutChildren;
track: BaseElementPropsWithoutChildren;
wbr: BaseElementPropsWithoutChildren;
svg: SvgProps;
path: SvgProps;
g: SvgProps;
[tagName: string]: BaseElementProps;
}
interface Element {
$$typeof: symbol;
type: string | FC | null;
key?: string;
ref?: string;
children?: JSX.Children;
props: any;
}
interface ElementChildrenAttribute {
children: {};
}
}

View File

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

View File

@ -0,0 +1,3 @@
import { jsx, Fragment } from "./jsx-runtime";
export const jsxDEV = jsx;
export { jsx, Fragment };

View File

@ -0,0 +1,28 @@
import { ElementSymbol } from "~/symbols";
import type { FC, JSXElement } from "~/types";
export const jsx = function (type: string | FC, fullProps: any): JSXElement {
const { key, children, ref, ...props } = fullProps;
return {
$$typeof: ElementSymbol,
type,
props: {
...props,
children: Array.isArray(children) ? children : [children],
},
key: key,
ref: ref,
};
};
export const Fragment = function (fullProps: any): JSXElement {
const { children, ...props } = fullProps;
return {
$$typeof: ElementSymbol,
type: null,
props,
key: props.key,
ref: props.ref,
children,
};
};

View File

@ -0,0 +1,7 @@
export const ElementSymbol = Symbol.for("cr.element");
export const FragmentSymbol = Symbol.for("cr.fragment");
export const FiberNodeSymbol = Symbol.for("cr.fiber-node");
export type DomFiberKey = "__cr_fiber";
export type DomPropsKey = "__cr_props";
export const domFiber = "__cr_fiber" satisfies DomFiberKey;
export const domProps = "__cr_props" satisfies DomPropsKey;

View File

@ -0,0 +1,95 @@
import { domProps, ElementSymbol, FiberNodeSymbol, domFiber } from "./symbols";
export type FC = (props: any) => JSXElement | string | null;
export type Child = JSXElement | string | null | undefined;
export type Children = Child | Child[];
export type ElementType = string | FC | null;
export interface JSXElement {
$$typeof: typeof ElementSymbol;
type: ElementType;
key?: string;
ref?: string;
props: any;
children?: Children;
}
export enum EffectTag {
NoEffect = 0x0,
Placement = 0x1,
Update = 0x2,
Deletion = 0x4,
Hydration = 0x8,
Passive = 0x10,
Layout = 0x20,
}
export const StateHook = Symbol.for("cr.state-hook");
export const EffectHook = Symbol.for("cr.effect-hook");
type Hook =
| {
type: typeof StateHook;
}
| {
type: typeof EffectHook;
};
export interface FiberNode {
$$typeof: typeof FiberNodeSymbol;
// Identity
type: ElementType;
key: string | null;
isRoot?: boolean;
child: FiberNode | null;
sibling: FiberNode | null;
parent: FiberNode | RootFiber | null;
index: number;
// State / Instance
dom: Node | null;
hooks: Hook[];
pendingProps: any;
effectTag: EffectTag;
// Used for double buffering
alternate: FiberNode | null;
nextEffect: FiberNode | null;
}
export type FunctionFiber = FiberNode & {
type: FC;
};
export type HostFiber = FiberNode & {
type: string;
};
export type RootFiber = Omit<FiberNode, "type"> & {
type: undefined;
dom: HTMLElement;
isRoot: true;
firstEffect: FiberNode | null;
lastEffect: FiberNode | null;
};
export const isFiberNode = (fiber: any): fiber is FiberNode =>
fiber && fiber.$$typeof === FiberNodeSymbol;
export const isElement = (fiber: any): fiber is JSXElement =>
fiber && fiber.$$typeof === ElementSymbol;
export const isFunctionFiber = (fiber: any): fiber is FunctionFiber =>
fiber.type instanceof Function;
export const isHostFiber = (fiber: any): fiber is HostFiber =>
typeof fiber.type === "string";
declare global {
interface HTMLElement {
[domFiber]?: FiberNode;
[domProps]?: any;
}
}

View File

@ -6,6 +6,7 @@
"module": "Preserve", "module": "Preserve",
"moduleDetection": "force", "moduleDetection": "force",
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "~/runtime",
"allowJs": true, "allowJs": true,
// Bundler mode // Bundler mode
@ -24,6 +25,9 @@
// Some stricter flags (disabled by default) // Some stricter flags (disabled by default)
"noUnusedLocals": false, "noUnusedLocals": false,
"noUnusedParameters": false, "noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false "noPropertyAccessFromIndexSignature": false,
"paths": {
"~/*": ["./src/*"]
}
} }
} }