feat: allow to modify rendered tree
This commit is contained in:
parent
36cb4e667d
commit
1dc675661a
3 changed files with 220 additions and 60 deletions
248
src/lib/YarJS.ts
248
src/lib/YarJS.ts
|
|
@ -1,10 +1,16 @@
|
|||
import {
|
||||
YarEffectTag,
|
||||
YarElement,
|
||||
YarFiber,
|
||||
YarHTMLTagName,
|
||||
YarProps,
|
||||
} from "./YarJs.interfaces";
|
||||
|
||||
let FIBER_TO_PROCESS: YarFiber | null | undefined = null;
|
||||
let WIP_ROOT: YarFiber | null | undefined = null;
|
||||
let CURRENT_ROOT: YarFiber | null | undefined = null;
|
||||
let FIBERS_TO_DELETE: YarFiber[] = [];
|
||||
|
||||
/**
|
||||
* This function converts Text into Dom Text Elements
|
||||
*/
|
||||
|
|
@ -37,82 +43,138 @@ export function createElement(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically generate a Dom node depending on it's type
|
||||
* @param fiber - The Fiber Tree element to generate a DOM node
|
||||
* @returns a new DOM Node
|
||||
*/
|
||||
function generateDomNode(fiber: YarFiber) {
|
||||
const dom =
|
||||
fiber.type === "TEXT"
|
||||
? document.createTextNode("")
|
||||
: document.createElement(fiber.type);
|
||||
|
||||
Object.keys(fiber.props)
|
||||
.filter((key) => key !== "children")
|
||||
.forEach((key) => {
|
||||
// @ts-expect-error: I cannot figure it out how to properly type the dom props
|
||||
dom[key] = fiber.props[key];
|
||||
});
|
||||
updateDom(dom, {}, fiber.props);
|
||||
|
||||
return dom;
|
||||
}
|
||||
|
||||
let nextUnitOfWork: YarFiber | null | undefined = null;
|
||||
let wipRoot: YarFiber | null | undefined = null;
|
||||
/**
|
||||
* Apply a set of props to the DOM element without affecting it's placement
|
||||
* @param dom - Element to update
|
||||
* @param prevProps - Previos set of params
|
||||
* @param nextProps - New set of params
|
||||
*/
|
||||
function updateDom(
|
||||
dom: HTMLElement | Text,
|
||||
prevProps: Record<string, unknown>,
|
||||
nextProps: Record<string, unknown>,
|
||||
) {
|
||||
const isEvent = (key: string) => key.startsWith("on");
|
||||
const isProperty = (key: string) => key !== "children" && !isEvent(key);
|
||||
const isNew =
|
||||
(prev: Record<string, unknown>, next: Record<string, unknown>) =>
|
||||
(key: string) =>
|
||||
prev[key] !== next[key];
|
||||
const isGone =
|
||||
(_prev: Record<string, unknown>, next: Record<string, unknown>) =>
|
||||
(key: string) =>
|
||||
!(key in next);
|
||||
|
||||
// Remove old properties
|
||||
Object.keys(prevProps)
|
||||
.filter(isProperty)
|
||||
.filter(isGone(prevProps, nextProps))
|
||||
.forEach((name) => {
|
||||
// @ts-expect-error: I cannot figure it out how to properly type the dom props
|
||||
dom[name] = "";
|
||||
});
|
||||
|
||||
// Set new or changed properties
|
||||
Object.keys(nextProps)
|
||||
.filter(isProperty)
|
||||
.filter(isNew(prevProps, nextProps))
|
||||
.forEach((name) => {
|
||||
// @ts-expect-error: I cannot figure it out how to properly type the dom props
|
||||
dom[name] = nextProps[name];
|
||||
});
|
||||
|
||||
//Remove old or changed event listeners
|
||||
Object.keys(prevProps)
|
||||
.filter(isEvent)
|
||||
.filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key))
|
||||
.forEach((name) => {
|
||||
const eventType = name.toLowerCase().substring(2);
|
||||
dom.removeEventListener(
|
||||
eventType,
|
||||
prevProps[name] as EventListenerOrEventListenerObject,
|
||||
);
|
||||
});
|
||||
|
||||
// Add event listeners
|
||||
Object.keys(nextProps)
|
||||
.filter(isEvent)
|
||||
.filter(isNew(prevProps, nextProps))
|
||||
.forEach((name) => {
|
||||
const eventType = name.toLowerCase().substring(2);
|
||||
dom.addEventListener(
|
||||
eventType,
|
||||
nextProps[name] as EventListenerOrEventListenerObject,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually commit the processed Fiber to the real DOM
|
||||
* @param fiber - The Fiber Element to process
|
||||
*/
|
||||
function commitWork(fiber: YarFiber | null) {
|
||||
if (!fiber) return;
|
||||
|
||||
const domParent = fiber.parent!.dom!;
|
||||
|
||||
switch (fiber.effectTag) {
|
||||
case YarEffectTag.Placement:
|
||||
domParent.appendChild(fiber.dom!);
|
||||
break;
|
||||
case YarEffectTag.Deletion:
|
||||
domParent.removeChild(fiber.dom!);
|
||||
break;
|
||||
case YarEffectTag.Update:
|
||||
updateDom(fiber.dom!, fiber.alternate!.props, fiber.props);
|
||||
break;
|
||||
}
|
||||
|
||||
commitWork(fiber.child);
|
||||
commitWork(fiber.sibling);
|
||||
}
|
||||
|
||||
/** Commit the work to be proccesed into the real DOM from it's root */
|
||||
function commitRoot() {
|
||||
if (!wipRoot) return;
|
||||
if (!WIP_ROOT) return;
|
||||
|
||||
commitWork(wipRoot.child);
|
||||
wipRoot = null;
|
||||
FIBERS_TO_DELETE.forEach(commitWork);
|
||||
commitWork(WIP_ROOT.child);
|
||||
CURRENT_ROOT = WIP_ROOT;
|
||||
WIP_ROOT = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the Fiber Tree, this is a representation of the DOM
|
||||
* Since this process could be interrupted before the whole
|
||||
* tree is processed, we just do the representation and computation here,
|
||||
* then in "commitWork" we add the representation to the actual DOM
|
||||
*
|
||||
* @param fiber - The Fiber to process
|
||||
*/
|
||||
function processFiber(fiber: YarFiber): YarFiber | undefined {
|
||||
// Process the Fiber Tree, this is a representation of the DOM
|
||||
// Since this this process could be interrupted before the whole
|
||||
// tree is processed, we just do the representation and computation here,
|
||||
// then in "commitWork" we add the representation to the actual DOM
|
||||
|
||||
if (!fiber.dom) {
|
||||
// Create the actual dom element
|
||||
fiber.dom = generateDomNode(fiber);
|
||||
}
|
||||
|
||||
// Create a new fiber for each child
|
||||
const elements = fiber.props.children;
|
||||
let index = 0;
|
||||
let prevSibling: YarFiber | null = null;
|
||||
|
||||
while (index < elements.length) {
|
||||
const element = elements[index];
|
||||
|
||||
const newFiber = {
|
||||
parent: fiber,
|
||||
type: element.type,
|
||||
props: element.props,
|
||||
child: null,
|
||||
sibling: null,
|
||||
dom: null,
|
||||
};
|
||||
|
||||
// Each Fiber only holds a reference to it's first child (in the Fiber Tree representation),
|
||||
// so if is the first new Fiber we add it as a child to the parent,
|
||||
// if is not, we add as a sibling of the last child
|
||||
// Nonetheless, each fiber has a reference to it's parent
|
||||
if (index === 0) {
|
||||
fiber.child = newFiber;
|
||||
} else if (prevSibling !== null) {
|
||||
prevSibling.sibling = newFiber;
|
||||
}
|
||||
|
||||
prevSibling = newFiber;
|
||||
index++;
|
||||
}
|
||||
reconcileChildren(fiber, fiber.props.children);
|
||||
|
||||
// Search for the new fiber that needs to be processed
|
||||
if (fiber.child) {
|
||||
|
|
@ -135,17 +197,94 @@ function processFiber(fiber: YarFiber): YarFiber | undefined {
|
|||
}
|
||||
|
||||
/**
|
||||
* Process the children of the Fiber node, deciding for each child
|
||||
* if it needs to be updated, added or deleted based on the last render tree.
|
||||
* @param fiber - Fiber element to process
|
||||
* @param elements - Children to add
|
||||
*/
|
||||
function reconcileChildren(fiber: YarFiber, elements: YarElement[]) {
|
||||
let index = 0;
|
||||
let prevSibling: YarFiber | null = null;
|
||||
let oldFiber = fiber.alternate?.child; // old element rendered
|
||||
let newFiber: YarFiber | null = null;
|
||||
|
||||
while (index < elements.length || oldFiber) {
|
||||
const element = elements[index]; // new element to render
|
||||
|
||||
// NOTE: Here React also uses keys, that makes a better reconciliation.
|
||||
// For example, it detects when children change places in the element array.
|
||||
|
||||
const isSameType = oldFiber?.type === element.type;
|
||||
|
||||
if (isSameType) {
|
||||
// update the node
|
||||
newFiber = {
|
||||
parent: fiber,
|
||||
type: oldFiber!.type,
|
||||
props: element.props,
|
||||
alternate: oldFiber,
|
||||
dom: oldFiber!.dom,
|
||||
child: null,
|
||||
sibling: null,
|
||||
effectTag: YarEffectTag.Update,
|
||||
};
|
||||
}
|
||||
|
||||
if (!isSameType && element !== null && element !== undefined) {
|
||||
// add new node
|
||||
newFiber = {
|
||||
parent: fiber,
|
||||
type: element.type,
|
||||
props: element.props,
|
||||
child: null,
|
||||
sibling: null,
|
||||
dom: null,
|
||||
alternate: null,
|
||||
effectTag: YarEffectTag.Placement,
|
||||
};
|
||||
}
|
||||
|
||||
if (!isSameType && oldFiber !== null && oldFiber !== undefined) {
|
||||
// Delete old node
|
||||
oldFiber!.effectTag = YarEffectTag.Deletion;
|
||||
FIBERS_TO_DELETE.push(oldFiber!);
|
||||
}
|
||||
|
||||
if (oldFiber) {
|
||||
oldFiber = oldFiber.sibling;
|
||||
}
|
||||
|
||||
// Each Fiber only holds a reference to it's first child (in the Fiber Tree representation),
|
||||
// so if is the first new Fiber we add it as a child to the parent,
|
||||
// if is not, we add as a sibling of the last child
|
||||
// Nonetheless, each fiber has a reference to it's parent
|
||||
if (index === 0) {
|
||||
fiber.child = newFiber;
|
||||
} else if (prevSibling !== null) {
|
||||
prevSibling.sibling = newFiber;
|
||||
}
|
||||
|
||||
prevSibling = newFiber;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start processing the fiber tree in the background while it has work to do.
|
||||
*
|
||||
* This function should be passed on requestIdleCallback
|
||||
* so the browser can process it while idle
|
||||
* so the browser can process it while idle.
|
||||
*
|
||||
* @param deadline - IdleDeadline procided by the browser
|
||||
*/
|
||||
function processFiberTree(deadline: IdleDeadline) {
|
||||
let shouldYield = false;
|
||||
while (nextUnitOfWork && !shouldYield) {
|
||||
nextUnitOfWork = processFiber(nextUnitOfWork);
|
||||
while (FIBER_TO_PROCESS && !shouldYield) {
|
||||
FIBER_TO_PROCESS = processFiber(FIBER_TO_PROCESS);
|
||||
shouldYield = deadline.timeRemaining() < 1;
|
||||
}
|
||||
|
||||
if (!nextUnitOfWork && wipRoot) {
|
||||
if (!FIBER_TO_PROCESS && WIP_ROOT) {
|
||||
commitRoot();
|
||||
}
|
||||
|
||||
|
|
@ -155,24 +294,29 @@ function processFiberTree(deadline: IdleDeadline) {
|
|||
requestIdleCallback(processFiberTree);
|
||||
|
||||
/**
|
||||
* Add a new element to render into the DOM
|
||||
* Render an element into a container.
|
||||
* @param element - New element to render
|
||||
* @param container - The parent DOM to add the element into
|
||||
*/
|
||||
export function render(
|
||||
element: React.JSX.Element,
|
||||
container: HTMLElement,
|
||||
): void {
|
||||
wipRoot = {
|
||||
WIP_ROOT = {
|
||||
type: "",
|
||||
dom: container,
|
||||
parent: null,
|
||||
child: null,
|
||||
sibling: null,
|
||||
alternate: CURRENT_ROOT,
|
||||
effectTag: null,
|
||||
props: {
|
||||
children: [element],
|
||||
},
|
||||
};
|
||||
|
||||
nextUnitOfWork = wipRoot;
|
||||
FIBERS_TO_DELETE = [];
|
||||
FIBER_TO_PROCESS = WIP_ROOT;
|
||||
}
|
||||
|
||||
export function Fragment() {}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
export type YarHTMLTagName = keyof React.JSX.IntrinsicElements | string;
|
||||
|
||||
export enum YarEffectTag {
|
||||
Update,
|
||||
Placement,
|
||||
Deletion,
|
||||
}
|
||||
|
||||
export type YarProps = {
|
||||
children: YarElement[];
|
||||
[key: string]: unknown;
|
||||
|
|
@ -15,4 +21,6 @@ export interface YarFiber extends YarElement {
|
|||
dom: null | HTMLElement | Text;
|
||||
child: null | YarFiber;
|
||||
sibling: null | YarFiber;
|
||||
alternate?: null | YarFiber;
|
||||
effectTag: null | YarEffectTag;
|
||||
}
|
||||
|
|
|
|||
22
src/main.tsx
22
src/main.tsx
|
|
@ -3,11 +3,19 @@ import { render } from "./lib/YarJS";
|
|||
|
||||
const root = document.querySelector<HTMLDivElement>("#app")!;
|
||||
|
||||
const element = (
|
||||
<div id="foo">
|
||||
<a>bar</a>
|
||||
<b />
|
||||
</div>
|
||||
);
|
||||
const rerender = (value: string) => {
|
||||
const onChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
rerender(e.currentTarget.value);
|
||||
};
|
||||
|
||||
render(element, root);
|
||||
const element = (
|
||||
<div id="foo">
|
||||
<input type="text" onInput={onChange} value={value} />
|
||||
<h2>Hello {value}</h2>
|
||||
</div>
|
||||
);
|
||||
|
||||
render(element, root);
|
||||
};
|
||||
|
||||
rerender("foo");
|
||||
|
|
|
|||
Reference in a new issue