Introduction to the
This article will write a simple React class framework from scratch. Easy access to the React code base to understand the Fiber principle and hooks implementation.
React.createElement
We’ll start by writing createElement, a function that converts JSX into a virtual DOM (JS object). Here we use @babel/plugin-transform-react-jsx to automatically convert.
// jsx
const element = (
<div id="name">
<a>name</a>
<b />
</div>
)
// At compile time the plug-in converts JSX to
const element = React.createElement(
"div",
{ id: "name" },
React.createElement("a".null."name"),
React.createElement("b"))Copy the code
Here we’re going to put both the properties and the children in the props parameter. Because the text/number type is not an object, we need to wrap the text object here for the sake of brevity (react is used in other ways for performance).
// ----------------- React -----------------
/** * Create text node *@param {*} text
*/
function createTextElement(text) {
return {
type: "TEXT".props: {
nodeValue: text,
children: [].}}; }const React = {};
// type The label name of the DOM node
// All attributes on the attrs node
// children node
React.createElement = function (type, props, ... children) {
return {
type,
props: {
...props,
children: children.map((child) = >
typeof child === "object" ? child : createTextElement(child)
),
},
};
};
Copy the code
Construction Project Details
ReactDOM.render
Next, we implement the React entry reactdom.render (Element, container) function. Receives a virtual DOM object and a real DOM (container). Basically, it takes the virtual DOM and converts it into a real DOM and puts it into a container.
- Determine the virtual DOM type and create a real DOM node based on the type.
- Assign the props of the virtual DOM to the real DOM node.
- Append the real DOM node to the container.
- There are child virtual DOM, looping child nodes, recursively processing each node.
// ----------------- ReactDOM -----------------
const ReactDOM = {};
/ * * * *@param {*} VDom Virtual DOM *@param {*} The container vessel * /
ReactDOM.render = function (vDom, container) {
// Create the real DOM
const dom = vDom.type == "TEXT"
? document.createTextNode("")
: document.createElement(vDom.type);
// Get all attributes except children
const isProperty = (key) = >key ! = ="children";
Object.keys(vDom.props)
.filter(isProperty)
.forEach((name) = > {
dom[name] = vDom.props[name];
});
// Recursive child node
vDom.props.children.forEach((child) = > ReactDOM.render(child, dom));
// The real DOM is put into the container
container.appendChild(dom);
};
// ----------------- use -----------------
ReactDOM.render(<div id="name">1111</div>.document.getElementById('root'));
Copy the code
Using Fiber architecture
What is Fiber architecture: Fiber architecture = Fiber node + Fiber scheduling algorithm
- React Fiber Architecture
- The React Fiber,
Here is not a detailed explanation of the content, for a detailed understanding of the above article.
1. The implementation browser does not interrupt the recursive child nodes of the previous implementation when it is idle. If the DOM tree is large, it can block the main thread, causing the page to stall. So we split the task into units of work, and after each unit is complete, we ask the browser to determine if it has time to continue, and then interrupt. The requestIdleCallback browser used here calls this method every frame and returns how much time is left. React doesn’t use this method, of course, but implements a more powerful Scheduler of its own.
// Unit of work to be performed
let nextUnitOfWork = null;
/** * Determine whether there is time to continue *@param {*} deadline* /
function workLoop(deadline) {
// Determine the remaining time
let shouldYield = false;
while(nextUnitOfWork && ! shouldYield) {// Returns the next unit of work
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (nextUnitOfWork) {
// Unit of work not executed continues to the browserrequestIdleCallback(workLoop); }}/** * Operation node *@param {*} fiber* /
function performUnitOfWork(nextUnitOfWork) {}Copy the code
2. Fiber node
In order to realize the task can interrupt, restore the function. We need a data structure:A Fiber list tree
.
eachFiber tree node
I can find the next one to executenode
What it is. So it’s in every nodeReturn points to its parent Fiber node, child points to its child Fiber node, and sibling points to its brother Fiber nodeAs shown in figure:
The root of the data structure is created in render, and then we assign it to nextUnitOfWork to enter the loop and call performUnitOfWork to process the current node.
inperformUnitOfWork
We need to implement
- Create real DOM nodes.
- The real node goes into the parent container.
- Create Fiber nodes for each child node.
- Find the next unit of work
The search sequence is to find the child node first, there are no children, find the sibling node of the child node, there are no children find the sibling node of the parent node, and keep repeating the same operation. Div #root -> APP -> div. APP -> p -> text -> span -> text.
Let’s start by modifying the Render function and encapsulating the ability to create the real DOM as a public function.
/ / to enter
ReactDOM.render = function (element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
};
requestIdleCallback(workLoop);
}
/** * Create node *@param {*} fiber* /
function createDom(fiber) {
const dom =
fiber.type == "TEXT"
? document.createTextNode("")
: document.createElement(fiber.type);
// Get all attributes except children
const isProperty = (key) = >key ! = ="children";
Object.keys(fiber.props)
.filter(isProperty)
.forEach((name) = > {
dom[name] = fiber.props[name];
});
return dom;
}
Copy the code
Then add the corresponding function in FormUnitofWork.
/** * Operation node *@param {*} fiber* /
function performUnitOfWork(fiber) {
// Create a real node
if(! fiber.dom) { fiber.dom = createDom(fiber); }// Put it in the parent container
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom);
}
// Create a DOM for each child node
const elements = fiber.props.children;
let index = 0;
// Save sibling nodes
let prevSibling = null;
/** * Create fiber nodes for each child node */
while (index < elements.length) {
const element = elements[index];
/ / child nodes
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null};if (index === 0) {
// If it is the first element, set it to a child node
fiber.child = newFiber;
} else {
// The first element is not set as a sibling of the previous one
// Set sibling nodes for the previous node
prevSibling.sibling = newFiber;
}
// Cache the last node
prevSibling = newFiber;
index++;
}
// Find the next unit of work
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
// If there are sibling nodes, return sibling nodes
return nextFiber.sibling;
}
// If there are no siblings, return to the parent -- continue the loop to find the parent's siblings.nextFiber = nextFiber.parent; }}Copy the code
Render and commit phases
Each time a node is processed, the new node created is put into the DOM. Because this process is interruptible, the UI presentation is incomplete. So we remove the code in performUnitOfWork that puts the real DOM into the container.
// if (fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom)
// }
Copy the code
The commit phase is to commit the entire Fiber tree, so we need to create a variable to hold the fiber tree.
/ / fiber tree
let wipRoot = null
/ * * * *@param {*} VDom Virtual DOM *@param {*} The container vessel * /
ReactDOM.render = function (element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
};
nextUnitOfWork = wipRoot
requestIdleCallback(workLoop);
};
Copy the code
You need to commit immediately after the unit of work is executed, add a judgment in the workLoop, commit the entire Fiber tree after execution and render it to the page.
/** * Determine whether there is time to continue *@param {*} deadline* /
function workLoop(deadline) {
// Determine the remaining time
let shouldYield = false;
while(nextUnitOfWork && ! shouldYield) {// Returns the next unit of work
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (nextUnitOfWork) {
// Unit of work not executed continues to the browser
requestIdleCallback(workLoop);
}
if(! nextUnitOfWork && wipRoot) {// Perform the commit render
commitRoot()
}
}
/** * Submit render */
function commitRoot() {
commitWork(wipRoot.child)
wipRoot = null
}
/** * The actual DOM of the node is submitted to the parent DOM *@param {*} fiber
*/
function commitWork(fiber) {
if(! fiber) {return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
// Operates on child and sibling nodes
commitWork(fiber.child)
commitWork(fiber.sibling)
}
Copy the code
Add the Diff comparison
To do this, you need to save the previous Fiber data and add the currentRoot variable. Nodes are not modified during the comparison phase. So define deletions to collect nodes to be deleted during the render phase.
// Last submitted tree
let currentRoot = null
// The node to be deleted after comparison
let deletions = null
/ * * * *@param {*} Element virtual DOM *@param {*} The container vessel * /
ReactDOM.render = function (element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot, // Save the last committed tree at the root node
};
nextUnitOfWork = wipRoot;
deletions = [];
requestIdleCallback(workLoop);
}
/** * Submit render */
function commitRoot() {
// Process the node to be deleted
deletions.forEach(commitWork)
// Process the node
commitWork(wipRoot.child)
currentRoot = wipRoot // Save the submitted tree
wipRoot = null
}
Copy the code
Now it is time to modify the performUnitOfWork function to add the reconcileChildren function to realize the operation of reconcileChildren’s fiber nodes, and to label the nodes at this stage to determine their operation type.
/** * Operation node *@param {*} fiber* /
function performUnitOfWork(fiber) {
// Create a real node
if(! fiber.dom) { fiber.dom = createDom(fiber); }// // into the parent container
// if (fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom);
// }
// Create a DOM for each child node
const elements = fiber.props.children;
// Compare child nodes
reconcileChildren(fiber, elements);
// let index = 0;
// // Save the sibling node
// let prevSibling = null;
/ / / * *
// * Create fiber nodes for each child node
/ / * /
// while (index < elements.length) {
// const element = elements[index];
// // child node
// const newFiber = {
// type: element.type,
// props: element.props,
// parent: fiber,
// dom: null,
/ /};
// if (index === 0) {
// // is set to child if it is the first element
// fiber.child = newFiber;
// } else {
// // is not a sibling of the first element set to the previous
// // sets sibling nodes for the previous node
// prevSibling.sibling = newFiber;
/ /}
// // Caches the last node
// prevSibling = newFiber;
// index++;
// }
// Find the next unit of work
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
// If there are sibling nodes, return sibling nodes
return nextFiber.sibling;
}
// If there are no siblings, return to the parent -- continue the loop to find the parent's siblings.nextFiber = nextFiber.parent; }}/** * compares the child node *@param {*} WipFiber Current operating node *@param {*} Elements Child node */
function reconcileChildren(wipFiber, elements) {
let index = 0;
// Last DOM committed
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
// Save sibling nodes
let prevSibling = null;
/** * Create fiber nodes for each child node */
// oldFiber ! = null As new child nodes become fewer and old nodes continue to be marked with delete labels
while(index < elements.length || oldFiber ! =null) {
const element = elements[index];
const newFiber = null
// The type is the same
let sameType = element && oldFiber && element.type === oldFiber.type;
// Use the same type of the original node to label the modified props
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE"}; }// Create a new node with a new label
if(element && ! sameType) { newFiber = {type: element.type,
props: element.props,
dom: null.parent: wipFiber,
alternate: null.effectTag: "PLACEMENT"}; }// Old nodes of different types are marked with delete labels
if(oldFiber && ! sameType) { oldFiber.effectTag ="DELETION";
deletions.push(oldFiber);// Collect the nodes to be deleted
}
// If no sibling node is set to null, the operation of the current node's old child node is complete
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index === 0) {
// If it is the first element, set it to a child node
wipFiber.child = newFiber;
} else {
// The first element is not set as a sibling of the previous one
// Set sibling nodes for the previous node
prevSibling.sibling = newFiber;
}
// Cache the last nodeprevSibling = newFiber; index++; }}Copy the code
The last step is to modify the commit operation, which determines how to handle the node based on the previously defined label.
/** * modify the DOM node *@param {*} fiber
*/
function commitWork(fiber) {
if(! fiber) {return
}
const domParent = fiber.parent.dom
// domParent.appendChild(fiber.dom)
if(fiber.effectTag === "PLACEMENT"&& fiber.dom ! =null) {// Add an operation
domParent.appendChild(fiber.dom)
}else if(fiber.effectTag === "DELETION"&& fiber.dom ! =null) {// Delete a node
domParent.removeChild(fiber.dom);
}else if(fiber.effectTag === "UPDATE"&& fiber.dom ! =null) {// Modify the node
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}
// Handle sibling and child nodes
commitWork(fiber.child)
commitWork(fiber.sibling)
}
// The prefix on returns true
const isEvent = (key) = > key.startsWith("on");
// Remove children and on
const isProperty = (key) = >key ! = ="children" && !isEvent(key);
// The previous time was different from this time
const isNew = (prev, next) = > (key) = >prev[key] ! == next[key];// Filter to match a value that is not available next time
const isGone = (prev, next) = > (key) = >! (keyin next);
/** * Modify node attributes *@param {*} Dom The actual DOM * of the current node@param {*} PrevProps Props of the previous time@param {*} NextProps this time */
function updateDom(dom, prevProps, nextProps) {
// Empty old events
Object.keys(prevProps)
.filter(isEvent)
.filter((key) = >! (keyin nextProps) || isNew(prevProps, nextProps)(key))
.forEach((name) = > {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[name]);
});
// Empty the old values
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach((name) = > {
dom[name] = "";
});
// Set a new event
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach((name) = > {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
// Set the new value
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach((name) = > {
if(dom instanceof Object && dom.setAttribute){
dom.setAttribute(name, nextProps[name]);
}else{ dom[name] = nextProps[name]; }}); }Copy the code
We implemented a new attribute comparison method, modifying the createDom function to use updateDom.
/** * Create node *@param {*} fiber* /
function createDom(fiber) {
const dom =
fiber.type == "TEXT"
? document.createTextNode("")
: document.createElement(fiber.type);
// Set the properties
updateDom(dom, {}, fiber.props)
// Get all attributes except children
// const isProperty = (key) => key ! == "children";
// Object.keys(fiber.props)
// .filter(isProperty)
// .forEach((name) => {
// dom[name] = fiber.props[name];
/ /});
return dom;
}
Copy the code
Join components
A component differs from a normal DOM in two ways: the Fiber node in the component does not have a real DOM, and the child nodes of the component are returned by execution. So in the performUnitOfWork function, add a way to handle real DOM creation and sub-DOM acquisition separately, depending on the fiber node type.
/** * Operation node *@param {*} fiber* /
function performUnitOfWork(fiber) {
// // Create a real node
// if (! fiber.dom) {
// fiber.dom = createDom(fiber);
// }
// Whether it is a component
const isFunctionComponent = fiber.type instanceof Function;
if (isFunctionComponent) {
/ / component
updateFunctionComponent(fiber);
} else {
updateHostComponent(fiber);
}
// // into the parent container
// if (fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom);
// }
// // creates a DOM for each child node
// const elements = fiber.props.children;
// // Comparison of child nodes
// reconcileChildren(fiber, elements);
// let index = 0;
// // Save the sibling node
// let prevSibling = null;
/ / / * *
// * Create fiber nodes for each child node
/ / * /
// while (index < elements.length) {
// const element = elements[index];
// // child node
// const newFiber = {
// type: element.type,
// props: element.props,
// parent: fiber,
// dom: null,
/ /};
// if (index === 0) {
// // is set to child if it is the first element
// fiber.child = newFiber;
// } else {
// // is not a sibling of the first element set to the previous
// // sets sibling nodes for the previous node
// prevSibling.sibling = newFiber;
/ /}
// // Caches the last node
// prevSibling = newFiber;
// index++;
// }
// Find the next unit of work
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
// If there are sibling nodes, return sibling nodes
return nextFiber.sibling;
}
// If there are no siblings, return to the parent -- continue the loop to find the parent's siblings.nextFiber = nextFiber.parent; }}/** * Non-component creation *@param {*} fiber* /
function updateHostComponent(fiber) {
if(! fiber.dom) { fiber.dom = createDom(fiber); }// Compare child nodes
reconcileChildren(fiber, fiber.props.children);
}
/** * Component creation *@param {*} fiber* /
function updateFunctionComponent(fiber) {
// Run the function to get the child node
const children = [fiber.type(fiber.props)];
// Compare child nodes
reconcileChildren(fiber, children);
}
Copy the code
Next comes the commit phase, because the component doesn’t have a real DOM, so the child nodes need to be put into the component node, the parent node’s real DOM. Delete works the same way. If there is no real DOM, delete the real DOM of the child node.
/** * modify the DOM node *@param {*} fiber
*/
function commitWork(fiber) {
if(! fiber) {return
}
// const domParent = fiber.parent.dom
// domParent.appendChild(fiber.dom)
// Get the parent node
let domParentFiber = fiber.parent;
// Determine if the parent has a real DOM and does not continue to look up
while(! domParentFiber.dom) { domParentFiber = domParentFiber.parent; }// Get the real DOM of the nearest parent
const domParent = domParentFiber.dom;
if(fiber.effectTag === "ADD"&& fiber.dom ! =null) {// Add an operation
domParent.appendChild(fiber.dom)
}else if(fiber.effectTag === "DELETION"&& fiber.dom ! =null) {// Delete a node
// domParent.removeChild(fiber.dom);
commitDeletion(fiber, domParent);
}else if(fiber.effectTag === "UPDATE"&& fiber.dom ! =null) {// Modify the node
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}
// Handle sibling and child nodes
commitWork(fiber.child)
commitWork(fiber.sibling)
}
/** * Delete node *@param {*} fiber
* @param {*} domParent
*/
function commitDeletion(fiber, domParent){
if(fiber.dom){
domParent.removeChild(fiber.dom)
}else{
commitDeletion(fiber.child, domParent)
}
}
Copy the code
Join useState
We know that useState is called while the function is running. In order for state to always save state, we need to initialize some global variables before calling, and we need to initialize hooks data on the Fiber node to save the last state.
let wipFiber = null;// Node for this operation
let hookIndex = null;// Index of state
/** * Component creation *@param {*} fiber* /
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0;// Index position of hook
wipFiber.hooks = []
// Run the function to get the child node and execute useState in the function
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
Copy the code
To fully implement useState, 1. Obtain and save state data. When the function calls useState, it first checks whether the previous fiber node has a value. If there is a value, use the previous value, and then put the value into the current node. (Note that a function can have multiple hooks, adding indexes by order of execution)
/** * hook function *@param {*} initial* /
ReactDOM.useState = function(initial) {
// Get the value of the corresponding index by the order of execution
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
// Use the default value if there is no value
const hook = {
state: oldHook ? oldHook.state : initial,
}
// Save data to queue
wipFiber.hooks.push(hook)
// The index increment function is executed from the top down. This is why react useState cannot be added in a judgment
hookIndex++
// Return the corresponding value
return [hook.state]
}
Copy the code
2. Modify the receiving and executing of status actions. To update the state, we define a setState action to receive the modified state. Add this action to the hooks queue. We then do something similar to what we did in the Render function, setting the fiber node for this operation as the next unit of work and entering the loop. When useState is executed again, it retrieves the previously received change state action and executes all of it, changing the current state value, and finally returning the latest state data.
/** * hook function *@param {*} initial* /
ReactDOM.useState = function(initial) {
// Get the value of the corresponding index by the order of execution
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
// Use the default value if there is no value
// Initializes the data in the modified state
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],}// Get the last modification
const actions = oldHook ? oldHook.queue : []
// Perform operations to modify state
actions.forEach(action= > {
if(action instanceof Function){
hook.state = action(hook.state)
}else{
hook.state = action
}
})
// Change the status
const setState = action= > {
// Save the operation
hook.queue.push(action)
// Update the next unit of work after modification
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot;
deletions = [];
// Add loop
requestIdleCallback(workLoop);
}
// Save data to queue
wipFiber.hooks.push(hook)
hookIndex++ // Index increment
// Returns the latest value
return [hook.state,setState]
}
Copy the code
Test using
// ----------------- use -----------------
const APPS = () = >{
const [state, setState] = ReactDOM.useState(1)
return (
<h1 class="bububu" onClick={()= > {setState(c => c + 1)}}>
Count: {state}
</h1>)}const APPP = () = >{
const [state, setState] = ReactDOM.useState(1)
return (
<h1 class={state= = =2 ? "sss":""} onClick={()= > {setState(c => c + 1)}}>
Countsss: {state}
</h1>)}const APP = () = >{
return (
<div>
<APPS />
<APPP />
</div>
)
}
ReactDOM.render( <APP />.document.getElementById('root'));
Copy the code
Source code address: github.com/nie-ny/reac…
Refer to the article
React Fiber This is probably the most popular way to open a React Fiber