Write in front:

React plays a big role in the front-end world. For most React developers, React is just a tool, just use it. Since a tool works well, you don’t need to know how the parts of the tool are made up. If we look at React from a designer’s point of view instead of a user’s, you might have a different experience with the familiar React and learn the best of the source code. This article is based on the book “Build Your Own React”, from the source point of view to implement a simple React, knocking on the door of the new world.

An overview

Feature articles will rewrite React as the real React architecture, ignoring some optimizations and insignificance in the source code. React 16.8 is a hook code, so there is no class related content.

In the react rewrite, we’ll use the following:

  • 1: The createElement Function
  • 2: The render Function
  • 3: Concurrent Mode
  • 4: Fibers
  • 5: Render and Commit Phases
  • 6: Reconciliation
  • 7: Function Components
  • 8: Hooks

React, JSX, and DOM elements: React, JSX, DOM elements

    // Define React Element, JSX statement
    const element = <h1 title="foo">Hello</h1>
    // Get the DOM element node
    const container = document.getElementById("root")
    // Render the React Element into the container
    ReactDOM.render(element,container)
Copy the code

In the first line we define the JSX element. This is not legitimate JavaScript code, so we need to replace it with legitimate JavaScript code.

JSX is converted to JS through the build tool Babel. The transformation is simple: replace the code in the tag with createElement and pass in the tag name, parameter, and child node as parameters. React.createElement validates the input parameter and generates an object. So we replace it with the following code:

    // const element = <h1 title="foo">Hello</h1>
    const element = React.createElement(
        "h1",
        {title : "foo"},
        "Hello"
    )
   // react.createElement () : creates a React element with the first argument specified.
   // The first argument is mandatory, passing in htML-like tag names, eg: ul, li;
   // The second argument is optional and represents the attribute, eg: className
   // The third argument is optional, child node, eg: text content to display
   //React.createElement( 
   // type,
   // [props],
   // [...children]
   // )
Copy the code

A JSX element is an object with properties such as type and props:

  • The Type attribute is a string whose value is the tag name in JSX, which specifies the type of the DOM node. Type will eventually be passed as a tagName to document.createElement to create HTML elements, but it can also be a function, which we’ll discuss later.
  • Props is an object that accepts all keys, values from the JSX property, and has a special property, children. In the example above, children is a string, but in the general case, it would be an array of elements. Thus the structure of an element becomes a tree.

So the final element is an object:

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

The additional code that needs to be handled is reactdom.render. React changes the DOM in the render function, so let’s update the DOM manually.

    const element = {
        type: "h1".props: {
            title: "foo".children: "Hello",}}const container = document.getElementById("root")
    // First we create a node based on the 'type' passed in
    // To avoid confusion, use "element" for React Element and "node" for DOM element
    const node = document.createElement(element.type)
    node["title"] = element.props.title
    // Next create the child nodes of node. In this case, the child nodes are strings, so we need to create a Text node
    const text = document.createTextNode("")
    text["nodeValue"] = element.props.children
    // Finally we add 'textNode' to 'h1' and 'h1' to 'container'
    node.append(text)
    container.append(node)
Copy the code

We can see that we successfully rendered the same content as React without using React. React is implemented in a similar way. The following is a specific introduction.

Construct your own React

Let’s change the example and use our own React to render the page. Create React (createElement) {React (createElement) {React (createElement);

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

1, The createElement Function

We need to convert JSX to JS first, and we need to focus on how createElement is called. Element is an object with type and props, so all the createElement function needs to do is create such an object.

    const element = React.createElement(
        "div",
        {id:"foo"},
        React.createElement('a'.null.'bar'),
        React.createElement('b'))// Use... for props. Operator that takes the remaining arguments to children in the input argument, such that the children argument is always an array.
    function createElement(type,props,... children){
        return {
            type,
            props: {... props, children } } }Copy the code

There may be some basic types in the children array, such as string, number, and so on. React doesn’t actually create an empty array for children of a base value. Instead, React has its own processing logic. To simplify the code here, we create a special type TEXT_ELEMENT for all values that are not objects. Note that our implementation differs from React. After all, we want simple code here, not perfect code. So let’s take the createElement function above one step further:

    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

We define our createElement function, we take Didact as our own library name, and compile JSX using didact.createElement:

    const Didact = {
            createElement,
           }
    // const element = Didact.createElement(
    // "div",
    // { id: "foo" },
    // Didact.createElement("a", null, "bar"),
    // Didact.createElement("b")
    / /)
    / * *@jsx Didact.createElement */
    // Because of the comments above, Babel will compile JSX into didact.createElement, the function we need
    const element = (
            <div id="foo">
            <a>bar</a>
            <b />
            </div>
          )
Copy the code

2, The render function

Let’s write the reactdom.render function. From easy to difficult, we’ll focus first on adding things to the DOM for now, and then on updating and deleting:

.function render(element,container){
        //TODO create dom nodes
    }
    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

If we only consider DOM adding elements, it’s easy to imagine what Render would do: create a DOM node based on the Type attribute in element and 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 for each child node, so that all child nodes can be added to the DOM and rendered:

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

One thing to note here is that elemen’s value may be plain text, i.e., TEXT_ELEMENT, in which case we need to create a text node, as we did at the beginning.

    const dom = element.type == "TEXT_ELEMENT" ? 
        document.createTextNode("")
        : document.createElement(element.type)
Copy the code

Finally, we need to add all the attributes on element to the corresponding element:

    const isProperty = key= >key ! = ="children";
    Object.keys(element.props).filter(isProperty).forEach(name= > {
        dom[name] = element.props[name];
    });
Copy the code

Now we have a library that can render JSX into the DOM. Here is the complete code:

    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 style="background: salmon">
        <h1>Hello front deer</h1>
        <h2 style="text-align:right">from Didact</h2>
      </div>
    );
    const container = document.getElementById("root");
    Didact.render(element, container);

Copy the code

The effect is as follows:

nice! React renders now work properly!

3, Concurrent Mode

Concurrrent Mode is an important concept in React Fiber. It is mainly responsible for task scheduling, which makes the page display less sluggish in the case of a large number of calculation updates. When a large number of DOM nodes are updated at the same time, React will suffer from severe lag. Specifically, interaction/render lag occurs. For example, a large number of asynchronous IO operations will block page updates and so on. React in the presence of the Concurrent Mode is in order to solve this problem, we here from the code level, Concurrent Mode about more detailed content, a lot of ah, online data (such as: zhuanlan.zhihu.com/p/109971435…

Let’s start by reviewing the code above. Do you see any problems with it? That’s right! Render function uses recursion. When we encounter recursion, we can’t help but think of stack overflow, performance, etc. In our render function, once we start rendering, there is no way to stop the process until the element is fully rendered and recurses. If the DOM tree is large, it may block the main thread. This means that some of the browser’s high-priority tasks wait for the rendering to complete, such as user input to keep the animation running smoothly. This results in page and interaction stalling.

So how do you deal with this problem? It’s as simple as breaking up a big task into smaller ones, and handing control to the browser each time we complete one of those smaller tasks and letting the browser decide if there’s a higher priority that needs to be done. If there are high-priority tasks, perform the high-priority tasks first, such as user input, etc. If there are no high-priority tasks, continue to perform the current task. Let’s start with the requestIdleCallback as a loop. Window. RequestIdleCallback role is in the browser’s free time called function to wait in line. 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. You can think of requestIdleCallback as a setTimeout, except this time it’s the browser that decides when to run the callback function, rather than a time we specify in setTimeout. The browser runs the callback function whenever the main thread is free.

.function render(element, container){...}
    ...
    // Next unit of work
    let nextUnitOfWork = null
    /** * workLoop workLoop function *@param {deadline} Deadline */
    function workLoop(deadline) {
      // Whether the working loop function should be stopped
      let shouldYield = false
      // If the next unit of work exists and there is no other work of higher priority, the loop is executed
      while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork )// If the deadline is near, stop working the loop function
        shouldYield = deadline.timeRemaining() < 1
      }
      // Inform the browser that workLoop should be executed in idle time
      requestIdleCallback(workLoop)
    }
    // Inform the browser that workLoop should be executed in idle time
    requestIdleCallback(workLoop)
    // Executes the cell event and returns the next cell event
    function performUnitOfWork(nextUnitOfWork) {
      // TODO}...Copy the code

We need to pay attention, ReactNot to userequestIdleCallback 的. It uses its ownscheduler package. But they are conceptually the same

As we know from the code above, we need to set up the first task unit for rendering and then start the loop. The performUnitOfWork function not only needs to execute each small task unit, but also needs to return the next task unit

4, Fibers

Fibers is a new feature that was added after React 16. What exactly this is is beyond the scope of this article, since you’ve already started writing code and you probably already know a little bit about it. If you do not understand, please baidu ha.

Concurrent Mode is used to perform small tasks after large task decomposition, and performUnitOfWork also needs to return the next task unit. This shows that all the task units are connected, and there is a data structure that organizes them together. This data structure is called fiber tree. Each element is a fiber, and each fiber is a task unit. For example, we want to render the following DOM tree:

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

In the previous section we created a fiber (nextUnitOfWork) as the first task unit, and the rest of the units are completed and returned by the performUnitOfWork function. Each Fiber node does three things:

  1. Add element to the DOM
  2. Create a new Fiber for the children of this fiber node
  3. Pick the next task unit

One purpose of this data structure is to make it easier to find the next task unit. So each fiber points to its first child, its next sibling, and its parent. So the DOM structure we want to render in this example will look like the fiber tree:

After we have processed a fiber node. Its Child Fiber node will be the next task unit. In this example, the div Fiber node is followed by the H1 fiber node.

  1. If this fiber doesn’t have onechild, then its sibling will be the next task unit. In this case, thenpAfter completing the fiber node task, we need to processaFiber node becausepThe node withoutchildNode.
  2. If a fiber has neitherchildThere is nosiblingIts “uncle” node (the sibling of the parent node) will be the next task unit. In this caseaThe corresponding “uncle” node ish2.
  3. ifparentThe node withoutsilbingContinues to find the parent node of the parent node until the node hassiblingOr until the root node is reached. Reaching the root node completes the entire treerender.

If there is no child node, look for the brother node. If there is no brother node, look for the uncle node (the brother node of the parent node). If there is no brother node, continue to look for the ancestor node until the ancestor node has a brother node. If you go all the way back to the root, the tree is render complete.

That’s the theory, but what about the code? Render function (render function);

.// The original render function
    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)
  }
  let nextUnitOfWork = null.// The modified function
  // There are two transformation points: 1. Extract the code for creating DOM nodes and encapsulate it into a function to improve reusability;
  //2, we set nextUnitOfWork as the root of the fiber tree
  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
  }
  function render(element, container){
      nextUnitOfWork = {
          dom:container,
          props: {children:[element],
          }
      }
  }
  let nextUnitOfWork = null
  
Copy the code

When the browser is free, it calls workLoop and we start walking through the tree.

    /** * workLoop workLoop function *@param {deadline} Deadline */
    function workLoop(deadline) {
      // Whether the working loop function should be stopped
      let shouldYield = false
      // If the next unit of work exists and there is no other work of higher priority, the loop is executed
      while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork )// If the deadline is near, stop working the loop function
        shouldYield = deadline.timeRemaining() < 1
      }
      // Inform the browser that workLoop should be executed in idle time
      requestIdleCallback(workLoop)
    }
    // Inform the browser that workLoop should be executed in idle time
    requestIdleCallback(workLoop)
    // Executes the cell event and returns the next cell event
    function performUnitOfWork(nextUnitOfWork) {
      // TODO add element to the dom
      // TODO Create New Fibers create a new fiber for the children of this fiber node
      // TODO return next unit of work
    }
Copy the code

The function does three things, so the function is naturally divided into three parts:

  1. First create fiber’s corresponding DOM node and append it to the parent DOM. In which we passfiber.domThis property maintains the created DOM node;
  2. Create a new fiber node for each child node. Note the distinction between the three different node entities, Element (createElement creates react Element), which is an object. React Element is rendered to the actual node. DOM node (the corresponding DOM node is eventually generated), Fiber node (the intermediate product from Element to DOM node, used for time slice) 】, and then set the parent node according to whether it is the first child nodechildProperty, or to the previous nodesiblingAttribute pointing;
  3. Finally find the next unit of work. Try the child node, then the Sibling node, then the Uncle node
    // Executes the cell event and returns the next cell event
    function performUnitOfWork(fiber) {
      // TODO add element to the dom
      if(! fiber.dom) { fiber.dom = createDom(fiber) }if (fiber.parent) {
        fiber.parent.dom.appendChild(fiber.dom)
      }
      // TODO Create New Fibers create a new fiber for the children of this fiber node
      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 return next unit of work
      if (fiber.child) {
        return fiber.child
      }
      let nextFiber = fiber
      while (nextFiber) {
        if (nextFiber.sibling) {
            return nextFiber.sibling
        }
        nextFiber = nextFiber.parent
      }
    }
Copy the code

This completes the formUnitofWork part of the work.

5, Render and Commit Phases

So far the code looks pretty good, doesn’t it? Take a look at this part of the performUnitOfWork function:

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

There is a problem with traversing the Element while generating a new DOM node and adding it to its parent. The browser also blocks this process before the entire tree is rendered. The user is likely to see a partially rendered UI. We certainly don’t want that to happen. So let’s move the part of the code that modifies the DOM node out of formUnitofwork. We record the DOM changes on the Fiber Tree and collect all the DOM node changes by tracking the tree, which is called wipRoot (Work in Progress Root, WIP).

There are two changes to the corresponding code:

  1. Define wipRoot and assign the initialized wipRoot to nextUnitOfWork as fiber root in render.
  2. WorkLoop handles wipRoot, because you’ve removed the part that handles DOM nodes from performUnitOfWork, and you need to add it back somewhere, otherwise you won’t be able to mount DOM nodes. Yeah, we’ll do it in this WookLoop, once we’re donewipRootFor all tasks in the tree (the next unit of work is undefined), we commit changes to the actual DOM for the entire tree. The commit operation is all therecommitRootFunction. We recursively add all nodes to the DOM

The code changes as follows:

. .function render(element, container) {
        wipRoot = {
            dom: container,
            props: {
                children: [element],
            },
        }
        nextUnitOfWork = wipRoot
    }
    let nextUnitOfWork = null
    let wipRoot = null. .function workLoop(deadline) {
      let shouldYield = false
      while(nextUnitOfWork && ! shouldYield) { ... }// Add this conditional judgment to handle submitting changes to the actual DOM of the complete wipRoot tree
      if(! nextUnitOfWork && wipRoot) { commitRoot() } requestIdleCallback(workLoop) }function commitRoot() {
        // TODO add nodes to dom
        commitWork(wipRoot.child)
        wipRoot = null
    }
    function commitWork(fiber) {
      if(! fiber) {return
      }
      constdomParent = fiber.parent.dom domParent.appendChild(fiber.dom) commitWork(fiber.child) commitWork(fiber.sibling) } ... .Copy the code

6, Reconciliation

So far we’ve only added things to the DOM. How about updating and deleting nodes? This requires us to compare the Fiber tree generated by the element newly received in Render to the fiber tree committed to the DOM last time. This makes sense because you have to compare the old and new trees to know which elements were updated or deleted. We need to save the “reference” of the last fiber tree submitted to the DOM node, which we call currentRoot. Add alternate properties on each fiber node to record references to the old fiber node (the one used in the previous COMMIT phase). Add the relevant part of currentRoot to the code:

. .function commitRoot() {
      commitWork(wipRoot.child)
      //++currentRoot
      currentRoot = wipRoot
      wipRoot = null
    }
   function commitWork(fiber) {... }function render(element, container) {
      wipRoot = {
        dom: container,
        props: {
          children: [element],
        },
         //++alternate
        alternate: currentRoot,
      }
      nextUnitOfWork = wipRoot
   }
    let nextUnitOfWork = null
     //++currentRoot
    let currentRoot = null
    let wipRoot = null. .Copy the code

To further simplify the code and facilitate the subsequent processing of the currentRoot and Wiproot trees, we extracted the code from the new Fiber node created in performUnitOfWork and encapsulated it into a function body called reconcileChildren:

    function performUnitOfWork(fiber) {...const elements = fiber.props.children
      reconcileChildren(fiber, elements)
      if (fiber.child) {
        return fiber.child
      }
      ...
    }
    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

We adapt the reconcileChildren function to reconcile the old Fiber nodes with the new React elements. How do you reconcile it? Alternate wipFiber node (wipFiber. Alternate) while iterating through the React Elements array. The reconcileChild is transformed below:

    function reconcileChildren(wipFiber, elements) {
      let index = 0
      // Get the fiber tree from the last commit
      let oldFiber = wipFiber.alternate && wipFiber.alternate.child
      let prevSibling = null
      // Process old and new Fiber nodes in the loop
      while( index < elements.length || oldFiber ! =null
      ) {
        const element = elements[index]
        let newFiber = null
        // TODO compare oldFiber to element. }}Copy the code

If we ignore some of the standard templates in iterating through arrays and corresponding links at the same time, we are left with two of the most important things: oldFiber and Element. Element is what we want to render into the DOM, and oldFiber is the fiber tree we rendered last time. We need to compare the two and see what changes need to be applied to the DOM. Since it involves comparing two trees, there is corresponding comparison logic, specifically:

  • For old and new nodesThe same typeWe can reuse the old DOM and only modify the above properties
  • ifDifferent types of“Means we need to create a new DOM node
  • If the type is different and the old node exists, the DOM of the old node needs to be removed

These three comparison rules smack of a simplified diff algorithm. Note that React uses the key attribute to optimize the reconciliation process. For example, the key property can be used to detect whether a child component in the Elements array has simply changed position. When using React, pay attention to the key property on the node. This will improve the performance of react. With the rules settled, I began to perfect the TODO compare oldFiber to Element in the reconcileChildren function in the code:

    // TODO compare oldFiber to element
    const sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type
      
    // The new and old nodes have the same type
    if (sameType) {
      // TODO update the node
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE"
      };
    }
    
    // The new and old nodes are of different types, and a new node needs to be created
    if(element && ! sameType) {// TODO add this node
       newFiber = {
        type: element.type,
        props: element.props,
        dom: null.parent: wipFiber,
        alternate: null.effectTag: "PLACEMENT"
      };
    }
    
    // The type of the new node is different from that of the old node. The old node needs to be removed
    if(oldFiber && ! sameType) {// TODO delete the oldFiber's node
      oldFiber.effectTag = "DELETION";
      deletions.push(oldFiber);
    }
Copy the code
  • When the new Element is of the same type as the old Fiber, we create a new Fiber node for the Element and reuse the old DOM node, but use the props on the Element. We also need to add new properties to the generated fiber:effectTag. It is used during the Commit phase.
  • For fibers that need to generate a new DOM node, we need to mark it asPLACEMENT.
  • For nodes that need to be removed, we do not generate fibers, so we add markers on the old fibers.

Note that when we commit the changes to the DOM of the entire Fiber tree, we do not traverse the old fiber tree. We need to record the changes to the Fiber tree, so we need an array to store the dom nodes to be removed, so we initialize a deletions variable to store subsequent changes to the DOM:

    function render(element, container) {... deletions = [] ... }...let deletions = null
Copy the code

Later, when we commit our changes to DOM, we need to submit the fiber changes in this array, and we make some changes to the commitWork function to handle our new effectTags:

    function commitRoot() {
    // Commit fiber changes
      deletions.forEach(commitWork)
      commitWork(wipRoot.child)
      currentRoot = wipRoot
      wipRoot = null
    }
    function commitWork(fiber) {
      if(! fiber) {return
      }
      const domParent = fiber.parent.dom
      
      // If the fiber node has the PLACEMENT we labeled earlier, that is, the new DOM node
      // Add the fiber's DOM to its parent
      if (
        fiber.effectTag === "PLACEMENT"&& fiber.dom ! =null
      ) {
        domParent.appendChild(fiber.dom)
      }
      // If "DELETION" is marked, delete the node
      else if (fiber.effectTag === "DELETION") {
        domParent.removeChild(fiber.dom)
      }
      // If there is a "UPDATE" mark, UPDATE the node attribute value, can reuse the original node
      else if (
        fiber.effectTag === "UPDATE"&& fiber.dom ! =null
      ) {
        updateDom(
          fiber.dom,
          fiber.alternate.props,
          fiber.props
        )

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

Next we implement our defined updateDom function:

    // Compare the properties of the old and new fiber nodes, remove, add, or modify the corresponding 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) {
      // Remove old properties
      Object.keys(prevProps)
        .filter(isProperty)
        .filter(isGone(prevProps, nextProps))
        .forEach(name= > {
          dom[name] = ""
        })
      // Set new or changed properties
      Object.keys(nextProps)
        .filter(isProperty)
        .filter(isNew(prevProps, nextProps))
        .forEach(name= > {
          dom[name] = nextProps[name]
        })
    }
Copy the code

If the value is prefixed with “on”, we need to handle this property differently. Therefore, we need to modify the code above to check if it is listening for events. If it is listening for events, we need to do special handling in updateDom:

     // Compare the properties of the old and new fiber nodes, remove, add, or modify the corresponding properties
     const isEvent = key= > key.startsWith("on")
     const isProperty = key= >key ! = ="children" && !isEvent(key)
     ...
    function updateDom(dom, prevProps, nextProps) {
      //Remove old or changed event listeners
      // We need to remove the old listener if it changes.
      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]);
        });
      // Remove old properties.// Set new or changed properties.// Add event listeners
      // Add a new listener event
      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

This section is a bit long, but it is the core of the React render update. Please read it carefully

7, the Function of Components

Next we need to support function components. To change this 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)
    
    // Convert JSX to js as follows
    function App(props) {
      return Didact.createElement(
        "h1".null."Hi ",
        props.name
      )
    }
    const element = Didact.createElement(App, {
      name: "foo",})const container = document.getElementById("root")
    Didact.render(element, container)
Copy the code

Remember what the performUnitOfWork function looked like in the previous section:

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

Note that function components are somewhat special in that:

  • Fiber of function component has no DOM node
  • And the child nodes are run from the function rather than directly from itpropsProperty

Therefore, this function does not apply to function components. How to do? Retrofit…

    function performUnitOfWork(fiber) {
    // When fiber is a function, we use a different function for diff
      const isFunctionComponent =
        fiber.type instanceof Function
      if (isFunctionComponent) {
      // Handle function components
        updateFunctionComponent(fiber)
      } else {
      // The original processing logic
        updateHostComponent(fiber)
      }
     ...
    }
    // Used to generate child components from function components
    function updateFunctionComponent(fiber) {
        // When you run this function, it returns h1 element (react JSX element).
      const children = [fiber.type(fiber.props)]
      // Once we get the child node, the rest of the reconciliation work is the same as before, we don't need to change anything
      reconcileChildren(fiber, children)
    }
    // The same logic as the original
    function updateHostComponent(fiber) {
      if(! fiber.dom) { fiber.dom = createDom(fiber) } reconcileChildren(fiber, fiber.props.children) }Copy the code

In addition, we need to modify the commitWork function. We need to modify two things when our fiber doesn’t have DOM:

  1. To find the parent of the DOM node, we need to traverse the Fiber node until we find the fiber node with the DOM node. Function components have no DOM nodes and need to be skipped in the actual DOM search for parent nodes and so on.
  2. To remove the node, you also need to find the first fiber node under the fiber that has a DOM node

The corresponding code is as follows:

    function commitWork(fiber) {
      if(! fiber) {return
      }
      // Find the fiber node with 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") {
        // Change 2: Remove the first descendant fiber node with a DOM node
        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

8 Hooks.

The final step is to add state to our function component. Let’s change the example to a classic counting component. Each time you click, the status increases by one:

. .const Didact = {
      createElement,
      render,
      useState,
    }
    / * *@jsx Didact.createElement */
    function Counter() {
      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 this code we use didact.setState to read and modify the counter value. We call the Counter function here, and we call useState in this function.

Some global variables need to be initialized before calling the function component. We need to use these global variables in the useState function. The code changes are to initialize some variables and assign values to them in the updateFunctionComponent. We also need to define a useState function to change the state:

. .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

First we set work in Progress Fiber. Add the hooks array to the corresponding fiber to allow us to call useState multiple times within the same function component. Then we record the serial number of the current hook. Those of you who have used hooks should be familiar with this.

When the function component calls useState, we check whether the old fiber under the alternate field corresponding to fiber has the old hook. The serial number of a hook is used to record the number of usestates under the component. If there is an old hook, we copy the value of the old hook to the new hook. If not, initialize state. Then add a new hook on fiber, increase the serial number of hook, and return to the state.

UseState also needs to return a function that updates the state. We define setState, which takes an action parameter. (In the Counter example, action is a function that increments state). We push the action into the queue in the hook we just added. We then set wipRoot to current Fiber as we did in the Render function, and our scheduler will help us start the new render.

The important thing to note here is that we are not running the action immediately. We will not consume the action until the next render. We will take all the actions out of the old hook queue and call them one by one until we go back to the new hook state value. The state returned is already updated.

    function useState(initial) {
    // Check if there are old hooks to initialize state
      const oldHook =
        wipFiber.alternate &&
        wipFiber.alternate.hooks &&
        wipFiber.alternate.hooks[hookIndex]
      // Add hook to fiber
      const hook = {
        state: oldHook ? oldHook.state : initial,
        queue: [],}const actions = oldHook ? oldHook.queue : []
      actions.forEach(action= > {
        hook.state = action(hook.state)
      })
      //setState is used to update the status
      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

So far, we’ve built our own React!! The source code is attached at the end of this article.

conclusion

We implemented our own mini React by writing code step by step. The reason why React is mini is that we didn’t cover a lot of features and optimizations of React. We also simplified some details. After all, React has more real source code and more performance concerns. But the existence of these differences does not mean that our code is not meaningful or useful. Instead, we implemented React in a neat way, with the same ideas and general flow as React! With this article you can get more insight into the React source code.

Writing is not easy. Look at it and cherish it. If you feel helpful, honestly, want to like 👍.

The resources

  • Build your own React
  • React & Didact
  • Understand React Fiber & Concurrent Mode
  • Concurrent mode API Reference (experimental)
  • Handwriting series – Implement a platinum section of React
  • What is React Fiber

The attachment

The source code:

    // import React from "react";
    // import ReactDOM from "react-dom";
    // import React, { Component } from 'react';
    // import { render } from "react-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 createDom(fiber) {
      const dom =
        fiber.type === "TEXT_ELEMENT"
          ? document.createTextNode("")
          : document.createElement(fiber.type);

      updateDom(dom, {}, fiber.props);

      return dom;
    }

    const isEvent = key= > key.startsWith("on");
    const isProperty = key= >key ! = ="children" && !isEvent(key);
    const isNew = (prev, next) = > key= >prev[key] ! == next[key];const isGone = (prev, next) = > key= >! (keyin next);
    function updateDom(dom, prevProps, nextProps) {
      //Remove old or changed event listeners
      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]);
        });

      // Remove old properties
      Object.keys(prevProps)
        .filter(isProperty)
        .filter(isGone(prevProps, nextProps))
        .forEach(name= > {
          dom[name] = "";
        });

      // Set new or changed properties
      Object.keys(nextProps)
        .filter(isProperty)
        .filter(isNew(prevProps, nextProps))
        .forEach(name= > {
          dom[name] = nextProps[name];
        });

      // Add event listeners
      Object.keys(nextProps)
        .filter(isEvent)
        .filter(isNew(prevProps, nextProps))
        .forEach(name= > {
          const eventType = name.toLowerCase().substring(2);
          dom.addEventListener(eventType, nextProps[name]);
        });
    }

    function commitRoot() {
      deletions.forEach(commitWork);
      commitWork(wipRoot.child);
      currentRoot = wipRoot;
      wipRoot = null;
    }

    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") {
        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); }}function render(element, container) {
      wipRoot = {
        dom: container,
        props: {
          children: [element]
        },
        alternate: currentRoot
      };
      deletions = [];
      nextUnitOfWork = wipRoot;
    }

    let nextUnitOfWork = null;
    let currentRoot = null;
    let wipRoot = null;
    let deletions = null;

    function workLoop(deadline) {
      let shouldYield = false;
      while(nextUnitOfWork && ! shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); shouldYield = deadline.timeRemaining() <1;
      }

      if(! nextUnitOfWork && wipRoot) { commitRoot(); } requestIdleCallback(workLoop); } requestIdleCallback(workLoop);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) {
          returnnextFiber.sibling; } nextFiber = nextFiber.parent; }}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) {
      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);
      });

      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];
    }

    function updateHostComponent(fiber) {
      if(! fiber.dom) { fiber.dom = createDom(fiber); } reconcileChildren(fiber, fiber.props.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;

        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(element && ! sameType) { newFiber = {type: element.type,
            props: element.props,
            dom: null.parent: wipFiber,
            alternate: null.effectTag: "PLACEMENT"
          };
        }
        if(oldFiber && ! sameType) { oldFiber.effectTag ="DELETION";
          deletions.push(oldFiber);
        }

        if (oldFiber) {
          oldFiber = oldFiber.sibling;
        }

        if (index === 0) {
          wipFiber.child = newFiber;
        } else if(element) { prevSibling.sibling = newFiber; } prevSibling = newFiber; index++; }}const Didact = {
      createElement,
      render,
      useState
    };

    / * *@jsx Didact.createElement */
    function Counter() {
      const [state, setState] = Didact.useState(1);
      return (
        <h1
          onClick={()= > setState(c => c + 1)}
          style={{
            "user-select": "none"
          }}
        >
          Count: {state}
        </h1>
      );
    }
    const element = <Counter />;
    const container = document.getElementById("root");
    // ReactDOM.render(element, container);
    Didact.render(element, container);

    /* didact. render is deprecated since React 0.14.0, use ReactDOM. Render instead (React /no-deprecated)eslint */
Copy the code

React & Didact