Foreword (must see)

This chapter will implement a Mini React based on react17, It covers all of the react source code, such as Fiber architecture, Render and commit phases, diff algorithm, class components, function components, hooks, and most of the react principle.

The github repository: Mini-React

I strongly recommend that you check with my warehouseThe commit recordTake a look at this article, which contains the code submission for each step of this article, and you can clearly see the code changes for each step: Each section of this article has a commit record marked with πŸ‘‰ in the title. Click the title link to jump directly to the commit record

One: Initialize the project πŸ‘‰

Create a react project with create-react-app. Create a React project with create-react-app.

create-react-app mini-react
Copy the code

Then run CD./mini-react to enter our project, remove the unnecessary files and code, only keep the index.js and index. CSS files, initialize the project directory structure as follows:

πŸ“¦mini-react ┣ πŸ“‚public ┣ πŸ“‚ SRC ┃ ┣ link index.css ┃ ull index.js ┣ held. gitignore ┣ dependency package πŸ“œ yarn. The lockCopy the code

The index.js file contains a JSX structure that contains various types of JSX content, including class components, function components, normal DOM, conditional rendering, and list rendering (which will be used for various situations we will consider when rendering later), and then renders it on the page via reactdom.render. The code is as follows:

import { Component } from 'react';
import ReactDOM from 'react-dom';
import './index.css';

class ClassComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {};
  }

  render() {
    return (
      <div className="class-component">
        <div>this is a class Component</div>
        <div>prop value is: {this.props.value}</div>
      </div>); }}function FunctionComponent(props) {
  return (
    <div className="function-component">
      <div>this is a function Component</div>
      <div>prop value is: {props.value}</div>
    </div>
  );
}

const jsx = (
  <div className="deep1-box">
    <ClassComponent value={666} />
    <FunctionComponent value={100} />
    <div className="deep2-box-1">
      <a href="https://github.com/zh-lx/mini-react">mini react link</a>
      <p style={{ color: 'red' }}> this is a red p</p>
      <div className="deep3-box">
        {true && <div>condition true</div>}
        {false && <div>condition false</div>}
        <input
          type="button"
          value="say hello"
          onClick={()= >{ alert('hello'); }} / ></div>
    </div>
    <div className="deep2-box-2">
      {['item1', 'item2', 'item3'].map((item) => (
        <li key={item}>{item}</li>
      ))}
    </div>
  </div>
);

ReactDOM.render(jsx, document.getElementById('root')); 
Copy the code

The index. CSS content is used to add styles to each class name to visually distinguish the hierarchy between them. The code is as follows:

.deep1-box {
  border: 1px solid rgb(146.89.236);
  padding: 8px;
}
.class-component {
  border: 1px solid rgb(228.147.147);
  padding: 8px;
}
.function-component {
  margin-top: 8px;
  padding: 8px;
  border: 1px solid rgb(133.233.120);
}
.deep2-box-1 {
  margin-top: 8px;
  padding: 8px;
  border: 1px solid rgb(233.224.107);
}
.deep3-box {
  padding: 8px;
  border: 1px solid rgb(55.189.241);
}
.deep2-box-2 {
  margin-top: 8px;
  padding: 8px;
  border: 1px solid rgb(23.143.77);
}
Copy the code

This completes the initialization of the project, and the page should look something like this:Next, enter our relevant source code implementation link.

Reactdom.render

The React version we created the project with was 17.0.2, so our JSX content was compiled by Babel at runtime as a react. Element. There is no need to convert the React.createElement API like react16.x and its predecessors (see my previous articles on JSX conversion and React.createElement).

So we don’t need to implement the react. createElement API anymore, just start with the reactdom. render implementation.

Create ReactDOM. Render πŸ‘‰

/ SRC /mini-react: / SRC /mini-react: / SRC /mini-react: / SRC /mini-react It exports the ReactDOM object with the Render function attached. All the render function does is take both Element and container and mount the Element as a real DOM to the Container. SRC /mini-react/react-dom.js

function render(element, container) {
  const dom = renderDom(element);
  container.appendChild(dom);
}

// Render the react. Element as a real DOM
function renderDom(element) {}

const ReactDOM = {
  render,
};
export default ReactDOM;
Copy the code

Create dom from React. Element πŸ‘‰

What we’re going to do isrenderDomThe react. element function creates the real DOM from the react. element structureJSX conversion and React. CreateElementHere we can also print the contents of JSX on the console:So we’ll render the React.element as a DOM, depending on the type of the react. element itself and itstype ε’Œ propsParameter, so next, based on the different types of JSX elements we saw above, consider the following when converting a react. element into a real DOM:

  1. When element is false (! Element === true) and not 0, false conditional rendering, no rendering (or passingdocument.createDocumentFragmentCreate an empty document node)
  2. When an element itself is a string, it is represented as a text nodedocument.createTextNodeTo create a
  3. When an element itself is of type number, it is converted to string and then calleddocument.createTextNodeCreating a text node
  4. When an element itself is of Array type, it represents an Array (for example, an Array of elements returned by a Map). You need to mount all elements of the Array using a fragment and then mount the fragment to the parent node
  5. When the elementtypeWhen it is a string, it represents a regular DOM element and is called directlydocument.createElementCreate dom.
  6. When the elementtypeFunction: indicates a class component or a function component
  7. If element’s children are not null, create the children recursively
  8. There are other cases such as the built-in components of ReactReact.fragment,Context,PortalWe will implement the React main function and ignore these situations for the time being

The complete renderDom looks like this:

// Render the react. Element as a real DOM
function renderDom(element) {
  let dom = null; // The DOM to return

  if(! element && element ! = =0) {
    // Conditional render false, return null
    return null;
  }

  if (typeof element === 'string') {
    // If element itself is a string, the text node is returned
    dom = document.createTextNode(element);
    return dom;
  }

  if (typeof element === 'number') {
    // If element itself is number, return the text node after converting it to string
    dom = document.createTextNode(String(element));
    return dom;
  }

  if (Array.isArray(element)) {
    // List rendering
    dom = document.createDocumentFragment();
    for (let item of element) {
      const child = renderDom(item);
      dom.appendChild(child);
    }
    return dom;
  }

  const {
    type,
    props: { children },
  } = element;

  if (typeof type === 'string') {
    // Render regular DOM nodes
    dom = document.createElement(type);
  } else if (typeof type === 'function') {
    // Render the React component
    if (type.prototype.isReactComponent) {
      / / class components
      const { props, type: Comp } = element;
      const component = new Comp(props);
      const jsx = component.render();
      dom = renderDom(jsx);
    } else {
      // Function components
      const { props, type: Fn } = element;
      constjsx = Fn(props); dom = renderDom(jsx); }}else {
    // Other cases are not considered for the time being
    return null
  }

  if (children) {
    // Children exist, recursively render child nodes
    const childrenDom = renderDom(children);
    if(childrenDom) { dom.appendChild(childrenDom); }}return dom;
}
Copy the code

Then replace the react-dom package with our own react-dom.js in/SRC /index.js:

import React from 'react';
- import ReactDOM from 'react-dom'; 
+ import ReactDOM from './mini-react/react-dom';
import './index.css';
// ...
Copy the code

After running it, you can see that our page has now rendered the relevant DOM element:

Update the DOM attribute πŸ‘‰

We rendered the dom element in the previous step, but the element attributes such as the href for the A tag, the style for the P tag, and the classname for the element are not in effect, and our input[button] displays the input field. None of the attributes mounted on our element are in effect.

So next we need to mount the element’s various attributes. We can see from print that the element’s attributes are in the react.elementpropsOn:In view of the elementspropsWe need to consider the following:

  1. If it ischildrenIs a child element of the element, not its attribute
  2. If it isclassNameIs converted to the corresponding class of the element
  3. If it isstyleYou need to update the key-value pairs in the object to the element’s style
  4. If isonThe leading property, stating that it is an event, needs to be handled as an event
  5. Other properties are mounted directly

We use an updateAttributes function to handle updates to element attributes, adding the following code to the react-dom.js file:

// Update dom attributes
function updateAttributes(dom, attributes) {
  Object.keys(attributes).forEach((key) = > {
    if (key.startsWith('on')) {
      // Event handling
      const eventName = key.slice(2).toLowerCase();
      dom.addEventListener(eventName, attributes[key]);
    } else if (key === 'className') {
      // Handle className
      const classes = attributes[key].split(' ');
      classes.forEach((classKey) = > {
        dom.classList.add(classKey);
      });
    } else if (key === 'style') {
      / / style
      const style = attributes[key];
      Object.keys(style).forEach((styleName) = > {
        dom.style[styleName] = style[styleName];
      });
    } else {
      // Handle other attributesdom[key] = attributes[key]; }}); }Copy the code

Then call the updateAttributes function in the renderDom function:

function renderDom(element) {
  // ...
  const {
    type,
- props: { children },
+ props: { children, ... attributes },
  } = element
  // ...  
+ updateAttributes(dom, attributes);
  return dom;
}
Copy the code

Let’s look at our page effects. The element attributes are already mounted:

Three: Realize fiber architecture

One problem with renderDom is that we recursively call renderDom functions for class components, function components, list renderers, and children. If our component tree was particularly large, our Mini-React would keep recursively rendering, causing the render to take too long to complete. Since JS is single-threaded, if there are higher-level tasks such as user input, animation, etc., these tasks will have to wait, and the user will visually feel the page is stuck.

This brings us to one of the core concepts of React — Fiber. We split large rendering tasks into multiple small rendering tasks. Each small task is a unit of work, and the unit of work is represented by a Fiber structure. If you don’t know what Fiber is, check out my previous article to understand more about fiber. It then prioritises high-priority tasks in each frame of the browser and low-priority tasks in idle time.

Deeper understanding of FiberFiber goes from fiber to fiberchild,sibling,returnSeveral fields are connected to each other to form a fiber tree. React processes fiber from the root fiber and uses depth-first traversal. If there is a child, continue processing the child. If no child, handle its sibling; When a fiber’s child and Sibling have been processed, return to the parent node to continue processing. As for the JSX structure in our application, the corresponding fiber tree structure is as follows:The numbers on the arrows in the figure above are the order in which Fiber is executed.

Create rootFiber and nextUnitOfWork πŸ‘‰

In/SRC /mini-react, we will create a new fiber. Js file to store the implementation code related to Fiber. Firstly, since we are depth-first traversal to iteratively process task unit and fiber, we need a global nextUnitOfWork variable as the next task unit to be processed.

Then we said that the fiber iteration starts with rootFiber, so we need to create a rootFiber that points to rootFiber based on the Element and Container parameters received by Reactdom.render. Each fiber needs to mount the stateNode and Element properties. The stateNode points to the actual DOM node created from the fiber for rendering, and the Element points to the React.Element corresponding to the Fiber. Once the rootFiber is created, point nextUnitOfWork to it as the first task unit to be processed.

/ SRC /mini-react/fiber.js

let nextUnitOfWork = null;
let rootFiber = null;

// Create rootFiber as the first nextUnitOfWork
export function createRoot(element, container) {
  rootFiber = {
    stateNode: container, // Record the corresponding real DOM node
    element: {
      / / mount element
      props: { children: [element] },
    },
  };
  nextUnitOfWork = rootFiber;
}
Copy the code

/ SRC /mini-react/react-dom.js /react-dom.js/minireact /react-dom.js/minireact /react-dom.js/minireact /react-dom.js/minireact /react-dom.js/minireact /react-dom.js

+ import { createRoot } from './fiber';

function render(element, container) {
- const dom = renderDom(element);
- container.appendChild(dom);
+ createRoot(element, container);
}
Copy the code

The recursion is changed to iteration πŸ‘‰

Get rid of recursive logic

RenderDom: / SRC /mini-react/fiber.js: / SRC /mini-react/fiber.js: / SRC /mini-react/fiber.js: / SRC /mini-react/fiber.js

Function renderDom(Element) {//...- if (Array.isArray(element)) {
- // List rendering
- dom = document.createDocumentFragment();
- for (let item of element) {
- const child = renderDom(item);
- dom.appendChild(child);
-}
- return dom;
-}

  const {
    type,
    props: { children },
  } = element;

  if (typeof type === 'string') {// Render dom = document.createElement(type);- } else if (typeof type === 'function') {
- // Render the React component
- if (type.prototype.isReactComponent) {
- // Class components
- const { props, type: Comp } = element;
- const component = new Comp(props);
- const jsx = component.render();
- dom = renderDom(jsx);
- } else {
- // Function components
- const { props, type: Fn } = element;
- const jsx = Fn(props);
- dom = renderDom(jsx);
-}} else {return null}- if (children) {
- // Children exist, recursively render child nodes
- const childrenDom = renderDom(children);
- if (childrenDom) {
- dom.appendChild(childrenDom);
-}
-}

  // ...
}
Copy the code

Create the DOM from Fiber

/ SRC /mini-react/fiber.js and create a performUnitOfWork function that iterates over fiber.

When the stateNode property of Fiber is empty, it means that the DOM has not been created yet. Therefore, we call renderDom function to create the corresponding DOM according to the Element property of Fiber. And mount it to the parent node.

The parent node looks for the parent fiber based on fiber’s return property. It’s worth noting that since we removed the iteration logic in renderDom, the DOM returned by the React component or conditional rendering would be empty. The parent fiber’s stateNode may also be null, in which case we continue to look up by return. Hang it until you find a fiber node where the stateNode is not empty.

The code is as follows:

import { renderDom } from './react-dom';

// ...

// Executes the current unit of work and sets the next unit of work to execute
function performUnitOfWork(workInProgress) {
  if(! workInProgress.stateNode) {// If there is no stateNode in fiber, the stateNode will be created based on the attribute of the element attached to fiber
    workInProgress.stateNode = renderDom(workInProgress.element);
  }
  if (workInProgress.return && workInProgress.stateNode) {
    // If fiber has a parent fiber and dom
    // Find the node that can mount the DOM for dom mounting
    let parentFiber = workInProgress.return;
    while (!parentFiber.stateNode) {
      parentFiber = parentFiber.return;
    }
    parentFiber.stateNode.appendChild(workInProgress.stateNode);
  }
}
Copy the code

Structure fiber tree

Now we only have root fiber, and we need to construct the fiber tree, so we need to create the corresponding fiber according to the react. element. Fiber tree is formed by child, Sibling and return fields. In addition to the React. Element has a children property, the React component and list rendering also form parent-child relationships. So let’s consider the following:

  1. When the React of elementtypeAttributes arefunction, represents the React component, and JSX obtained after rendering is treated as children.
  2. If the React of the elementtypeAttributes areArrayAt this time, the node array is meaningless, and fiber is not needed to be formed. Therefore, we directly flattened the child nodes in array and put them into the children array at the same level as array for processing to generate corresponding fiber
  3. Element property of the current Fiber propertychildrenWhen not empty, iteratively build fiber tree according to children

In all three cases, whether children are a single node or an array of multiple nodes, we end up treating them as arrays for code brevity. Then the fiber generated by the first node of the Children array is connected to the fiber tree through the child attribute of the current fiber, and the other fibers are connected through the sibling attribute of the previous child fiber.

The code is as follows:

// Executes the current unit of work and sets the next unit of work to execute
function performUnitOfWork(workInProgress) {
  // Create dom according to fiber
  // ...

  letchildren = workInProgress.element? .props? .children;lettype = workInProgress.element? .type;if (typeof type === 'function') {
    // If the current fiber corresponds to the React component, return it
    if (type.prototype.isReactComponent) {
      // Class component that returns JSX via the render method of the generated class instance
      const { props, type: Comp } = workInProgress.element;
      const component = new Comp(props);
      const jsx = component.render();
      children = [jsx];
    } else {
      // Function component that returns JSX by calling the function directly
      const { props, type: Fn } = workInProgress.element;
      constjsx = Fn(props); children = [jsx]; }}if (children || children === 0) {
    // If children exist, iterate over children
    let elements = Array.isArray(children) ? children : [children];
    // Flat list rendering of 2d arrays (not considering 3d and larger arrays)
    elements = elements.flat();

    let index = 0; // The subscript under the parent of the currently traversed child element
    let prevSibling = null; // Record the last sibling node

    while (index < elements.length) {
      // Iterate over the child elements
      const element = elements[index];
      // Create a new fiber
      const newFiber = {
        element,
        return: workInProgress,
        stateNode: null};if (index === 0) {
        // If subscript is 0, set the current fiber to the child of the parent fiber
        workInProgress.child = newFiber;
      } else {
        // Otherwise use Sibling as sibling fiber connectionprevSibling.sibling = newFiber; } prevSibling = newFiber; index++; }}}Copy the code

Set the next unit of work

As we said at the beginning of this section, fiber tree traversal uses depth-first traversal. If the current fiber has a child, set the child as the next unit of work. If there is no child but SIBLING, set Sibling as the next unit of work; If none, depth-first traversal returns the parent fiber by return. The code is as follows:

function performUnitOfWork(fiber) {
  // Create dom according to fiber
  // ...

  // Build the fiber tree
  // ...

  // Set the next unit of work
  if (workInProgress.child) {
    // If there is a subfiber, the next unit of work is the subfiber
    nextUnitOfWork = workInProgress.child;
  } else {
    let nextFiber = workInProgress;
    while (nextFiber) {
      if (nextFiber.sibling) {
        // If no child fiber has a sibling fiber, the next unit of work is the sibling fiber
        nextUnitOfWork = nextFiber.sibling;
        return;
      } else {
        // No subfiber or sibling fiber, depth-first traversal returns to the previous layernextFiber = nextFiber.return; }}if(! nextFiber) {// If the top layer is returned, the iteration is over, and nextUnitOfWork is empty
      nextUnitOfWork = null; }}}Copy the code

Create workLoop πŸ‘‰

Now that we’ve implemented the iterative logic, when do we execute the iterative logic? We will create a workLoop function in/SRC /mini-react/fiber.js. In this function, nextUnitOfWork will be iterated during the idle time of each frame in the browser. The code is as follows:

// Handle loops and interrupt logic
function workLoop(deadline) {
  let shouldYield = false;
  while(nextUnitOfWork && ! shouldYield) {// Loop through unit of work tasks
    performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }
  requestIdleCallback(workLoop);
}
Copy the code

We use the requestIdleCallback function to call the workLoop callback during each frame idle period in the browser. RequestIdleCallback will pass the deadline argument to the callback function and we can use that to check how much time is left in the current frame for the browser to be free. When deadline.timeremaining () < 1, shouldYied is true to interrupt the current iteration and save it for the next frame. React no longer uses requestIdleCallback due to slow execution and compatibility issues. Instead, React implements similar functionality.

/ SRC /mini-react/fiber.js init workLoop with requestIdleCallback

requestIdleCallback(workLoop);
Copy the code

4: render and commit πŸ‘‰

Another problem with the code above is that we iterate from the root fiber to the sub-fiber. After each fiber is processed, we create the corresponding DOM and mount it to the page. But our iteration task is interruptible, and if it breaks midway, the user will see an incomplete UI on the page:

function performUnitOfWork(fiber) {
  // ...
  
  if (workInProgress.return && workInProgress.stateNode) {
    // If fiber has a parent fiber and dom
    // Find the node that can mount the DOM for dom mounting
    let parentFiber = workInProgress.return;
    while(! parentFiber.stateNode) { parentFiber = parentFiber.return; } parentFiber.stateNode.appendChild(workInProgress.stateNode); }// ...
}
Copy the code

This is not what we want, so we can consider creating all the DOM and then mounting it on the page.

The Render and Commit phases of React are used to process units of work, creating the DOM but not mounting the DOM. After all units of work have been processed, the DOM is mounted synchronously in the Commit phase.

So to sum up what we need to do is as follows:

  1. performUnitOfWorkTo remove the DOM mount operation, only fiber creates the corresponding DOM but does not mount it
  2. To implement acommitRootFunction to perform the DOM mount operation, which is performed synchronously and cannot be interrupted
  3. inworkLoop, nextUnitOfWork is null androotFiberIf it exists, it indicates that the render phase is over and the call startscommitRootThe function enters the commit phase.

Create a commitRoot file in the/SRC /mini-react folder. Since this process is non-disruptive, we recursively perform the DOM mount. At the same time, we use bottom-up mount, first mount the child node, and finally mount the parent node, which can reduce the page rearrangement and redraw, saving performance.

// Commit from the root node
export function commitRoot(rootFiber) {
  commitWork(rootFiber.child);
}

// Perform commit recursively, without breaking the process
function commitWork(fiber) {
  if(! fiber) {return;
  }
  // depth-first traversal, child first, sibling later
  commitWork(fiber.child);
  let parentDom = fiber.return.stateNode;
  parentDom.appendChild(fiber.stateNode);
  commitWork(fiber.sibling);
}
Copy the code

Because it is from the bottom up mount the dom, so when the corresponding React fiber components, we can in renderDom function returns a document. The createDocumentFragment () the child nodes of the document node to mount the components below, This eliminates the need to worry about stateNode being null (the condition rendering false was already filtered during iterative fiber creation, so it doesn’t need to be considered either). This makes it easier for us to implement our own apis for class and function components later on. / SRC /mini-react/react-dom.js

Function renderDom(Element) {//... if (typeof type=== 'string') {// Render dom = document.createElement(type);+ } else if (typeof type === 'function') {
+ // React component render
+ dom = document.createDocumentFragment();} else {return null; } / /... }Copy the code

/ SRC /mini-react/ commitRoot in/SRC /mini-react/commit.js is called at the end of render and rootFiber is reset at the end of commit. Remove the dom creation logic from the performUnitOfWork function:

+ import { commitRoot } from './commit';

function performUnitOfWork(fiber) {
  // ...
  
- if (workInProgress.return && workInProgress.stateNode) {
- // If fiber has a parent fiber and a DOM
- // Find the node that can mount the DOM for dom mounting
- let parentFiber = workInProgress.return;
- while (! parentFiber.stateNode) {
- parentFiber = parentFiber.return;
-}
- parentFiber.stateNode.appendChild(workInProgress.stateNode);
-}/ /... Function workLoop(deadline) {//...+ if (! nextUnitOfWork && rootFiber) {
+ // Indicates the COMMIT phase
+ commitRoot(rootFiber);
+ rootFiber = null;
+}
  requestIdleCallback(workLoop);
}
Copy the code

Five: Diff algorithm — implement update and delete

We’ve just covered dom creation for the first rendering, but what about element deletion and updates? This brings us to another core of React — the Diff algorithm. For the understanding of the DIFF algorithm, you can also see my previous article to fully understand the DIFF algorithm, here we do not expand more, directly into the implementation of the code.

Current and workInProgess πŸ‘‰

In diff, there are two Fiber trees in React: The diff process is actually the diFF between the current Fiber tree (generated during last rendering) and the workInProgress Fiber tree (generated during this rendering).

The fiber tree executed in each render phase of our code is actually the workInProgress Fiber tree. RootFiber is the root of the workInProgress Fiber tree. So we need to maintain another current Fiber tree. Also, for ease of understanding, we renamed rootFiber to workInProgressRoot:

- let rootFiber = null;
+ let workInProgressRoot = null; // The current working fiber tree,
+ let currentRoot = null; // Last rendered fiber tree
Copy the code

All the places where rootFiber is used are renamed workInProgressRoot, so I won’t expand it here.

As mentioned in our previous article on in-depth understanding of Fiber, there is an alternate property in workInProgress Fiber, pointing to the corresponding Current fiber. CurrentRoot points to workInProgressRoot after the React update process (commit phase) is complete.

NextUnitOfWork export function createRoot(Element, container) {workInProgressFiber = {stateNode: Container, // Record the corresponding real DOM node Element: {// Props: {children: [Element]},},+ alternate: currentRoot}; nextUnitOfWork = workInProgressFiber; } / /... Function workLoop(deadline) {//...- if (! nextUnitOfWork && rootFiber) {
+ if (! nextUnitOfWork && workInProgressRoot) {// Indicates the COMMIT phase- commitRoot(rootFiber);
+ commitRoot(workInProgressRoot);// At the end of the commit phase, reset the variable- rootFiber = null;
+ currentRoot = workInProgressRoot;
+ workInProgressRoot = null;
  }
  requestIdleCallback(workLoop);
}
Copy the code

Create the reconciler πŸ‘‰

The next step is to start implementing the Diff process, which is based on reconcileChildren as the entry function, by marking fibers with different Flag side effects in the construction of the Fiber tree. In the/SRC/Mini-react directory, create a new Reconcilil.js file from which to export the reconcileChildren function, and logically migrate the Fiber tree from the Fiber structure in the performUnitOfWork function into this function. The code is as follows:

export function reconcileChildren(workInProgress, elements) {
  let index = 0; // The subscript under the parent of the currently traversed child element
  let prevSibling = null; // Record the last sibling node

  while (index < elements.length) {
    // Iterate over the child elements
    const element = elements[index];
    // Create a new fiber
    const newFiber = {
      element,
      return: workInProgress,
      stateNode: null};if (index === 0) {
      // If subscript is 0, set the current fiber to the child of the parent fiber
      workInProgress.child = newFiber;
    } else {
      // Otherwise use Sibling as sibling fiber connectionprevSibling.sibling = newFiber; } prevSibling = newFiber; index++; }}Copy the code

Remove the logic for constructing the Fiber tree from the performUnitOfWork function and introduce the reconcileChildren function above:

+ import { reconcileChildren } from './reconciler';Function performUnitOfWork(workInProgress) {// Create dom based on fiber //... // Iterates on function components, class components, list rendering, and children... if (children || children= = = 0) {Let elements = array. isArray(children)? children : [children]; Elements = elements. Flat ();- // Remove this section to construct fiber tree logic
+ reconcileChildren(workInProgress, elements);} // Set the next unit of work //... }Copy the code

Diff and add flag πŸ‘‰

Now we have elements in the reconcileChildren function, elements that make us want to render elements on the page, and in order to maximize rendering performance, we need to know how to operate on the old DOM tree with minimal overhead. Therefore, we need to diff elements and the old fiber, and the corresponding old fiber is the child element under workInProgress. Alternate.

Elements and oldFiber are iterated at the same time, compared by element type and olderFiber’s corresponding Element type, and diff results are labeled with flag side effects:

  • If type is the same, the same element is addedUpdateTo directly update dom element attributes
  • If the type is different and a new element exists, addPlacementFlag, indicating that a new DOM needs to be created. And also add to itindexProperty that records the subscript position under the parent node at insertion time
  • If the type is different and oldFiber exists, addDeletionFlag, indicating that the old element needs to be deleted

React uses both type and key comparisons. This is more efficient in some situations such as list rendering when list items change, but we use only type because of the complexity of implementation. React also has other side effects other than delete, update, and add tags, so it uses flags binary operators to add multiple tags.

The adjusted reconcileChildren code is as follows:

import { deleteFiber } from './fiber';

export function reconcileChildren(workInProgress, elements) {
  let index = 0; // The subscript under the parent of the currently traversed child element
  let prevSibling = null; // Record the last sibling node
  letoldFiber = workInProgress? .alternate? .child;// The corresponding old fiber

  while (index < elements.length || oldFiber) {
    // Iterate over elements and oldFiber
    const element = elements[index];
    // Create a new fiber
    let newFiber = null;
    constisSameType = element? .type && oldFiber? .element? .type && element.type === oldFiber.element.type;// Add flag side effects
    if (isSameType) {
      // If the type is the same, it is updated
      newFiber = {
        element: {
          ...element,
          props: element.props,
        },
        stateNode: oldFiber.stateNode,
        return: workInProgress,
        alternate: oldFiber,
        flag: 'Update'}; }else {
      // Different types can be added or deleted
      if (element || element === 0) {
        // If element exists, it is added
        newFiber = {
          element,
          stateNode: null.return: workInProgress,
          alternate: null.flag: 'Placement',
          index,
        };
      }
      if (oldFiber) {
        // oldFiber exists, delete oldFiber
        oldFiber.flag = 'Deletion'; deleteFiber(oldFiber); }}if (oldFiber) {
      // If oldFiber exists, continue traversing its sibling
      oldFiber = oldFiber.sibling;
    }

    if (index === 0) {
      // If subscript is 0, set the current fiber to the child of the parent fiber
      workInProgress.child = newFiber;
      prevSibling = newFiber;
    } else if (newFiber) {
      // newFiber and prevSibling exist, connected via Sibling as sibling fiberprevSibling.sibling = newFiber; prevSibling = newFiber; } index++; }}Copy the code

Placement — Add dom πŸ‘‰

When Fiber is flagged as a Placement element, we find which child element to insert in front of the parent element based on the index attribute of the fiber corresponding to the element. If parentdom.childNodes [fiber.index] exists, insert the element before the parentDom.childNodes[fiber.index]. If not, insert to the end of the parent element directly through appendChild.

The code is as follows:

// Perform commit recursively, without breaking the process
function commitWork(fiber) {
  // depth-first traversal, child first, sibling later
  commitWork(fiber.child);
  let parentDom = fiber.return.stateNode;
  if (fiber.flag === 'Placement') {
    / / add the dom
    const targetPositionDom = parentDom.childNodes[fiber.index]; // To insert before that DOM
    if (targetPositionDom) {
      // If targetPositionDom exists, insert
      parentDom.insertBefore(fiber.stateNode, targetPositionDom);
    } else {
      // targetPositionDom does not exist, inserted last
      parentDom.appendChild(fiber.stateNode);
    }
  }
  commitWork(fiber.sibling)
}
Copy the code

Update — Update dom πŸ‘‰

When fiber is flagged as Update, it updates the DOM. Remove the properties and listener events from the old DOM and add new ones.

Here we modify the updateAttributes function directly and export it to add the logic to remove the old attributes:

export function updateAttributes(dom, attributes, oldAttributes) {
  if (oldAttributes) {
    // There are old attributes, remove old attributes
    Object.keys(oldAttributes).forEach((key) = > {
      if (key.startsWith('on')) {
        // Remove the old event
        const eventName = key.slice(2).toLowerCase();
        dom.removeEventListener(eventName, oldAttributes[key]);
      } else if (key === 'className') {
        // Handle className
        const classes = oldAttributes[key].split(' ');
        classes.forEach((classKey) = > {
          dom.classList.remove(classKey);
        });
      } else if (key === 'style') {
        / / style
        const style = oldAttributes[key];
        Object.keys(style).forEach((styleName) = > {
          dom.style[styleName] = 'initial';
        });
      } else {
        // Handle other attributes
        dom[key] = ' '; }}); }Object.keys(attributes).forEach((key) = > {
    / /... Logic to add new attributes earlier}}Copy the code

/ SRC /mini-react/commit.js; / SRC /mini-react/commit.js; / SRC /mini-react/commit.js;

+ import { updateAttributes } from './react-dom';
// ...

function commitWork(fiber) {
  // ...
  if (fiber.flag === 'Placement') {
    // ...
+ } else if (fiber.flag === 'Update') {
+ const { children, ... newAttributes } = fiber.element.props;
+ const oldAttributes = Object.assign({}, fiber.alternate.element.props);
+ delete oldAttributes.children;
+ updateAttributes(fiber.stateNode, newAttributes, oldAttributes);
  }
  commitWork(fiber.sibling)
}
Copy the code

Deletion — delete dom πŸ‘‰

When fiber is marked with the flag label for Deletion, it means that the element is deleted. For deleting the element, we need to consider two questions:

  1. Yeah, yeahDeletionThe fiber of flag is shown in the previous current Fiber tree, but not in the workInProgress Fiber tree, so we cannot find it in the workInProgress fiber tree traversing.
  2. To delete an element, just remove it from its parent node, without traversing the entire Fiber tree

So based on the above two points, we need a global deletions array to store all the corresponding fibers to delete the DOM.

/ SRC /mini-react/fiber.js; / SRC /mini-react/fiber.js; / SRC /mini-react/fiber.

let deletions = []; // Remove fiber from dom

// Add a fiber to the deletions array
export function deleteFiber(fiber) {
  deletions.push(fiber);
}

// Get the deletions array
export function getDeletions() {
  return deletions;
}
Copy the code

Then in the performUnitOfWork function, call deleteFiber every time you add the flag side effect label on the fiber Deletion, and add the fiber to the deletions array:

+ import { deleteFiber } from './fiber';export function reconcileChildren(workInProgress, elements) { // ... If (oldFiber) {// oldFiber exists, delete oldFiber oldFiber. Flag = 'Deletion';+ deleteFiber(oldFiber);
  }
  
  // ...
}
Copy the code

/ SRC /mini-react/commit.js add the corresponding logic to delete the DOM. For deleting the DOM, we only need to iterate over the array deletions to perform the deletion action. After deleting the DOM, we directly return the recursive operation. The adjusted commit.js content is as follows (see github for the code modification) :

import { updateAttributes } from './react-dom';
import { getDeletions } from './fiber';

// Commit from the root node
export function commitRoot(rootFiber) {
  const deletions = getDeletions();
  deletions.forEach(commitWork);

  commitWork(rootFiber.child);
}

// Perform commit recursively, without breaking the process
function commitWork(fiber) {
  if(! fiber) {return;
  }

  let parentDom = fiber.return.stateNode;
  if (fiber.flag === 'Deletion') {
    if (typeoffiber.element? .type ! = ='function') {
      parentDom.removeChild(fiber.stateNode);
    }
    return;
  }

  // depth-first traversal, child first, sibling later
  commitWork(fiber.child);
  if (fiber.flag === 'Placement') {
    / / add the dom
    const targetPositionDom = parentDom.childNodes[fiber.index]; // To insert before that DOM
    if (targetPositionDom) {
      // If targetPositionDom exists, insert
      parentDom.insertBefore(fiber.stateNode, targetPositionDom);
    } else {
      // targetPositionDom does not exist, inserted lastparentDom.appendChild(fiber.stateNode); }}else if (fiber.flag === 'Update') {
    const{ children, ... newAttributes } = fiber.element.props;const oldAttributes = Object.assign({}, fiber.alternate.element.props);
    delete oldAttributes.children;
    updateAttributes(fiber.stateNode, newAttributes, oldAttributes);
  }

  commitWork(fiber.sibling);
}
Copy the code

Finally, remember that the workLoop function in/SRC /mini-react/fiber.js will empty the deletions array after the commitRoot is finished:

Function workLoop(deadline) {let shouldYield = false; while (nextUnitOfWork && ! ShouldYield) {// loop to performUnitOfWork(nextUnitOfWork); shouldYield = deadline.timeRemaining() < 1; } if (! NextUnitOfWork && workInProgressRoot) {// commitRoot(workInProgressRoot); currentRoot = workInProgressRoot; workInProgressRoot = null;+ deletions = [];
  }
  requestIdleCallback(workLoop);
}
Copy the code

Check the effect

code

Now that we have added, updated, and removed dom content, we can change the JSX content in/SRC /index.js with a 5s delay. After 5s, we will delete a label, remove the red font style of P label, and set the font size of Li label (this part of the code changes are only used for effect preview, and will not be submitted) :

import { Component } from 'react';
import ReactDOM from './mini-react/react-dom';
import './index.css';

class ClassComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {};
  }

  render() {
    return (
      <div className="class-component">
        <div>this is a class Component</div>
        <div>prop value is: {this.props.value}</div>
      </div>); }}function FunctionComponent(props) {
  return (
    <div className="function-component">
      <div>this is a function Component</div>
      <div>prop value is: {props.value}</div>
    </div>
  );
}

const jsx = (
  <div className="deep1-box">
    <ClassComponent value={666} />
    <FunctionComponent value={100} />
    <div className="deep2-box-1">
      <a href="https://github.com/zh-lx/mini-react">mini react link</a>
      <p style={{ color: 'red' }}> this is a red p</p>
      <div className="deep3-box">
        {true && <div>condition true</div>}
        {false && <div>condition false</div>}
        <input
          type="button"
          value="say hello"
          onClick={()= >{ alert('hello'); }} / ></div>
    </div>
    <div className="deep2-box-2">
      {['item1', 'item2', 'item3'].map((item) => (
        <li key={item}>{item}</li>
      ))}
    </div>
  </div>
);

ReactDOM.render(jsx, document.getElementById('root'));

setTimeout(() = > {
  const jsx = (
    <div className="deep1-box">
      <ClassComponent value={666} />
      <FunctionComponent value={100} />
      <div className="deep2-box-1">
        <p> this is a red p</p>
        <div className="deep3-box">
          {true && <div>condition true</div>}
          {false && <div>condition false</div>}
          <input
            type="button"
            value="say hello"
            onClick={()= >{ alert('hello'); }} / ></div>
      </div>
      <div className="deep2-box-2">
        {['item1', 'item2', 'item3'].map((item) => (
          <li style={{ fontSize: '20px'}}key={item}>
            {item}
          </li>
        ))}
      </div>
    </div>
  );

  ReactDOM.render(jsx, document.getElementById('root'));
}, 5000);
Copy the code

preview

The preview is as follows:

Six: implement React.Component

The React.Component API in/SRC /index is still referenced from the React library, so we’ll implement it ourselves.

Improve class component functionality πŸ‘‰

Now let’s improve our ClassComponent by adding a button that increases count by 1:

class ClassComponent extends Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  addCount = () = > {
    this.setState({
      count: this.state.count + 1}); };render() {
    return (
      <div className="class-component">
        <div>this is a class Component</div>
        <div>prop value is: {this.props.value}</div>
        <div>count is: {this.state.count}</div>
        <input type="button" value="add count" onClick={this.addCount} />
      </div>); }}Copy the code

Implement setState πŸ‘‰

As we know, a class component creates an instance of the class and calls the Render method on the instance to return JSX. When we perform setState, we need to change the value of state and then trigger an update of the class component to render the new DOM.

So we need to adjust the rendering logic of the class component. We will separate the iterating logic of the class component from the performUnitOfWork function and create a new updateClassComponent to extend it:

Function performUnitOfWork(workInProgress) {// On their return iteration if (type. The prototype. IsReactComponent) {/ / class components- const { props, type: Comp } = workInProgress.element;
- const component = new Comp(props);
- const jsx = component.render();
- children = [jsx];
+ updateClassComponent(workInProgress);
  }

  // ...
}
Copy the code

Then in the updateClassComponent function, we compare fiber. Alternate to see if fiber exists. If it does, the Component has been rendered before, so we can reuse the previous instance of the class (the state of the previous instance holds the latest state, otherwise the state of the new instance will reset), and then create a _UpdateProps method on the Component class. Update the latest props; If it does not exist, the class method is called to create a new instance of the class and render.

The code is as follows:

function updateClassComponent(fiber) {
  let jsx;
  if (fiber.alternate) {
    // There are old components, reuse
    const component = fiber.alternate.component;
    fiber.component = component;
    component._UpdateProps(fiber.element.props);
    jsx = component.render();
  } else {
    // Create a new component
    const { props, type: Comp } = fiber.element;
    const component = new Comp(props);
    fiber.component = component;
    jsx = component.render();
  }

  reconcileChildren(fiber, [jsx]);
}
Copy the code

/ SRC /mini-react: / SRC /mini-react: / SRC /mini-react: / SRC /mini-react: / SRC /mini-react: / SRC /mini-react: / SRC /mini-react

  1. The Component class takes the props argument and mounts it tothisOn the object
  2. Add on the prototype chainisReactComponentProperty used by React to identify whether it is a class or function component
  3. Add on the prototype chainsetStateMethod, which accepts aobjectOr is itfunctionType, if yesfunctionType that the function acceptsthis.state ε’Œ this.propsReturns the updated state value and merges it tothis.state; If it isobjectType, directly merged tothis.stateIn the. And then callcommitRenderFunction to start the update (the logic of this function will be explained next)
  4. Add on the prototype chain_UpdatePropsMethod to update props when updating a class component

/ SRC /mini-react/react.

import { commitRender } from './fiber';
export class Component {
  constructor(props) {
    this.props = props;
  }
}
Component.prototype.isReactComponent = true;

Component.prototype.setState = function (param) {
  if (typeof param === 'function') {
    const result = param(this.state, this.props);
    this.state = { ... this.state, ... result, }; }else {
    this.state = { ... this.state, ... param, }; } commitRender(); }; Component.prototype._UpdateProps =function (props) {
  this.props = props;
};
Copy the code

Then go back to the commitRender function, where the logic is simple: Set currentRoot as workInProgressRoot and nextUnitOfWork to it to trigger render:

export function commitRender() {
  workInProgressRoot = {
    stateNode: currentRoot.stateNode, // Record the corresponding real DOM node
    element: currentRoot.element,
    alternate: currentRoot,
  };
  nextUnitOfWork = workInProgressRoot;
}
Copy the code

/ SRC /index.js react.component.js

- import { Component } from 'react';
+ import { Component } from './mini-react/react';
Copy the code

Results the preview

The effect preview is as follows, very nice!

Hooks πŸ‘‰

Finally, we implement the hooks function of the function component.

Improve function component functionality

As with the class component, we first refine the functionality of the function component by introducing the useState hook and then making the value of count +1 when the button is clicked. The code is as follows:

import { useState } from 'react';

function FunctionComponent(props) {
  const [count, setCount] = useState(0);
  const addCount = () = > {
    setCount(count + 1);
  };
  return (
    <div className="function-component">
      <div>this is a function Component</div>
      <div>prop value is: {props.value}</div>
      <div>count is: {count}</div>
      <input type="button" value="add count" onClick={addCount} />
    </div>
  );
}
Copy the code

Implement useState

As mentioned above, the class component changes the state of the current class instance when calling the setState API and triggers an update to render the DOM. But in the setState of a class component, we can get the class instance from this and then get the state, but we can’t get the function component from this, so how do we do that?

We can set a global variable currentFunctionFiber in/SRC /mini-react/fiber.js to point to fiber corresponding to the function component currently processed in render and use it to mount the current hooks of this function component. Because there can be multiple hooks in a function component, we also need a global hookIndex variable to record the number of hooks in the current function component that are currently executing. / SRC /mini-react/react /react/currentFunctionFiber/hookIndex

let currentFunctionFiber = null; // The currently executing function component corresponds to fiber
let hookIndex = 0; // The subscript of the currently executing function component hook

// Get the fiber corresponding to the currently executing function component
export function getCurrentFunctionFiber() {
  return currentFunctionFiber;
}

// Get the current hook subscript
export function getHookIndex() {
  return hookIndex++;
}
Copy the code

We then separate the processing logic of the performUnitOfWork function component into an updateFunctionComponent as well:

function performUnitOfWork(workInProgress) {
  // ...

  if (typeof type === 'function') {// The current fiber corresponds to the React component. On their return iteration if (type. The prototype. IsReactComponent) {/ / class components updateClassComponent (workInProgress); } else {// Function components- const { props, type: Fn } = workInProgress.element;
- const jsx = Fn(props);
- children = [jsx];
+ updateFunctionComponent(workInProgress);}} / /... }Copy the code

The updateFunctionComponent function points currentFunctionFiber to workInProgress and empties the hooks array mounted on it, resetting the global hookIndex to 0. Then call the function component constructor and return the corresponding JSX structure as follows:

// Update the function component
function updateFunctionComponent(fiber) {
  currentFunctionFiber = fiber;
  currentFunctionFiber.hooks = [];
  hookIndex = 0;
  const { props, type: Fn } = fiber.element;
  const jsx = Fn(props);
  reconcileChildren(fiber, [jsx]);
}
Copy the code

The last step is to implement our useState function. It first accepts an initial value and returns an array, then retrieves currentFunctionFiber and hookIndex using getCurrentFunctionFiber and getHookIndex functions.

Then according to the currentFunctionFiber. Alternate. Hooks. [hookIndex] judgment is there already exist corresponding old hooks, if any, are directly and use in order to get the hook before the status value; If not, initialize a hook with the initialization value passed in.

A hook has two properties:

  • State: indicates the current stateuseStateHook The value to return
  • Queue: Stores the render process to thisstateArray of operations performed

The first value of the array is hook. State and the second value is a function that pushes the received arguments to hook. Queue.

In summary, the above codes are as follows:

export function useState(initial) {
  const currentFunctionFiber = getCurrentFunctionFiber();
  const hookIndex = getHookIndex();
  // Take the hook before the currently executing function component
  constoldHook = currentFunctionFiber? .alternate? .hooks? .[hookIndex];// oldHook exists, take the previous value, otherwise take the current value
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [].// setState may be called several times during a function execution, putting it into a queue for execution
  };

  const actions = oldHook ? oldHook.queue : [];
  actions.forEach((action) = > {
    hook.state = action(hook.state);
  });

  const setState = (action) = > {
    if (typeof action === 'function') {
      hook.queue.push(action);
    } else {
      hook.queue.push(() = > {
        return action;
      });
    }
    commitRender();
  };
  currentFunctionFiber.hooks.push(hook);
  return [hook.state, setState];
}
Copy the code

Results the preview

And finally in our/src/index.jsTo introduce their own implementationuseStateAfter that, take a look at the results:

conclusion

React code: Fiber, Render and Commit phases, Diff algorithm, class components, function components, hooks, etc.

Of course, there are some shortcomings in the source code, such as dom creation without considering React.fragment, other built-in components, nested list rendering, etc. In addition, the implementation of our DIFF algorithm is a simple version of DIFF, without considering the common diff of key value and type, etc. Those who are interested can go to the Mini React warehouse for further expansion and improvement.

React17 react17 react17 react17 react17 React17 React17 React17 React17