Skip to content

Instantly share code, notes, and snippets.

@KairuiLiu
Created March 10, 2024 12:35
Show Gist options
  • Save KairuiLiu/dc8aaab1777425ece3a746e35f3de42c to your computer and use it in GitHub Desktop.
Save KairuiLiu/dc8aaab1777425ece3a746e35f3de42c to your computer and use it in GitHub Desktop.
Basic React Rendering and Hooks Mechanism
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.jsx"></script>
</body>
</html>
/**
* File: main.jsx
* Author: Kairui Liu
* Date: 2024-03-11
* Note: run `pnpm init; pnpm i -D vite` to create and start project
*/
import ReactDOM from './core/ReactDOM';
import React from './core/React';
import Welcome from './components/Welcome';
ReactDOM.createRoot(document.getElementById('root')).render(<Welcome />);
/**
* File: core/React.js
* Author: Kairui Liu
* Date: 2024-03-11
*/
// utils
function isTextNode(node) {
if (typeof node === 'function') return false;
if (node === null || typeof node !== 'object') return true;
const proto = Object.getPrototypeOf(node);
return proto !== Object.prototype && proto !== null;
}
function formatNode(node) {
if (isTextNode(node))
return {
nodeName: 'TEXT_ELEMENT',
props: {
children: [`${node}`],
data: `${node}`,
},
};
node.props ??= {};
node.props.children ??= [];
node.props.children = Array.isArray(node.props.children)
? node.props.children
: [node.props.children];
node.props.children.forEach(formatNode);
return node;
}
function isFunctionComponent(component) {
return component.nodeName instanceof Function;
}
function isSameType(component, alternateComponent) {
return component.nodeName === alternateComponent?.nodeName;
}
function isEvent(k) {
return k.match(/^on[A-Z][a-zA-Z]*$/);
}
function getEventName(k) {
return k.replace(
/^on([A-Z])([a-zA-Z]*)$/,
(_, prefix, suffix) => prefix.toLowerCase() + suffix
);
}
function bindProps(node, k, v) {
if (isEvent(k)) node.addEventListener(getEventName(k), v);
else node[k] = v;
}
function removeProps(node, k, v) {
if (isEvent(k)) node.removeEventListener(getEventName(k), v);
else node[k] = null;
}
// render pipeline
function createElement(nodeName, props = {}, ...children) {
return {
nodeName,
props: {
...props,
children: children.map(formatNode),
},
};
}
function createNode({ nodeName, props }) {
if (nodeName === 'TEXT_ELEMENT')
return document.createTextNode(props.children?.[0] || '');
return document.createElement(nodeName);
}
function render(component, container) {
wipRootFiber = {
dom: container,
component,
parentFiber: null,
firstChildFiber: null,
siblingFiber: null,
childrenFiber: [],
newFiber: null,
useStateCount: 0,
useStateStorage: [],
useEffectCount: 0,
useEffectStorage: [],
};
nextFiber = wipRootFiber;
}
// attribute
function patchProps(node, props, oldProps = {}) {
const oldKey = Object.keys(oldProps);
const newKey = Object.keys(props);
newKey
.filter((k) => k !== 'children' && !oldKey.includes(k))
.forEach((k) => bindProps(node, k, props[k]));
oldKey
.filter((k) => k !== 'children' && !newKey.includes(k))
.forEach((k) => removeProps(node, k, oldProps[k]));
newKey
.filter(
(k) => k !== 'children' && oldKey.includes(k) && oldProps[k] !== props[k]
)
.forEach((k) => {
removeProps(node, k, oldProps[k]);
bindProps(node, k, props[k]);
});
}
// fiber loop
const deletions = [];
let nextFiber = null;
let wipRootFiber = null;
let currentRootFiber = null;
function reconcileChildren(fiber, children) {
if (fiber.component.nodeName === 'TEXT_ELEMENT') return;
let alternateFiber = fiber?.alternateFiber?.firstChildFiber;
children.reduce((prev, cur) => {
const curFiber = {
dom: null,
component: cur,
parentFiber: fiber,
firstChildFiber: null,
siblingFiber: null,
effectTag: null,
childrenFiber: [],
newFiber: null,
useStateCount: 0,
useStateStorage: [],
useEffectCount: 0,
useEffectStorage: [],
};
if (alternateFiber) {
if (isSameType(cur, alternateFiber.component)) {
curFiber.dom = alternateFiber.dom;
curFiber.effectTag = 'update';
curFiber.alternateFiber = alternateFiber;
curFiber.useStateStorage = alternateFiber.useStateStorage;
curFiber.useEffectStorage = alternateFiber.useEffectStorage;
alternateFiber.newFiber = curFiber;
} else {
deletions.push(alternateFiber);
curFiber.effectTag = 'placement';
}
} else {
curFiber.effectTag = 'placement';
}
if (!prev) fiber.firstChildFiber = curFiber;
else prev.siblingFiber = curFiber;
if (alternateFiber) alternateFiber = alternateFiber?.siblingFiber;
fiber.childrenFiber.push(curFiber);
return curFiber;
}, null);
if (fiber.alternateFiber) {
fiber.alternateFiber.childrenFiber.forEach(
(d) => !d.newFiber && deletions.push(d)
);
}
}
function getNextFiber(fiber) {
if (fiber.firstChildFiber) return fiber.firstChildFiber;
if (fiber.siblingFiber) return fiber.siblingFiber;
let grandFiber = fiber.parentFiber;
while (grandFiber) {
if (grandFiber === wipRootFiber) return null;
if (grandFiber.siblingFiber) return grandFiber.siblingFiber;
grandFiber = grandFiber.parentFiber;
}
return null;
}
function appendDOM(fiber) {
let containerFiber = fiber.parentFiber;
while (!containerFiber.dom) containerFiber = containerFiber.parentFiber;
containerFiber.dom.append(fiber.dom);
}
function switchFiber(fiber) {
const nextFiber = getNextFiber(fiber);
return nextFiber && applySubmit(nextFiber);
}
function applyDeletions() {
while (deletions.length) {
const fiber = deletions.shift();
if (!isFunctionComponent(fiber.component)) fiber.dom.remove();
else fiber.childrenFiber.forEach((d) => deletions.push(d));
}
}
function applySubmit(fiber) {
if (isFunctionComponent(fiber.component)) return switchFiber(fiber);
if (fiber.effectTag === 'placement') {
appendDOM(fiber);
} else if (fiber.effectTag === 'update') {
patchProps(
fiber.dom,
fiber.component.props,
fiber?.alternateFiber?.component?.props
);
}
switchFiber(fiber);
}
function applyEffect() {
useEffectQueue.forEach((hook) => {
if (hook.cleanUp) hook.cleanUp();
hook.cleanUp = hook.cb();
});
useEffectQueue = [];
}
function processHostFiber(fiber) {
if (!fiber.dom) {
fiber.dom = createNode(fiber.component);
patchProps(fiber.dom, fiber.component.props);
}
reconcileChildren(fiber, fiber.component.props.children);
}
function processFunctionComponentFiber(fiber) {
const component = fiber.component.nodeName(fiber.component.props);
reconcileChildren(fiber, [component]);
}
function performFiber(fiber) {
if (isFunctionComponent(fiber.component))
processFunctionComponentFiber(fiber);
else processHostFiber(fiber);
nextFiber = getNextFiber(fiber);
}
function performFiberLoop(idleDeadline) {
while (idleDeadline.timeRemaining() > 1 && nextFiber) {
performFiber(nextFiber);
}
if (!nextFiber && wipRootFiber) {
applyDeletions();
applySubmit(getNextFiber(wipRootFiber));
applyEffect();
currentRootFiber = wipRootFiber;
wipRootFiber = null;
}
requestIdleCallback(performFiberLoop);
}
requestIdleCallback(performFiberLoop);
// update
function update() {
const currentFiber = nextFiber;
return () => {
currentRootFiber = currentFiber;
wipRootFiber = {
...currentRootFiber,
alternateFiber: currentRootFiber,
childrenFiber: [],
useStateCount: 0,
useEffectCount: 0,
};
nextFiber = wipRootFiber;
};
}
function useState(value) {
const updateComponent = update();
const currentFiber = nextFiber;
const index = currentFiber.useStateCount++;
if (currentFiber.useStateStorage.length <= index) {
currentFiber.useStateStorage.push({
value,
queue: [],
});
}
const hook = currentFiber.useStateStorage[index];
hook.queue.forEach((f) => (hook.value = f(hook.value)));
hook.queue = [];
const setState = (f) => {
const action = f instanceof Function ? f : () => f;
const eagerValue = action(hook.value);
if (hook.value === eagerValue) return;
hook.queue.push(action);
updateComponent();
};
return [currentFiber.useStateStorage[index].value, setState];
}
let useEffectQueue = [];
function useEffect(cb, dep = []) {
const currentFiber = nextFiber;
const effectIndex = currentFiber.useEffectCount++;
if (currentFiber.useEffectStorage.length <= effectIndex) {
const hook = { cb, dep, cleanUp: undefined };
currentFiber.useEffectStorage.push(hook);
useEffectQueue.push(hook);
return;
}
const hook = currentFiber.useEffectStorage[effectIndex];
if (
dep.length !== hook.dep.length ||
dep.some((item, index) => item !== hook.dep[index])
) {
hook.dep = dep;
useEffectQueue.push(hook);
}
}
// export
export default {
render,
createElement,
update,
useState,
useEffect,
};
/**
* File: core/ReactDOM.js
* Author: Kairui Liu
* Date: 2024-03-11
*/
import React from './React.js';
const ReactDOM = {
createRoot(container) {
return {
render(app) {
React.render(app, container);
},
};
},
};
export default ReactDOM;
/**
* File: components/Welcome.jsx
* Author: Kairui Liu
* Date: 2024-03-11
*/
import React from '../core/React';
// Test FC
function TestFC() {
return (
<p>
<TestFCChild num={Date.now()}></TestFCChild>
</p>
);
}
function TestFCChild({ num }) {
return <b>Test FC Value: {num}</b>;
}
// Test Mount
const TestMount = () => {
return (
<>
<h1 id="title">
Hello World <span id="emoji">🤗</span>
</h1>
<p>This is a mini react</p>
Looks Cool !
</>
);
};
// Test Props Update
function TestPropsUpdate() {
const [num, setNum] = React.useState(0);
function handleClick() {
setNum(num + 1);
}
React.useEffect(() => {
console.log('[INIT] Test Props Update Component');
return () => console.error('[CLEANUP-INIT] Test Props Update Component');
}, []);
React.useEffect(() => {
console.log('[UPDATE] Test Props Update Component');
return () => console.log('[CLEANUP-UPDATE] Test Props Update Component');
}, [num]);
return (
<div>
<p id={num}>count is: {num}</p>
<button onClick={handleClick}>num++</button>
</div>
);
}
// Test Type Update
const SpanFC = () => <span>FC Span</span>;
const MarkFC = () => <mark>FC Mark</mark>;
function TestTypeUpdate() {
let [isMark, setIsMark] = React.useState(false);
function handleClick() {
setIsMark((d) => !d);
}
React.useEffect(() => {
console.log('[INIT] Test Type Diff Component');
return () => console.error('[CLEANUP-INIT] Test Type Diff Component');
}, []);
React.useEffect(() => {
console.log('[UPDATE] Test Type Diff Component');
return () => console.log('[CLEANUP-UPDATE] Test Type Diff Component');
}, [isMark]);
return (
<div>
<p>
{isMark ? <mark>Currently: Mark</mark> : <span>Currently: Span</span>}
</p>
<p>{isMark ? <MarkFC /> : <SpanFC />}</p>
<button onClick={handleClick}>Change Type</button>
</div>
);
}
// Test Remove
const BoundFC = () => <span>#</span>;
const InnerFC = () => <span>X</span>;
function TestRemove() {
const [isRemoved, setIsRemoved] = React.useState(false);
React.useEffect(() => {
console.log('[INIT] Test Remove Component');
return () => console.error('[CLEANUP-INIT] Test Remove Component');
}, []);
React.useEffect(() => {
console.log('[UPDATE] Test Remove Component');
return () => console.log('[CLEANUP-UPDATE] Test Remove Component');
}, [isRemoved]);
function handleClick() {
setIsRemoved((d) => !d);
}
return (
<div>
<div>
<span>#</span>
{isRemoved ? (
<span id="Xs"></span>
) : (
<span id="Xs">
<span>X</span>
<span>X</span>
<span>X</span>
<span>X</span>
<span>X</span>
</span>
)}
<span>#</span>
</div>
<div>
<BoundFC />
{isRemoved ? (
<></>
) : (
<>
<InnerFC />
<InnerFC />
<InnerFC />
<InnerFC />
<InnerFC />
</>
)}
<BoundFC />
</div>
<button onClick={handleClick}>Remove X</button>
</div>
);
}
function Welcome() {
return (
<div id="welcome">
<p>Test for: FC, textNode rendering, attribute patching</p>
<TestMount />
<hr />
<p>Test for: FC, fiber</p>
<TestFC />
<TestFC />
<hr />
<p>Test for: update - same type update</p>
<TestPropsUpdate />
<hr />
<p>Test for: update - different type update</p>
<TestTypeUpdate />
<p>Test for: remove</p>
<TestRemove />
</div>
);
}
export default Welcome;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment