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 = nullfunction 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:

  1. Add elements to the DOM.
  2. Create fiber for the child element of the element.
  3. 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 = nullwhile (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 = nullwhile (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 = nullwhile (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 = nullwhile (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 = nullwhile( 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 = nullwhile( 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 = nullwhile (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 = nullfunction 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 rerenderedconst 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.