Created
March 10, 2024 12:35
-
-
Save KairuiLiu/dc8aaab1777425ece3a746e35f3de42c to your computer and use it in GitHub Desktop.
Basic React Rendering and Hooks Mechanism
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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 />); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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, | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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