Build your own React

Build Your Own React

preview

Build your own React CN

preface

Rewrite React to follow the architecture in React code, but not optimize it. Based on Act16.8, hooks are used and all class-related code is removed.

Zero: review

First, to review some React concepts, here is a simple React application. The first line defines a React element, the second line gets the DOM node, and the last line renders the React element into the container.

const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)
Copy the code

In the first line, we use JSX. JSX is not a valid JavaScript, and we replace it with native JS. JSX is converted to JS, usually through build tools such as Babel. Replace the JSX tag with createElement and take the tag name, props, and children as parameters.

const element = React.createElement(
  "h1",
  { title: "foo" },
  "Hello"
);
Copy the code

React. CreateElement, which creates an object based on the parameters. Apart from some validation, that’s all react.createElement does. We can simply replace the react. createElement function with its output.

const element = {
  type: "h1".props: {
    title: "foo".children: "Hello",}}Copy the code

A normal JavaScript object with two main properties, type and props. The type attribute is a string that represents the type of DOM node we created. It could also be a function, but we’ll talk about that later. Props is an object with a special property children in props. In the current case, children is a string, but it’s usually an array of more elements. Next we need to replace reactdom.render.

First, use the type attribute to create a node. We assign all props of element to this node, so far only the title attribute is available. Then we create the nodes for the child nodes. Our children is a string, so we create a text node.

Why use createTextNode instead of innerText? Because all elements are treated the same way later on.

Finally, textNode is added to h1, which is added to the Container.

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

We now have the same application as before, but we don’t use React.

A: the createElement method

We started with a new program, this time using our React instead of the React code.

const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
Copy the code

Let’s start by writing our own createElement.

const element = createElement(
  "div",
  { id: "foo" },
  createElement("a".null."bar"),
  createElement("b"))const container = document.getElementById("root")
render(element, container)
Copy the code

All createElement needs to do is create an object of type and props. In createElement, the children argument uses the REST operator; children will always be an array.

function createElement(type, props, ... children) {
  return {
    type,
    props: {
      ...props,
      children,
    },
  }
};
Copy the code

For example, createElement(“div”, null, a, b) returns:

{
  "type": "div"."props": { "children": [a, b] }
}
Copy the code

The children array currently contains raw values, such as strings and numbers. We need to package them. We create a special type, TEXT_ELEMENT.

The React source code does not wrap raw values or create empty arrays without children. We did this to simplify our code.

function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT".props: {
      nodeValue: text,
      children: [],},}}function createElement(type, props, ... children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child= >
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    },
  }
}
Copy the code

How do we get Babel to use our own createElement during compilation? We customize the pragma parameter when configuring the @babel/preset- React plug-in for Babel

2: render

Next we need to write our own reactdom.render.

For now, we’ll just focus on adding content to the DOM and deal with updates and deletions later

We first create a DOM node using the element’s type, and then add the new node to the container

function render(element, container) {
  const dom = document.createElement(element.type)
  container.appendChild(dom)
}
Copy the code

We need to recursively do the same thing for each children 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

A node with text elements added earlier, so you need to determine the type of element when creating the 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, we need to add the props for the element to the properties of the node

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)
}
Copy the code

So far, we have a library that renders JSX to the DOM.

Three: concurrent mode

To do that, we need to refactor the code.

The problem with recursive rendering is that once we start rendering, we can’t stop until we’ve rendered the entire tree. If the tree is large, it will block the main thread for too long.

🤓️: The React Fiber architecture uses a linked list tree to implement interruptible rendering, if you are interested in this article

So we need to break the work down into small units, and after we finish each unit, there are important things to do, and we interrupt the rendering.

We use the requestIdleCallback loop, and the browser executes the requestIdleCallback callback when it is idle. React doesn’t use the requestIdleCallback internally. React uses the Scheduler package internally. RequestIdleCallback also tells us how much time we have available for rendering.

🤓️: More details about requestIdleCallback can be found in this article

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

🤓️: The nextUnitOfWork variable keeps the required work node reference in Fiber or is null, if null, it does not work.

To start our workLoop, we need the first unit of work (the Fiber node) and then write the performUnitOfWork function, which performs the work and returns the next node that needs to work.

Four: Fibers

We need a data structure Fiber tree (linked list tree). Each element has a corresponding Fiber node, and each Fiber is a unit of work.

Suppose we need to render a tree like this:

render(
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>,
  container
)
Copy the code

In Render, create Fiber and assign the Fiber of the root node to the nextUnitOfWork variable. The rest of the work is done in the performUnitOfWork function, which does three things:

  1. Add elements to the DOM
  2. Create Fiber for the subsection
  3. Returns the next unit of work

Fiber tree is a linked list tree, each Fiber node has child, parent, sibling attributes

  • child, the first sub-level reference
  • sibling“, the first peer reference
  • parent, parent reference

🤓️: In the Fiber node of React, the return field preserves the reference to the parent Fiber node

The Fiber tree is traversed by depth-first traversal.

  1. Get the first child node from root
  2. If root has child nodes, set the current pointer to the first child node and proceed to the next iteration. (Depth-first traversal)
  3. If root’s first child has no children, it tries to get its first sibling.
  4. If there are siblings, set the current pointer to the first child, and then the sibling enters the depth-first traversal.
  5. If there are no siblings, the root node is returned. Try to get a sibling of the parent node.
  6. If the parent node has no siblings, the root node is returned. Finally, the traversal is finished.

Ok, so let’s start adding code, separate out the code for the DOM we created and use it later

function createDom(fiber) {
  const dom = fiber.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]
    })
  return dom
}
Copy the code

In the Render function, set the nextUnitOfWork variable to the root of the Fiber node tree

function render(element, container) {
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    },
  }
}
Copy the code

When the browser is ready, call workLoop to start processing the root node

let nextUnitOfWork = nullfunction workLoop(deadline) {
  let shouldYield = false
  while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() <1
  }
  requestIdleCallback(workLoop)
}
​
requestIdleCallback(workLoop)
​
function performUnitOfWork(fiber) {
  // Add a DOM node
  / / create the Fiber
  // Get the Fiber node for the next processing work
}
Copy the code

We first create the DOM and add it to the DOM field of the Fiber node, where we keep the reference to the DOM

function performUnitOfWork(fiber) {
  if(! fiber.dom) { fiber.dom = createDom(fiber) }if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }
}
Copy the code

🤓️: In the React Fiber node, the stateNode field reserves references to class component instances, DOM nodes, or other React element class instances associated with the Fiber node.

Next, create Fiber nodes for each child element. Also, since the Fiber tree is a linked list tree, we need to add child, parent, sibling fields to the Fiber node

function performUnitOfWork(nextUnitOfWork) {
  if(! fiber.dom) { fiber.dom = createDom(fiber) }if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }

  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, // Reference to the parent Fiber node
      dom: null,}if (index === 0) {
      // Add the child field to the parent Fiber node
      fiber.child = newFiber
    } else {
      // Add sibling field to the sibling Fiber node
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}
Copy the code

After finishing the work on the current node, we need to go back to the next node. Since it’s depth-first traversal, first try traversing child, then Sibling, and finally go back to parent and try traversing parent’s Sibling

function performUnitOfWork(nextUnitOfWork) {
  if(! fiber.dom) { fiber.dom = createDom(fiber) }if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }

  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, // A reference to the parent node
      dom: null,}if (index === 0) {
      // Add the child field to the parent Fiber node
      fiber.child = newFiber
    } else {
      // Add sibling field to the sibling Fiber node
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }

  // First try the child node
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    // Try the peer node
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}
Copy the code

Five: render and commit

The current problem, while traversing the Fiber tree, is that we are currently adding new nodes to the DOM here, and since we are using requestIdleCallback, the browser may break our rendering and the user will see an incomplete UI. This violates the principle of consistency.

🤓️: One of the core principles of React is “consistency “, which always updates the DOM once without showing partial results.

🤓️: In the React source code, React is divided into two phases: render and COMMIT. The render phase works asynchronously, with React processing one or more Fiber nodes depending on the available time. React stops and saves the finished work when something more important happens. After the important stuff is done, React picks up where it left off. But sometimes it’s possible to drop what you’ve already done and start over from the top. The work performed in this phase is not visible to the user, so it can be paused. However, the commit phase is always synchronous and results in changes visible to the user, such as DOM modifications. That’s why React needs to do them all in one go.

We need to remove the code in the performUnitOfWork function that changes the DOM.

function performUnitOfWork(nextUnitOfWork) {
  if(! fiber.dom) { fiber.dom = createDom(fiber) }const elements = fiber.props.children

  // ...
Copy the code

We need to keep the Fiber root reference, which we call root or wipRoot working.

🤓️: The root of the Fiber tree is called HostRoot in React. DOM._reactRootContainer. _internalroot.current.

🤓 ️ : WipRoot is similar to the root of the workInProgress Tree in the React source code. We can through the container DOM. _reactRootContainer. _internalRoot. Current. Alternate, obtain workInProgress root node of the tree.

let wipRoot = null

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
  }
  nextUnitOfWork = wipRoot
}
Copy the code

All the work is done. We need to update the entire Fiber tree to the DOM. We need to do this in the commitRoot function.

function commitWork(fiber) {
  if(! fiber) {return
  }
  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  // Recursive child node
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

function commitRoot() {
  commitWork(wipRoot.child)
  wipRoot = null
}

function workLoop(deadline) {
  let shouldYield = false
  while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() <1
  }
  // If nextUnitOfWork is false, all the work is done and we need to enter the commit phase
  if(! nextUnitOfWork && wipRoot) {/ / add the dom
    commitRoot()
  }
}
Copy the code

🤓️: In the React source code, the COMMIT phase starts with the completeRoot function, which sets the FiberRoot finishedWork property to null before starting any work.

Six: coordination

So far, we’ve only added content to the DOM, but what about updates and deletions? We need to compare the elements received by the Render function to the final Fiber tree submitted to the DOM.

Therefore, at commit we need to save the reference to the last Fiber tree, which we call currentRoot. We also add the alternate field, which holds a reference to currentRoot, to each Fiber node.

🤓️: In the React source code, React generates a Fiber tree after the first rendering. This tree maps the state of the application and is called a Current tree. When the application starts updating, React builds a workInProgress Tree, which maps the future state.

🤓️: All work is done on Fiber on the workInProgress Tree. When React starts traversing Fiber, it creates a backup for each existing Fiber node. In the alternate field, the backup forms the workInProgress Tree.

let nextUnitOfWork = null
let wipRoot = null
let currentRoot = null

function commitWork(fiber) {
  if(! fiber) {return
  }
  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  // Recursive child node
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

function commitRoot() {
  commitWork(wipRoot.child)
  // Save the last output to the Fiber tree on the page
  currentRoot = wipRoot
  wipRoot = null
}

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    alternate: currentRoot,
  }
  nextUnitOfWork = wipRoot
}
Copy the code

Next we need to extract the code that creates Fiber from the performUnitOfWork function, a new reconcileChildren function. Here we will coordinate the currentRoot(the Fiber tree corresponding to the current page) with the new element.

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: fiber, // A reference to the parent node
      dom: null,}if (index === 0) {
      // Add the child field to the parent Fiber node, which points to the first child node
      wipFiber.child = newFiber
    } else {
      // Add sibling field to the sibling Fiber node
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}

function performUnitOfWork(fiber) {
  if(! fiber.dom) { fiber.dom = createDom(fiber) }const elements = fiber.props.children
  reconcileChildren(wipFiber, elements)

  // First try the child node
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    // Try the peer node
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}
Copy the code

We walk through the old Fiber tree at the same time, wipFiber. Alternate, and the new elements that need to be reconciled. If we ignore the template code for traversing lists and arrays. So in the while loop, the most important things are oldFiber and Element. Element is the DOM we need to render, oldFiber is the Fiber we rendered last time. We need to compare them to see if the DOM needs any changes.

function reconcileChildren(wipFiber, elements) {
  let index = 0
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child
  let prevSibling = null

  while( index < elements.length || oldFiber ! = =null
  ) {
    const element = elements[index]
​    let newFiber = null

    // TODO compare oldFiber to element

    / /...

    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }

    if (index === 0) {
      // Add the child field to the parent Fiber node, which points to the first child node
      wipFiber.child = newFiber
    } else {
      // Add sibling field to the sibling Fiber node
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}
Copy the code

To compare them we use the following rules:

  1. ifoldFiberandelementWith the same type, we keep the DOM node and update it with the new props
  2. If the type is different and there are new elements. We need to create a new DOM node.
  3. If the type is different and the previous Fiber exists, we need to remove the old node
function reconcileChildren(wipFiber, elements) {
  let index = 0
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child
  let prevSibling = null

  while( index < elements.length || oldFiber ! = =null
  ) {
    const element = elements[index]
    let newFiber = null

    // Check whether it is of the same type
    const sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type

    if (sameType) {
      // Update the node
    }

    if(! sameType && element) {// Add a node
    }

    if(! sameType && oldFiber) {// Delete a node
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }

    if (index === 0) {
      // Add the child field to the parent Fiber node, which points to the first child node
      wipFiber.child = newFiber
    } else {
      // Add sibling field to the sibling Fiber node
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}
Copy the code

React uses a key, which can be used for better coordination. The key can be used to detect whether the position of the element in the list changes, which makes it easier to reuse nodes.

When the previous Fiber and the new element have the same type, we create a new Fiber node that retains the DOM node of the old Fiber and the props of the element.

A new attribute, effectTag, was added to Fiber to be used later in the COMMIT phase

🤓️: In the React source effectTag, effectTag encodes the effects associated with the Fiber node. React effectTags are stored numerically, using bitwise or constructing a set of attributes. Check out more

function reconcileChildren(wipFiber, elements) {
  let index = 0
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child
  let prevSibling = null

  while( index < elements.length || oldFiber ! = =null
  ) {
    const element = elements[index]
    let newFiber = null

    // Check whether it is of the same type
    const sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type

    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE",}}if(! sameType && element) {// Add a node
    }

    if(! sameType && oldFiber) {// Delete a node
    }

    // ...}}Copy the code

For the new nodes, we mark them with PLACEMENT flags on the effectTag property.

function reconcileChildren(wipFiber, elements) {
  let index = 0
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child
  let prevSibling = null

  while( index < elements.length || oldFiber ! = =null
  ) {
    const element = elements[index]
    let newFiber = null

    // Check whether it is of the same type
    const sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type

    // ...

    if(! sameType && element) {// Add a node
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null.parent: wipFiber,
        alternate: null.effectTag: "PLACEMENT",}}if(! sameType && oldFiber) {// Delete a node
    }

    // ...}}Copy the code

For nodes to be deleted, instead of creating a new Fiber, we set the effectTag to DELETION and add it to the old Fiber node.

function reconcileChildren(wipFiber, elements) {
  let index = 0
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child
  let prevSibling = null

  while( index < elements.length || oldFiber ! = =null
  ) {
    const element = elements[index]
    let newFiber = null

    // Check whether it is of the same type
    const sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type

    // ...

    if(! sameType && oldFiber) {// Delete a node
      oldFiber.effectTag = "DELETION"
      deletions.push(oldFiber)
    }

    // ...}}Copy the code

When we commit, we start walking through the newly constructed Fiber node tree, since there are no old nodes to save and delete. So we need to use an additional array deletions to hold the old nodes that need to be deleted

🤓️: In the React source, the Fiber node of the workInProgress Tree has a reference to the corresponding node of the Current Tree. And vice versa.

🤓️: The React source code builds a linear list of all Fiber nodes that need to perform side effects during the COMMIT phase for quick iteration. Iterated linear lists are much faster than iterated trees because there is no need to iterate over nodes without side-effects.

let deletions = null

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    alternate: currentRoot,
  }
  deletions = []
  nextUnitOfWork = wipRoot
}
Copy the code

When we enter the COMMIT phase, we use Fiber in this array

function commitRoot() {
  deletions.forEach(commitWork)
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  wipRoot = null
}
Copy the code

Now let me modify the commitWork function to handle the new effectTag field

If the effectTag is a PLACEMENT, add the DOM to the parent node as before

function commitWork(fiber) {
  if(! fiber) {return
  }
  const domParent = fiber.parent.dom
  // Handle the new node
  if (
    fiber.effectTag === "PLACEMENT"&& fiber.dom ! =null
  ) {
    domParent.appendChild(fiber.dom)
  }
  // Process child nodes recursively
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}
Copy the code

If the effectTag is DELETION, we delete the node from the parent node

function commitWork(fiber) {
  if(! fiber) {return
  }
  const domParent = fiber.parent.dom
  if (
    fiber.effectTag === "PLACEMENT"&& fiber.dom ! =null
  ) {
    // Handle the new node
    domParent.appendChild(fiber.dom)
  } else if (fiber.effectTag === "DELETION") {
    // Handle node deletion
    domParent.removeChild(fiber.dom)
  }
  // Process child nodes recursively
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}
Copy the code

If the effectTag is UPDATE, we UPDATE the existing DOM with the new props

function commitWork(fiber) {
  if(! fiber) {return
  }
  const domParent = fiber.parent.dom
  if (
    fiber.effectTag === "PLACEMENT"&& fiber.dom ! =null
  ) {
    // Handle the new node
    domParent.appendChild(fiber.dom)
  } else if (fiber.effectTag === "DELETION") {
    // Handle node deletion
    domParent.removeChild(fiber.dom)
  } else if (
    fiber.effectTag === "UPDATE"&& fiber.dom ! =null
  ) {
    // Processing of nodes that need to be updated
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  }
  // Process child nodes recursively
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}
Copy the code

Next you need to implement the updateDom function

function updateDom(dom, prevProps, nextProps) {
  // TODO
}
Copy the code

We compared the old Fiber props to the new Fiber props, removed the removed props, and added or updated the changed props

// Used to exclude the children attribute
const isProperty = key= >key ! = ="children"
// Is used to determine whether attributes are updated
const isNew = (prev, next) = > key= >prev[key] ! == next[key]// Check whether there are properties on the new props
const isGone = (prev, next) = > key= >! (keyin next)

function updateDom(dom, prevProps, nextProps) {
  // Delete the previous attributes
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach(name= > {
      dom[name] = ""
    })
  // Add or update attributes
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name= > {
      dom[name] = nextProps[name]
    })
}
Copy the code

We need to do special handling for event listeners, so if props starts with ON, we handle them differently

// check whether props start with on
const isEvent = key= > key.startsWith("on")
// Use to exclude the children attribute, and attributes beginning with on
const isProperty = key= >key ! = ="children" && !isEvent(key)
Copy the code

If the event handler changes, we need to delete it first and then add a new handler

🤓️: Event handlers are added directly to the DOM, somewhat like those in Preact

function updateDom(dom, prevProps, nextProps) {
  Object.keys(prevProps)
    .filter(isEvent)
    .filter(
      key= >
        // If the event handler is updated, the new props is not available
        // The previous handler needs to be deleted! (keyin nextProps) ||
        isNew(prevProps, nextProps)(key)
    )
    .forEach(name= > {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.removeEventListener(
        eventType,
        prevProps[name]
      )
    })

  // Delete the previous attributes
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach(name= > {
      dom[name] = ""
    })
  // Add or update attributes
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name= > {
      dom[name] = nextProps[name]
    })

  // Add event listener
  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

Function component

The next thing we need to add is support for the Function component. Let’s modify our example.

function App(props) {
  return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
render(element, container)
Copy the code

We convert JSX to JS

function App(props) {
  return createElement(
    "h1".null."Hi ",
    props.name
  )
}
const element = createElement(App, {
  name: "foo",})Copy the code

There are two main differences between the Function component and the DOM

  1. The Fiber of the Function component has no DOM node
  2. Children come from Function, not directly from DOM

We check if Fiber is a function and pass in the updateHostComponent if it is a different DOM

function performUnitOfWork(fiber) {
  // Check whether it is a function component
  const isFunctionComponent =
    fiber.type instanceof Function

  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }
  // Return to the next Fiber node that needs to be processed, since it is depth-first traversal, starting from the child nodes first
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}
Copy the code

UpdateHostComponent does the same thing we did before

function updateHostComponent () {
  if(! fiber.dom) {// Create a DOM node
    fiber.dom = createDom(fiber)
  }
​ / / child elements
  const elements = fiber.props.children
  // The subelements coordinate with the old Fiber
  reconcileChildren(wipFiber, elements)
}
Copy the code

UpdateFunctionComponent runs the function component to get children. In our case, the App will return the H1 element. Once you have children, the coordination can go on as before. No modifications are required.

function updateFunctionComponent () {
  // Get children of the Function component
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}
Copy the code

Next we need to modify the commitWork function. Because the Fiber node of our Function component does not have a DOM node. We need to fix two things.

First, if we want to find the parent of the DOM node, we need to look up and find the Fiber with the DOM node


function commitWork(fiber) {
  if(! fiber) {return
  }
  / / parent Fiber
  let domParentFiber = fiber.parent
  // Until you find the Fiber node that contains the DOM
  while(! domParentFiber.dom) { domParentFiber = domParentFiber.parent }const domParent = domParentFiber.dom

  // ...
}
Copy the code

To remove the node, we need to go down until we find the Fiber that contains the DOM node

function commitDeletion (fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom)
  } else {
    commitDeletion(fiber.child, domParent)
  }
}

function commitWork(fiber) {
  // ...

  if (fiber.effectTag === "PLACEMENT"&& fiber.dom ! =null) {
    // Process the addition
    domParent.appendChild(fiber.dom)
  } else if (fiber.effectTag === "DELETION") {
    // Handle deletion
    commitDeletion(fiber, domParent)
  } else if (fiber.effectTag === "UPDATE"&& fiber.dom ! =null) {
    // Handle updates
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  }
  // ...
}
Copy the code

8: hooks

Last step. Now that we have the Function component, let’s add state. Here is an example of a counter

function Counter() {
  const [state, setState] = Didact.useState(1)
  return (
    <h1 onClick={()= > setState(c => c + 1)}>
      Count: {state}
    </h1>)}const element = <Counter />
onst container = document.getElementById("root")
render(element, container)
Copy the code

We use useState to get and update the value of the counter. Before calling the function component, we need to initialize some global variables to use in the useState function.

First we get Fiber working. We add a hooks array to the Fiber node. The purpose of using the array is to support multiple Usestates. And references the index of the current hooks.

// Fiber is currently working
let wipFiber = null
// Index to the hooks of current Fiber
let hookIndex = null

function updateFunctionComponent () {
  // Working Fiber
  wipFiber = fiber
  // The index of the current hooks defaults to 0
  hookIndex = 0
  // a collection of hooks
  wipFiber.hooks = []
  // Get children of the Function component
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}
Copy the code

When the component calls useState, we first check to see if there are any previous hooks, and if there are old hooks, we copy the previous state to the new hook. Otherwise, initialize the hook with the initial value.

Then add the hook to Fiber and increase the index of the hook by one

function useState(initial) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  // Check whether there is any state before
  const hook = {
    state: oldHook ? oldHook.state : initial,
  }
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state]
}
Copy the code

UseState should also return a function to update the status. So we define setState to receive the action, to update the state. SetState pushes the action onto the hook queue.

We then do something similar to what we did in the Render function, we set nextUnitOfWork to start the new rendering phase.

function useState(initial) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  // Check whether there is any state before
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [].// Update the queue
  }
  const setState = (action) = > {
    // Add the action to the queue
    hook.queue.push(action)
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    // When nextUnitOfWork is not empty, it enters the render phase
    nextUnitOfWork = wipRoot
    deletions = []
  }
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state, setState]
}
Copy the code

🤓️: This simplifies setState, which only accepts functions as arguments.

But we have not updated state yet. The next time we render the component, we get all the actions from the old queue. Then apply them one by one to the new hook states. When we return to the state, the state will be updated.

🤓️: setState is called, and state is not updated immediately. Instead, the state is updated after the Render phase, and useState returns the new state.

function useState(initial) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  // Check whether there is any state before
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [].// Update the queue
  }
  const actions = oldHook ? oldHook.queue : []
  actions.forEach(action= > {
    hook.state = action(hook.state)
  })
  const setState = (action) = > {
    // Add the action to the queue
    hook.queue.push(action)
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    // When nextUnitOfWork is not empty, it enters the render phase
    nextUnitOfWork = wipRoot
    deletions = []
  }
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state, setState]
}
Copy the code

We’ve already built our React.

conclusion

In addition to helping you understand how React works, another goal of this article is to make it easier for you to dive into React later. So we repeatedly use the same function and variable names as in the React source code.

We left out a lot of React optimizations

  • inrenderPhases traverse the entire tree, but React skips subtrees without any changes.
  • commitPhase, React does a linear traversal
  • Currently we create a new Fiber each time, and React will reuse the previous Fiber nodes

And a lot more…

We can continue to add features such as:

  1. Add the key
  2. Add useEffect
  3. Use objects as props for the style
  4. Children flattening

reference

  • Build your Own React.
  • Didact: A DIY Guide to Build Your Own React
  • didact