Now, we’ll follow the React code architecture and implement our own React version step by step. However, optimizations and non-essential features will not be added this time.
If you’ve read any of my previous React build your own articles, the difference is that this build is based on React version 16.8, so that means we can use hooks instead of classes.
You can read about the react build in previous articles or in the Didact repository. And here’s another video that does the same thing. But this article covers the entire build process and does not rely on previous content.
Starting from the beginning, here are the features we need to add to the React build.
- Step 1: The createElement function
- Step 2: Render function
- Step 3: Concurrent mode (learn more)
- Step 4: Fibers (Learn more)
- Step 5: Render and commit
- Step 6: Reconciliation (Learn more)
- Step 7: Function components
- Hooks (Learn more)
Before WE begin: Review
First let’s review some basic concepts. If you already have a good understanding of how React, JSX, and DOM elements work, skip this step.
Let’s use the React application as an example. This example only takes three lines of code. The first line creates a React element. Next, get a node from the DOM. Finally, render the React element into the acquired DOM node.
const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)
Copy the code
Replace all React code with native JS.
In the first line, an element is created using JSX. But JSX is not valid Javascript code, so you need to use native JS instead.
JSX can be converted to JS by compilation tools such as Babel. The conversion process is usually simple: call the createElement function to replace the code inside the tag, passing in the tag name, the props attribute, and the childen element as parameters.
/ / before the conversion
const element = <h1 title="foo">Hello</h1>
/ / after the transformation
const element = React.createElement(
"h1",
{ title: "foo" },
"Hello"
)
Copy the code
** React. CreateElement ** Creates an object based on the argument. Apart from some validation, that’s all this function does. So we can simply replace the function call with its output.
/ / before the conversion
const element = React.createElement(
"h1",
{ title: "foo" },
"Hello"
)
/ / after the transformation
const element = {
type: "h1".props: {
title: "foo".children: "Hello",}}Copy the code
That’s the essence of a React element, an object with two properties: Type and props (there are more properties, of course, but we’ll focus on those two).
Type is a string that identifies the type of DOM element we want to create. When you want to create an HTML element, type is passed as a tag name argument to the document.createElemnt function. We’ll use it in Step 7.
Props is an object that contains all of the JSX properties, and it has a special property: children.
Children is a string in this example, but it is usually an array with many elements. Each element in the array may also have its own child elements, so all the elements end up in a tree structure.
The other part we need to replace is the call to the reactdom.render function.
Render is where React changes the DOM, so let’s update the DOM ourselves.
The first step is to create a DOM node based on the element type h1 in the example.
We then add attributes from all elements props to the DOM node. All you need to add here is the title attribute.
* (To avoid confusion, I use “element” to refer to React elements and “node” to refer to DOM elements.)
Next we create nodes for children. Here we only have one string as children, so we just need to create a text node.
Use textNode instead of setting innerHTML because you can then create other elements in the same way. Notice how we set the nodeValue, just like we set the title for h1, as if the string had props:{nodeValue:”hello”}.
Finally, we add the text node to the H1 node, and then the H1 node to the Container node retrieved from the DOM.
Now our application is the same as before, but not built with React.
const element = {
type: "h1".props: {
title: "foo".children: "Hello",}}const container = document.getElementById("root")
const node = document.createElement(element.type)
node["title"] = element.props.title
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
node.appendChild(text)
container.appendChild(node)
Copy the code
The first stepcreateElementfunction
Let’s start with another application, this time replacing the React code with our own implementation version of React.
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
Copy the code
The first step is to implement our createElement.
Let’s convert JSX to JS so we can see how the *createElement function is called.
const element = React.createElement(
"div",
{id:"foo"},
React.createElement("a".null."bar"),
React.createElement("b"),const container = document.getElementById("root")
ReactDOM.render(element, container)
Copy the code
As we saw in the previous step, an element is an object that contains properties of Type and props. The only thing our function has to do is create one.
function createElement(type, props, ... children) {
return {
type,
props: {
...props,
children,
},
}
}
Copy the code
We use the extension operator for props and the rest argument syntax for *children, so that the children property will always be an array.
For example, createElement(“div”) returns the following:
{
"type": "div"."props": { "children": []}}Copy the code
CreateElement (“div”,null,a) returns the following:
{
"type": "div"."props": { "children": [a] }
}
Copy the code
CreateElement (“div”,null,a,b) returns the following:
{
"type": "div"."props": { "children": [a,b] }
}
Copy the code
The Children array can also contain data of primitive types such as strings or numbers. So we create a special type: TEXT_ELEMENT to wrap all values that are not objects in their own elements.
React does not wrap primitive type values as we do, creating empty arrays without child elements. We do this for brevity, and for our library, brevity versus performance is preferred.
function createElement(type, props, ... children) {
return {
type,
props: {
...props,
children:children.map(child= >
typeof child === "object"
? child
: createTextElement(child)
),,
},
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT".props: {
nodeValue: text,
children: [],},}}Copy the code
Currently we still call createElement with the React name.
To replace it, let’s give the library a name. We need a name that sounds like React, but also hints at its educational purpose.
We could call it Didact (didactic means teaching).
const Didact = {
createElement,
}
const element = Didact.createElement(
"div",
{ id: "foo" },
Didact.createElement("a".null."bar"),
Didact.createElement("b"))Copy the code
But we still want to use JSX here. So how do we tell Babel to use Didact createElement instead of React?
If we comment like this, when Babel transforms JSX, the functions we define are used.
/ * *@jsx Didact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
Copy the code
The second steprenderfunction
Next, we implement the reactdom.render function.
For now, we just need to focus on putting elements into the DOM. How to handle updates and deletions of elements will be discussed later.
function render(element, container) {
// TODO create dom nodes
}
const Didact = {
createElement,
render,
}
Copy the code
We create a DOM node based on the type of the element and then add the new node to the Container element.
function render(element, container) {
const dom = document.createElement(element.type)
container.appendChild(dom)
}
Copy the code
We recursively perform the same operation on each child element.
function render(element, container) {
const dom = document.createElement(element.type)
element.props.children.forEach(child= >
render(child, dom)
)
container.appendChild(dom)
}
Copy the code
We also need to deal with text elements. If the element is of type TEXT_ELEMENT, we need to create a text node.
function render(element, container) {
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
element.props.children.forEach(child= >
render(child, dom)
)
container.appendChild(dom)
}
Copy the code
Finally, all we need to do is add attributes from the element props to the DOM node.
function render(element, container) {
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
// Add attributes from the element props to the DOM node
const isProperty = key= >key ! = ="children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name= > {
dom[name] = element.props[name]
})
element.props.children.forEach(child= >
render(child, dom)
)
container.appendChild(dom)
Copy the code
In this way, we have implemented a library that can render JSX elements into the DOM.
function createElement(type, props, ... children) {
return {
type,
props: {
...props,
children: children.map(child= >
typeof child === "object"
? child
: createTextElement(child)
),
},
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT".props: {
nodeValue: text,
children: [],},}}function render(element, container) {
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type)
const isProperty = key= >key ! = ="children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name= > {
dom[name] = element.props[name]
})
element.props.children.forEach(child= >
render(child, dom)
)
container.appendChild(dom)
}
const Didact = {
createElement,
render,
}
/ * *@jsx Didact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
Didact.render(element, container)
Copy the code
You can see the full code in codesandbox.
Step 3 Concurrent mode
But… We need to do a refactoring of the code before we can add any more features.
There is a problem with recursive calls:
Once the rendering starts, it doesn’t stop. Imagine a page with so many elements that it blocks the main process during rendering. If the browser wants to handle high-priority tasks like user input or keeping the animation flowing, it has to wait until all elements have been rendered, which can be a very unpleasant experience for the user.
So what we need to do is break down the entire task into smaller sub-tasks, and the browser can break down the rendering process after each small task and move on to something else.
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
We use reqeustIdleCallback to create a circular task. You can equate requestIdleCallback with setTimeout. Except that setTimeout requires us to tell it when to execute, whereas requestIdleCallback performs the callback when the browser’s main process is idle.
React uses the scheduler package instead of requestIdleCallback, but conceptually the two are the same.
The requestIdleCallback provides a deadline parameter that tells you how much time is left before the browser takes back control.
As of November 2019, the Cocurrent model is still unstable. The stable version of the loop looks more like this:
while (nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
}
Copy the code
To start with the loop, we need to set up the initial subtask, and then we need to create a performUnitWork function that not only performs the current subtask, but also returns the next subtask to execute.
Step 4. Fibers
We use a data structure called a Fiber tree to connect all the subtasks.
Each element corresponds to a fiber, and each fiber is a subtask.
Let me give you an example.
If we want to render the following elements:
Didact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
Copy the code
inrenderIn, we will create a root fiber and set it tonextUnitOfWork. The rest of the work will be inperformUnitOfWorkIn this function we do three things for each fiber:
- Add elements to the DOM.
- Create fiber for the child element of the element.
- Find the next subtask.
One of the purposes of using this data structure is to make it easier to find the next subtask. This is why each fiber points to its first child fiber, its adjacent sibling Fiber, and its parent fiber.
When a fiber task is completed, if the fiber has a subfiber, that subfiber is the next subtask to be executed.
For example, when you’re done with div fiber, you’re done with H1 fiber.
If the current Fiber does not have a subfiber, then its sibling fiber is the next subtask to execute.
For example, p fiber has no sub-fiber, so we will deal with a fiber after the current fiber task ends.
If the current fiber has neither a subfiber nor a sibling fiber. You need to go to “uncle” : The father of brother Fiber fiber. Like the EXAMPLE of a and H2 fiber.
If the parent fiber does not have a sibling fiber, we need to continue looking up until we find a fiber node with a sibling fiber or reach the root fiber. If you reach the root fiber, you have completed all the rendering work.
Now let’s add the above processing logic to the code.
First, you need to remove the code that creates the DOM node from the Render function.
We put the logic for creating DOM nodes in the createDom function alone, which we’ll use later.
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
const isProperty = key= >key ! = ="children"
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name= > {
dom[name] = fiber.props[name]
})
return dom
}
Copy the code
In the render function, we assign the root node of the fiber tree to nextUnitOfWork.
function render(element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
}
}
let nextUnitOfWork = null
Copy the code
After that, if the browser has free time, it calls our workLoop and starts working from the root node.
First, we create a node and add it to the DOM.
We can get this DOM node in fiber.dom.
function performUnitOfWork(fiber) {
// Add the element to the DOM
if(! fiber.dom) { fiber.dom = createDom(fiber) }if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
// TODO creates the fiber node for the child element
// TODO returns the next subtask
}
Copy the code
After that, we create a new Fiber node for each child element.
And add it to the Fiber tree as a child or sibling, depending on whether it’s the first child.
function performUnitOfWork(fiber) {
// Add the element to the DOM
if(! fiber.dom) { fiber.dom = createDom(fiber) }if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
// Create a fiber node for the child element
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,}if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
// TODO returns the next subtask
}
Copy the code
Finally, we find the next subtask in the order of child nodes, brother nodes, uncle nodes and so on.
function performUnitOfWork(fiber) {
// Add the element to the DOM
if(! fiber.dom) { fiber.dom = createDom(fiber) }if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
// Create a fiber node for the child element
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,}if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
// Returns the next subtask
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
Copy the code
That’s all the logic of FormUnitofwork.
Step 5: Render and commit
After the above series of treatment, another problem appeared.
When we process an element, we create a node for it to add to the DOM. Remember, however, that the browser interrupts the rendering process and the user is left with an incomplete UI, which is not what we want to see.
So we need to remove the code adding the DOM from the performUnitOfWork function.
function performUnitOfWork(fiber) {
// Add the element to the DOM
if(! fiber.dom) { fiber.dom = createDom(fiber) }// Delete the part where the DOM is added
// if (fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom)
/ /}
// Create a fiber node for the child element
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,}if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
// Returns the next subtask
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
Copy the code
We add a new variable pointing to the root node of the Fiber tree. We can name it Work in Progress root (the root node in the ongoing workflow) or wipRoot.
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let wipRoot = null
Copy the code
Thus, the entire Fiber tree will not be committed to the DOM until all of the rendering work is done (at which point there are no subtasks).
We handle the add logic in the commitRoot function. All nodes are added to the DOM recursively in the function.
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 workLoop(deadline) {
let shouldYield = false
while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() <1
}
// Commit the entire Fiber tree to the DOM node until there is no more subtask
if(! nextUnitOfWork && wipRoot) { commitRoot() } requestIdleCallback(workLoop) } requestIdleCallback(workLoop)Copy the code
Step 6: Reconciliation
So far, we’ve just added elements to the DOM, but how do we update or delete nodes?
And that’s exactly what we’re going to do. We need to compare the element returned in the Render function to the fiber tree we last committed to the DOM.
So we need to save the reference to the last Fiber tree after it commits, which we can call currentRoot.
We also need to add an alternate property to each fiber node. This property saves the fiber node that was last submitted to the DOM.
function commitRoot() {
commitWork(wipRoot.child)
// Update the current fiber tree after DOM is updated
currentRoot = wipRoot
wipRoot = null
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
// Add currentRoot to save the fiber tree currently committed to the DOM
let currentRoot = null
let wipRoot = null
Copy the code
Now we move the logic in performUnitOfWork for creating the new Fiber nodes into a new reconcileChildren function.
function performUnitOfWork(fiber) {
if(! fiber.dom) { fiber.dom = createDom(fiber) }const elements = fiber.props.children
// Move the logic for creating the new Fiber node to the reconcileChildren function
reconcileChildren(fiber, elements)
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
function reconcileChildren(wipFiber, elements) {
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: wipFiber,
dom: null,}if (index === 0) {
wipFiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
Copy the code
In the reconcileChildren function, we reconcile (compare and reuse) the old fiber with the new element.
We iterate over both the children of the old fiber and the array of elements we want to coordinate.
If we ignore the iterative code, then only the most important parts are left: the old Fiber, which is what we want to render into the DOM, and the old Fiber, which is what we rendered last time.
We need to compare them to see if any DOM changes need to be made.
function reconcileChildren(wipFiber, elements) {
let index = 0
// Get the old fiber node
let oldFiber =
wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
while( index < elements.length || oldFiber ! =null) {const element = elements[index]
let newFiber = null;
// TODO compares the old Fiber node to the element here
if(oldFiber){
oldFiber = oldFiber.sibling;
}
if (index === 0) {
wipFiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
Copy the code
We use the type attribute to compare them:
- If the previous fiber node is the same as the new element type attribute, keep the DOM node and just update the new props attribute.
- If the Type attribute is different and there are new elements, this means we need to create a new DOM node.
- If the Type attribute is different and there is an old Fiber node, that node needs to be removed.
* React also uses the key attribute in the coordination phase for better comparison. For example, adding a key property lets you know if the elements in an array are just reversed order, reusing the previous node directly.
function reconcileChildren(wipFiber, elements) {
let index = 0
// Get the old fiber node
let oldFiber =
wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
while( index < elements.length || oldFiber ! =null) {const element = elements[index]
let newFiber = null;
// Compare the old Fiber node to the element here
const sameType =
oldFiber &&
element &&
element.type == oldFiber.type
if (sameType) {
// TODO updates the node
}
if(element && ! sameType) {// TODO adds nodes
}
if(oldFiber && ! sameType) {// TODO removes nodes in the old fiber
}
if(oldFiber){
oldFiber = oldFiber.sibling;
}
if (index === 0) {
wipFiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
Copy the code
When the old Fiber node has the same type attribute as the new element, we will reuse the DOM object of the old fiber node when we create the new fiber node.
We also need to add an effectTag attribute to the Fiber node. This property will be used later in the commit phase.
if (sameType) {
// Update the node
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",}}Copy the code
In the current example, if the element needs to create a new DOM node, the Fiber’s effectTag property is marked as PLACEMENT.
if(element && ! sameType) {// Add a node
newFiber = {
type: element.type,
props: element.props,
dom: null.parent: wipFiber,
alternate: null.effectTag: "PLACEMENT",}}Copy the code
If the current element needs to be deleted, there is no need to create a new Fiber node, so we mark the effectTag property of the old fiber node as DELETION.
if(oldFiber && ! sameType) {// Remove the nodes in the old fiber
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber) // create deletions. Let's break it down immediately
}
Copy the code
But when we commit the Fiber tree that wipRoot points to into the DOM, we don’t include the old Fiber node.
So we need an array to store the fiber nodes that need to be removed.
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
deletions = [] / / new
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null
// Add deletions to store fiber nodes to be deleted
let deletions = null
Copy the code
This way, when we commit changes to the DOM, we can use this array to remove DOM nodes that are no longer needed.
function commitRoot() {
deletions.forEach(commitWork) //
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
Copy the code
Now, let’s do some processing with the new effectTags property inside the commitWork function.
If the fiber node’s effectTag attribute is PLACEMENT, we simply place the DOM node into the DOM node corresponding to the parent fiber as we did before.
If the attribute is DELETION, we need to remove the DOM node as opposed to the previous step.
If the attribute is UPDATE, we need to UPDATE the existing DOM node based on changes to the props attribute.
function commitWork(fiber) {
if(! fiber) {return
}
const domParent = fiber.parent.dom
// Add the new node to the DOM
if (
fiber.effectTag === "PLACEMENT"&& fiber.dom ! =null
) {
domParent.appendChild(fiber.dom)
}
// Update the node
else if (
fiber.effectTag === "UPDATE"&& fiber.dom ! =null
) {
// Where is updateDom? Hehe, continue to read
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
}
// Delete a node
else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
Copy the code
We handle the logic of updating the DOM in the updateDom function.
We compared the props properties between the old fiber node and the new fiber node, deleted nonexistent properties, set new properties, and updated changed properties.
const isProperty = key= >key ! = ="children"
const isNew = (prev, next) = > key= >prev[key] ! == next[key]const isGone = (prev, next) = > key= >! (keyin next)
function updateDom(dom, prevProps, nextProps) {
// Delete attributes that are no longer needed
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name= > {
dom[name] = ""
})
// Update or set new properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name= > {
dom[name] = nextProps[name]
})
}
Copy the code
In addition, we also need to do special handling for the special PORps property of the event listener. So if the property name of a prop is prefixed with “on”, we need to differentiate it.
If the event handler changes, we remove the event listener from the node.
Then we’ll add the new event handler.
const isEvent = key= > key.startsWith("on")
const isProperty = key= >key ! = ="children" && !isEvent(key)
const isProperty = key= >key ! = ="children"
const isNew = (prev, next) = > key= >prev[key] ! == next[key]const isGone = (prev, next) = > key= >! (keyin next)
function updateDom(dom, prevProps, nextProps) {
// Delete the old event listener
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]
)
})
// Delete attributes that are no longer needed
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name= > {
dom[name] = ""
})
// Update or set new properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name= > {
dom[name] = nextProps[name]
})
// Add new event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name= > {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
}
Copy the code
You can browse the code version with the added coordination mechanism at CodesandBox.
Step 7 function components
The next thing we need to do is add support for functional components.
First, we need to change the example. We use a simple function component that returns an H1 element.
/ * *@jsx Didact.createElement */
function App(props) {
return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
Didact.render(element, container)
Copy the code
Note that if we convert JSX to then it will look like this:
function App(props) {
return Didact.createElement(
"h1".null."Hi ",
props.name
)
}
const element = Didact.createElement(App, {
name: "foo",})Copy the code
Function components are different in two ways:
- The fiber node that represents the function component has no DOM node
- The child nodes are required to run functions rather than directly from the props property
If we decide that the fiber type attribute is a function, we need a different update function to handle it.
function performUnitOfWork(fiber) {
// Determine whether it is a function component, and if so, treat it with special treatment
const isFunctionComponent =
fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
// Create a fiber node for the child element
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,}if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
// Returns the next subtask
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
function updateFunctionComponent(fiber) {
// TODO
}
function updateHostComponent(fiber) {
if(! fiber.dom) { fiber.dom = createDom(fiber) } reconcileChildren(fiber, fiber.props.children) }Copy the code
In updateHostComponent we continue with the previous logic.
In the updateFunctionComponent function, we need to call the function (fiber.type) to get the children element.
In our case, the fiber.type attribute is the App function, which returns an H1 element.
Once we get the child nodes, the subsequent coordination phase is the same as before, nothing changes.
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
Copy the code
What we need to change is the commitWork function.
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
Now we need to make two changes to the Fiber node that does not contain the DOM.
First, to find the parent of a DOM node, we need to look up the Fiber tree until we find a fiber that contains the DOM node.
function commitWork(fiber) {
if(! fiber) {return
}
// Find the parent of the DOM node
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
When we need to remove a node, we also need to keep looking until we find a child node that contains the DOM node.
function commitWork(fiber) {
if(! fiber) {return
}
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") {
// Recurse through commitDelection until the child node is found
commitDeletion(fiber,domParent);
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
Copy the code
Step 8 Hooks
The last step, since we are using the function component, is to add state to it as well.
Let’s replace the example with a classic computed component where each time we click on it, the value in the component increases by 1.
Note that we use didact.usestate to get and update this calculated value.
const Didact = {
createElement,
render,
useState,
}
/ * *@jsx Didact.createElement */
function Counter() {
// Use didact.usestate to create state within the function component (useState will be implemented later)
const [state, setState] = Didact.useState(1)
return (
<h1 onClick={()= > setState(c => c + 1)}>
Count: {state}
</h1>)}const element = <Counter />
const container = document.getElementById("root")
Didact.render(element, container)
Copy the code
In the useState function, the state of the component is stored and updated.
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
// TODO
}
Copy the code
Before calling the function component, we need to declare some global variables for use in the useState function.
First, we create a wipFiber variable (WIP stands for Work in Progress, representing the fiber nodes involved in this rendering).
We also need to add an hooks array to the Fiber node to enable multiple calls to the useState method within the same component. We also need to record the index of the current hook.
let wipFiber = null
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
// TODO
}
Copy the code
When calling useState in a function component, we need to check for the existence of old hooks using fiber’s alternate property and the hook index.
If the old hook exists, we copy the state from the old hook to the new hook. If not, we initialize the state of the hook with the initial value.
We then add the new hook to the fiber node, increase the index value of the hook (pointing to the next hook), and return the processed state (in this case, the calculated value).
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state]
}
Copy the code
And useState also returns a function to update the state, so we need to define a setState function that takes action (in our case, the function that incremented the value by one) as an argument.
We need to add the action to the queue array of hooks.
We then need to do something similar to what we did in the Render function, assign a new fiber to wipRoot as the next task, and the workLoop will start a new render phase.
But we haven’t dealt with the action function yet.
The next time we render the component, we get all the actions from the old hooks and use those actions to handle state, so when we return the state, the value will be updated.
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
// Add action (the function that updates the status) to the queue array
queue:[]
}
// Get action for state
const actions = oldHook ? oldHook.queue : []
actions.forEach(action= > {
hook.state = action(hook.state)
})
// setState adds the action to the hook queue and creates the subtask so that the page is rerendered
const setState = action= > {
hook.queue.push(action)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []
}
wipFiber.hooks.push(hook)
hookIndex++
// Expose state and setState
return [hook.state,setState]
}
Copy the code
So, we built our own React.
You can see the full source code at Codesandbox or Github.
conclusion
In addition to helping you understand how React works, one of the goals of this article is to give you easier insight into the React codebase. That’s why we use the same variable and function names as React almost everywhere.
** For example, if you add a breakpoint to the function component of any real React application, the call stack will show you the following information: **
- workLoop
- performUnitOfWork
- updateFunctionComponent
** The code we implemented didn’t include many React features or optimizations. For example, here’s what React does differently from our implementation: **
- In Didact, the render phase traverses the entire tree. React uses some mechanism to skip subtrees that haven’t changed.
- We also iterate through the entire tree during the commit phase, while React keeps only the fiber node list that has an impact.
- Each time we build a new tree, we create a new object for each fiber. React, however, will reuse the fiber nodes in the previous tree.
- When Didact receives a new update in the render phase, it dismisses the previous working tree and starts over from the root node. React, however, marks each update with an expiration timestamp, which determines the priority of each update.
- There are many more…
** Here are a few more features you can easily add: **
- Add the style object property to props (that is, the style property)
- Flattening an array of child elements
- useEffect hook
- Introduce key mechanisms into the coordination mechanism
If you add any of these or other features to Didact, you can send a PR to the Github repository so that others can see it.
Thanks for reading.
Finally, attach the original address.