Please refer to pomb.us/build-your-…

Environment set up

We needed a Vanilla JS environment that could convert JSX. It was easy to set up our development environment using Vite

yarn create vite .
Select Vanilla JS

# install dependencies
yarn

touch vite.config.js
Copy the code
// vite.config.js
export default {
  esbuild: {
    jsxFactory: "createElement",}};Copy the code

Here we will install the React dependency to compare our implementation and debug

yarn add react react-dom
Copy the code
<body>
  <div id="root"></div>
  <script type="module" src="/main.jsx"></script>
</body>
Copy the code
// main.jsx
import React, { createElement } from "react";
import ReactDom from "react-dom";

const element = <h1>hello world</h1>;
const root = document.getElementById("root");

ReactDom.render(element, root);
Copy the code
yarn dev
Copy the code

You can see our project running.

createElement

Before we can implement createElement, we need to understand what JSX is.

babel:try it out

How does Babel convert JSX to JS

We print the return value console.log(Element)

Let’s look at the createElement documentation:

React.CreateElement( 
  type,
  [props],
  [...children]
)
Copy the code

Creates and returns a new React element of the specified type. The type arguments can be tag name strings (such as ‘div’ or ‘SPAN’), React component types (class or function components), or React Fragment types.

JSX converts to createElement and returns a JS object (React Element).

Let’s implement createElement

function createElement(type, props, ... children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child= > {
        if (typeof child === 'string') {
          return createTextElement(child)
        }
        return child
      })
    },
  }
}

function createTextElement(text) {
  return {
    type: 'TEXT_ELEMENT'.props: {
      nodeValue: text,
      children: []}}}Copy the code

Here we treat the text nodes specifically to facilitate the organization of the code.

React:

Our:

Github.com/pomber/dida…

render

With React Element, let’s implement Render. For now we are only concerned with creating the DOM; updates and deletions will be implemented later.

function createDom(element) {
  // Create a node
  const dom = element.type === 'TEXT_ELEMENT' ?
    document.createTextNode(' ') :
    document.createElement(element.type)

  // Add attributes
  const isProperty = key= >key ! = ="children"
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name= > {
      dom[name] = element.props[name]
    })

  return dom
}

function render(element, container) {
  const dom = createDom(element)

  // Render child recursively
  element.props.children.forEach((child) = > {
    render(child, dom)
  })

  container.appendChild(dom)
}
Copy the code

Github.com/pomber/dida…

concurrent mode

There is a problem with render above: if the render tree is large, it will occupy the main thread for a while. During this time, higher-priority operations such as animation and processing user input are blocked. (the event loop)

We break render into small task units

This will use the browser’s API: requestIdleCallback, react is to realize himself this way

Window. RequestIdleCallback () method inserts a function, this function will be called the browser idle period. This enables developers to perform background and low-priority work on the main event loop without affecting the delay of critical events such as animations and input responses.

let nextUnitOfWork = null function workLoop(deadline) { let shouldYield = false while (nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() < 1 } requestIdleCallback(workLoop) } requestIdleCallback(workLoop) function performUnitOfWork(nextUnitOfWork) { // TODO }Copy the code

Make this workloop run

We’re going to set the first unitOfWork

PerformUnitOfWork (nextUnitOfWork) returns the nextUnitOfWork.

Github.com/pomber/dida…

fiber

To organize unitOfWork, we need a data structure: fiber tree

Our render function does only one thing, setting root fiber to Next Step of work

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

In our performUnitOfWork function, we need to do three things

  1. Add elements to the DOM
  2. All the children fiber structures that create this element
    • Child points to the first child fiber
    • Sibling refers to sibling Fiber
    • Parent points to the parent fiber
  3. Return to the next fiber

How to set up the next fiber? Here we use depth-first traversal, find child, no child, find Sibling, no sibling, find parent’s Sibling, all the way to root, this rendering is done.

function performUnitOfWork(fiber) {
  //1. Add the element to the DOM
  if(! fiber.dom) { fiber.dom = createDom(fiber) }if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }

//2. Create all the children fiber structures for this element
// -child points to the first subfiber
// -sibling points to sibling fiber
// -parent points to the parent fiber
  const elements = fiber.props.children
  let index = 0
  let prevSibling = null

  while (index < elements.length) {
    const element = elements[index]

    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,}// Set child or sibling depending on whether it is the first child
    if (index === 0) {
      fiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    index++
  }

  / / have a child
  if (fiber.child) {
    return fiber.child
  }
  
  // No child find sibling or parent sibling
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}
Copy the code

Github.com/pomber/dida…

Render and Commit Phases

One problem with the above implementation is that we render node by node, each time the PerformUnited of Work(next Tunit of Work) browser renders, so the user is left with an incomplete UI.

So we need to split rendering into two stages: commit and render


// Set wipRoot and nextUnitOfWork to wipRoot
function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
  }
  nextUnitOfWork = wipRoot
}

function workLoop(deadline) {
  let shouldYield = false
  while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() <1
  }
  // Commit after unitWork is complete
  if(! nextUnitOfWork && wipRoot) { commitRoot() } requestIdleCallback(workLoop) }function commitRoot() {
  commitWork(wipRoot.child)
  wipRoot = null
}

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


function performUnitOfWork(fiber) {
  if(! fiber.dom) { fiber.dom = createDom(fiber) }// Do not render, send to commitRoot unified processing
  // if (fiber.parent) {
  // fiber.parent.dom.appendChild(fiber.dom)
  // }.Copy the code

Github.com/pomber/dida…

Reconciliation

At this point, we have implemented the first rendering process. The next step is to implement an update

  1. Add an alternate property to each Fiber node (including root) to store the last updated oldFiber

  2. Two updates with the same fiber.type are considered to be the same element and marked as UPDATE.

Element exists but has a different type and is marked PLACEMENT. The old filber exists but of a different type, and is marked DELETION.

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

// preformNextUnitOfWork adds fiber to all children
function reconcileChildren(wipFiber, elements) {
  let index = 0

  let oldFiber =
    wipFiber.alternate && wipFiber.alternate.child

  let prevSibling = null

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

    // compare oldFiber to element
    const sameType = oldFiber && element && element.type === oldFiber.type
    if (sameType) {
      // update
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE",}}if(element && ! sameType) {// add this node
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null.parent: wipFiber,
        alternate: null.effectTag: "PLACEMENT",}}if(oldFiber && ! sameType) {// TODO delete the oldFiber's node
      oldFiber.effectTag = "DELETION"
      deletions.push(oldFiber)
    }
    // const newFiber = {
    // type: element.type,
    // props: element.props,
    // parent: fiber,
    // dom: null,
    // }

    if (index === 0) {
      wipFiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    index++
  }
}
Copy the code
  1. incommitWorkThe DOM is processed according to the tag
function commitWork(fiber) {
  if(! fiber) {return
  }
  const domParent = fiber.parent.dom
  if (
    fiber.effectTag === "PLACEMENT"&& fiber.dom ! =null
  ) {
    domParent.appendChild(fiber.dom)
  } else if (
    fiber.effectTag === "UPDATE"&& fiber.dom ! =null
  ) {
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  } else if (fiber.effectTag === "DELETION") {
    domParent.removeChild(fiber.dom)
  }

  commitWork(fiber.child)
  commitWork(fiber.sibling)
}
Copy the code

Github.com/pomber/dida…

Function component

The function component has two distinct places

  • No DOM nodes
  • The children of the function is returned by a call instead ofprops.childrenDirectly acquired
updateFunctionComponent(fiber){
  const elements = [fiber.type(fiber.props)]
  reconcileChildren(fiber, elements)
}

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

function performUnitOfWork(fiber) {
  const isFunctionComponent =
    fiber.type instanceof Function
  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }

  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}
Copy the code

On commitWork, continue looking up if the parent element does not have a DOM

function commitWork(fiber) {
  if(! fiber) {return
  }
  // const domParent = fiber.parent.dom
  let domParentFiber = fiber.parent
  while(! domParentFiber.dom) { domParentFiber = domParentFiber.parent }const domParent = domParentFiber.dom

  if (
    fiber.effectTag === "PLACEMENT"&& fiber.dom ! =null
  ) {
    domParent.appendChild(fiber.dom)
  } else if (
    fiber.effectTag === "UPDATE"&& fiber.dom ! =null
  ) {
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  } else if (fiber.effectTag === "DELETION") {
    domParent.removeChild(fiber.dom)
  }

  commitWork(fiber.child)
  commitWork(fiber.sibling)
}
Copy the code

Github.com/pomber/dida…

Hooks


let wipFiber = null

// Add 1 each time you call useState
let hookIndex = null

function updateFunctionComponent(fiber) {
  wipFiber = fiber
  
  // Reset to 0 for each update
  hookIndex = 0
  
  // Use hookIndex to track the results of multiple calls to useState
  wipFiber.hooks = []
  
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

function useState(initial) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [],}const actions = oldHook ? oldHook.queue : []
  actions.forEach(action= > {
    hook.state = action(hook.state)
  })
    
   // The action is stored in hook. Queque and the update is triggered
  const setState = action= > {
    hook.queue.push(action)
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    nextUnitOfWork = wipRoot
    deletions = []
  }

  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state, setState]
}
Copy the code

Github.com/pomber/dida…

Personal blog