Please refer to pomb.us/build-your-…
Environment set up
We needed a Vanilla JS environment that could convert JSX. It was easy to set up our development environment using Vite
yarn create vite .
Select Vanilla JS
# install dependencies
yarn
touch vite.config.js
Copy the code
// vite.config.js
export default {
esbuild: {
jsxFactory: "createElement",}};Copy the code
Here we will install the React dependency to compare our implementation and debug
yarn add react react-dom
Copy the code
<body>
<div id="root"></div>
<script type="module" src="/main.jsx"></script>
</body>
Copy the code
// main.jsx
import React, { createElement } from "react";
import ReactDom from "react-dom";
const element = <h1>hello world</h1>;
const root = document.getElementById("root");
ReactDom.render(element, root);
Copy the code
yarn dev
Copy the code
You can see our project running.
createElement
Before we can implement createElement, we need to understand what JSX is.
babel:try it out
How does Babel convert JSX to JS
We print the return value console.log(Element)
Let’s look at the createElement documentation:
React.CreateElement(
type,
[props],
[...children]
)
Copy the code
Creates and returns a new React element of the specified type. The type arguments can be tag name strings (such as ‘div’ or ‘SPAN’), React component types (class or function components), or React Fragment types.
JSX converts to createElement and returns a JS object (React Element).
Let’s implement createElement
function createElement(type, props, ... children) {
return {
type,
props: {
...props,
children: children.map(child= > {
if (typeof child === 'string') {
return createTextElement(child)
}
return child
})
},
}
}
function createTextElement(text) {
return {
type: 'TEXT_ELEMENT'.props: {
nodeValue: text,
children: []}}}Copy the code
Here we treat the text nodes specifically to facilitate the organization of the code.
React:
Our:
Github.com/pomber/dida…
render
With React Element, let’s implement Render. For now we are only concerned with creating the DOM; updates and deletions will be implemented later.
function createDom(element) {
// Create a node
const dom = element.type === 'TEXT_ELEMENT' ?
document.createTextNode(' ') :
document.createElement(element.type)
// Add attributes
const isProperty = key= >key ! = ="children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name= > {
dom[name] = element.props[name]
})
return dom
}
function render(element, container) {
const dom = createDom(element)
// Render child recursively
element.props.children.forEach((child) = > {
render(child, dom)
})
container.appendChild(dom)
}
Copy the code
Github.com/pomber/dida…
concurrent mode
There is a problem with render above: if the render tree is large, it will occupy the main thread for a while. During this time, higher-priority operations such as animation and processing user input are blocked. (the event loop)
We break render into small task units
This will use the browser’s API: requestIdleCallback, react is to realize himself this way
Window. RequestIdleCallback () method inserts a function, this function will be called the browser idle period. This enables developers to perform background and low-priority work on the main event loop without affecting the delay of critical events such as animations and input responses.
let nextUnitOfWork = null function workLoop(deadline) { let shouldYield = false while (nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() < 1 } requestIdleCallback(workLoop) } requestIdleCallback(workLoop) function performUnitOfWork(nextUnitOfWork) { // TODO }Copy the code
Make this workloop run
We’re going to set the first unitOfWork
PerformUnitOfWork (nextUnitOfWork) returns the nextUnitOfWork.
Github.com/pomber/dida…
fiber
To organize unitOfWork, we need a data structure: fiber tree
Our render function does only one thing, setting root fiber to Next Step of work
function render(element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
}
}
Copy the code
In our performUnitOfWork function, we need to do three things
- Add elements to the DOM
- All the children fiber structures that create this element
- Child points to the first child fiber
- Sibling refers to sibling Fiber
- Parent points to the parent fiber
- Return to the next fiber
How to set up the next fiber? Here we use depth-first traversal, find child, no child, find Sibling, no sibling, find parent’s Sibling, all the way to root, this rendering is done.
function performUnitOfWork(fiber) {
//1. Add the element to the DOM
if(! fiber.dom) { fiber.dom = createDom(fiber) }if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
//2. Create all the children fiber structures for this element
// -child points to the first subfiber
// -sibling points to sibling fiber
// -parent points to the parent fiber
const elements = fiber.props.children
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,}// Set child or sibling depending on whether it is the first child
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
/ / have a child
if (fiber.child) {
return fiber.child
}
// No child find sibling or parent sibling
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
Copy the code
Github.com/pomber/dida…
Render and Commit Phases
One problem with the above implementation is that we render node by node, each time the PerformUnited of Work(next Tunit of Work) browser renders, so the user is left with an incomplete UI.
So we need to split rendering into two stages: commit and render
// Set wipRoot and nextUnitOfWork to wipRoot
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}
function workLoop(deadline) {
let shouldYield = false
while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() <1
}
// Commit after unitWork is complete
if(! nextUnitOfWork && wipRoot) { commitRoot() } requestIdleCallback(workLoop) }function commitRoot() {
commitWork(wipRoot.child)
wipRoot = null
}
function commitWork(fiber) {
if(! fiber) {return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function performUnitOfWork(fiber) {
if(! fiber.dom) { fiber.dom = createDom(fiber) }// Do not render, send to commitRoot unified processing
// if (fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom)
// }.Copy the code
Github.com/pomber/dida…
Reconciliation
At this point, we have implemented the first rendering process. The next step is to implement an update
-
Add an alternate property to each Fiber node (including root) to store the last updated oldFiber
-
Two updates with the same fiber.type are considered to be the same element and marked as UPDATE.
Element exists but has a different type and is marked PLACEMENT. The old filber exists but of a different type, and is marked DELETION.
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot
}
deletions = []
nextUnitOfWork = wipRoot
}
// preformNextUnitOfWork adds fiber to all children
function reconcileChildren(wipFiber, elements) {
let index = 0
let oldFiber =
wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
while(index < elements.length || oldFiber ! =null) {
const element = elements[index]
let newFiber = null
// compare oldFiber to element
const sameType = oldFiber && element && element.type === oldFiber.type
if (sameType) {
// update
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",}}if(element && ! sameType) {// add this node
newFiber = {
type: element.type,
props: element.props,
dom: null.parent: wipFiber,
alternate: null.effectTag: "PLACEMENT",}}if(oldFiber && ! sameType) {// TODO delete the oldFiber's node
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
// const newFiber = {
// type: element.type,
// props: element.props,
// parent: fiber,
// dom: null,
// }
if (index === 0) {
wipFiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
Copy the code
- in
commitWork
The DOM is processed according to the tag
function commitWork(fiber) {
if(! fiber) {return
}
const domParent = fiber.parent.dom
if (
fiber.effectTag === "PLACEMENT"&& fiber.dom ! =null
) {
domParent.appendChild(fiber.dom)
} else if (
fiber.effectTag === "UPDATE"&& fiber.dom ! =null
) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
Copy the code
Github.com/pomber/dida…
Function component
The function component has two distinct places
- No DOM nodes
- The children of the function is returned by a call instead of
props.children
Directly acquired
updateFunctionComponent(fiber){
const elements = [fiber.type(fiber.props)]
reconcileChildren(fiber, elements)
}
updateHostComponent(fiber){
if(! fiber.dom) { fiber.dom = createDom(fiber) }const elements = fiber.props.children
reconcileChildren(fiber, elements)
}
function performUnitOfWork(fiber) {
const isFunctionComponent =
fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
Copy the code
On commitWork, continue looking up if the parent element does not have a DOM
function commitWork(fiber) {
if(! fiber) {return
}
// const domParent = fiber.parent.dom
let domParentFiber = fiber.parent
while(! domParentFiber.dom) { domParentFiber = domParentFiber.parent }const domParent = domParentFiber.dom
if (
fiber.effectTag === "PLACEMENT"&& fiber.dom ! =null
) {
domParent.appendChild(fiber.dom)
} else if (
fiber.effectTag === "UPDATE"&& fiber.dom ! =null
) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
Copy the code
Github.com/pomber/dida…
Hooks
let wipFiber = null
// Add 1 each time you call useState
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
// Reset to 0 for each update
hookIndex = 0
// Use hookIndex to track the results of multiple calls to useState
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],}const actions = oldHook ? oldHook.queue : []
actions.forEach(action= > {
hook.state = action(hook.state)
})
// The action is stored in hook. Queque and the update is triggered
const setState = action= > {
hook.queue.push(action)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}
Copy the code
Github.com/pomber/dida…
Personal blog