Introduction to the

This article will write a simple React class framework from scratch. Easy access to the React code base to understand the Fiber principle and hooks implementation.

React.createElement

We’ll start by writing createElement, a function that converts JSX into a virtual DOM (JS object). Here we use @babel/plugin-transform-react-jsx to automatically convert.

// jsx
const element = (
  <div id="name">
    <a>name</a>
    <b />
  </div>
)
// At compile time the plug-in converts JSX to
const element = React.createElement(
  "div",
  { id: "name" },
  React.createElement("a".null."name"),
  React.createElement("b"))Copy the code

Here we’re going to put both the properties and the children in the props parameter. Because the text/number type is not an object, we need to wrap the text object here for the sake of brevity (react is used in other ways for performance).

// ----------------- React -----------------
/** * Create text node *@param {*} text 
 */
function createTextElement(text) {
  return {
    type: "TEXT".props: {
      nodeValue: text,
      children: [].}}; }const React = {};
// type The label name of the DOM node
// All attributes on the attrs node
// children node
React.createElement = function (type, props, ... children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) = >
        typeof child === "object" ? child : createTextElement(child)
      ),
    },
  };
};
Copy the code

Construction Project Details

ReactDOM.render

Next, we implement the React entry reactdom.render (Element, container) function. Receives a virtual DOM object and a real DOM (container). Basically, it takes the virtual DOM and converts it into a real DOM and puts it into a container.

  1. Determine the virtual DOM type and create a real DOM node based on the type.
  2. Assign the props of the virtual DOM to the real DOM node.
  3. Append the real DOM node to the container.
  4. There are child virtual DOM, looping child nodes, recursively processing each node.
// ----------------- ReactDOM -----------------
const ReactDOM = {};
/ * * * *@param {*} VDom Virtual DOM *@param {*} The container vessel * /
ReactDOM.render = function (vDom, container) {
  // Create the real DOM
  const dom = vDom.type == "TEXT"
      ? document.createTextNode("")
      : document.createElement(vDom.type);

  // Get all attributes except children
  const isProperty = (key) = >key ! = ="children";
  Object.keys(vDom.props)
    .filter(isProperty)
    .forEach((name) = > {
      dom[name] = vDom.props[name];
    });

  // Recursive child node
  vDom.props.children.forEach((child) = > ReactDOM.render(child, dom));

  // The real DOM is put into the container
  container.appendChild(dom);
};


// ----------------- use -----------------
ReactDOM.render(<div id="name">1111</div>.document.getElementById('root'));
Copy the code

Using Fiber architecture

What is Fiber architecture: Fiber architecture = Fiber node + Fiber scheduling algorithm

  1. React Fiber Architecture
  2. The React Fiber,

Here is not a detailed explanation of the content, for a detailed understanding of the above article.

1. The implementation browser does not interrupt the recursive child nodes of the previous implementation when it is idle. If the DOM tree is large, it can block the main thread, causing the page to stall. So we split the task into units of work, and after each unit is complete, we ask the browser to determine if it has time to continue, and then interrupt. The requestIdleCallback browser used here calls this method every frame and returns how much time is left. React doesn’t use this method, of course, but implements a more powerful Scheduler of its own.

// Unit of work to be performed
let nextUnitOfWork = null;
/** * Determine whether there is time to continue *@param {*} deadline* /
function workLoop(deadline) {
  // Determine the remaining time
  let shouldYield = false;
  while(nextUnitOfWork && ! shouldYield) {// Returns the next unit of work
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }

  if (nextUnitOfWork) {
    // Unit of work not executed continues to the browserrequestIdleCallback(workLoop); }}/** * Operation node *@param {*} fiber* /
function performUnitOfWork(nextUnitOfWork) {}Copy the code

2. Fiber node

In order to realize the task can interrupt, restore the function. We need a data structure:A Fiber list tree.

eachFiber tree nodeI can find the next one to executenodeWhat it is. So it’s in every nodeReturn points to its parent Fiber node, child points to its child Fiber node, and sibling points to its brother Fiber nodeAs shown in figure:

The root of the data structure is created in render, and then we assign it to nextUnitOfWork to enter the loop and call performUnitOfWork to process the current node.

inperformUnitOfWorkWe need to implement

  1. Create real DOM nodes.
  2. The real node goes into the parent container.
  3. Create Fiber nodes for each child node.
  4. Find the next unit of work

The search sequence is to find the child node first, there are no children, find the sibling node of the child node, there are no children find the sibling node of the parent node, and keep repeating the same operation. Div #root -> APP -> div. APP -> p -> text -> span -> text.

Let’s start by modifying the Render function and encapsulating the ability to create the real DOM as a public function.

/ / to enter
ReactDOM.render = function (element, container) {
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    },
  };

  requestIdleCallback(workLoop);
}

/** * Create node *@param {*} fiber* /
function createDom(fiber) {
  const dom =
    fiber.type == "TEXT"
      ? document.createTextNode("")
      : document.createElement(fiber.type);
  // Get all attributes except children
  const isProperty = (key) = >key ! = ="children";
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach((name) = > {
      dom[name] = fiber.props[name];
    });

  return dom;
}
Copy the code

Then add the corresponding function in FormUnitofWork.

/** * Operation node *@param {*} fiber* /
function performUnitOfWork(fiber) {
  // Create a real node
  if(! fiber.dom) { fiber.dom = createDom(fiber); }// Put it in the parent container
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom);
  }

  // Create a DOM for each child node
  const elements = fiber.props.children;

  let index = 0;
  // Save sibling nodes
  let prevSibling = null;
  /** * Create fiber nodes for each child node */
  while (index < elements.length) {
    const element = elements[index];
    / / child nodes
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null};if (index === 0) {
      // If it is the first element, set it to a child node
      fiber.child = newFiber;
    } else {
      // The first element is not set as a sibling of the previous one
      // Set sibling nodes for the previous node
      prevSibling.sibling = newFiber;
    }
    // Cache the last node
    prevSibling = newFiber;
    index++;
  }

  // Find the next unit of work
  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      // If there are sibling nodes, return sibling nodes
      return nextFiber.sibling;
    }
    // If there are no siblings, return to the parent -- continue the loop to find the parent's siblings.nextFiber = nextFiber.parent; }}Copy the code

Render and commit phases

Each time a node is processed, the new node created is put into the DOM. Because this process is interruptible, the UI presentation is incomplete. So we remove the code in performUnitOfWork that puts the real DOM into the container.

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

The commit phase is to commit the entire Fiber tree, so we need to create a variable to hold the fiber tree.

/ / fiber tree
let wipRoot = null
/ * * * *@param {*} VDom Virtual DOM *@param {*} The container vessel * /
ReactDOM.render = function (element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
  };

  nextUnitOfWork = wipRoot

  requestIdleCallback(workLoop);
};
Copy the code

You need to commit immediately after the unit of work is executed, add a judgment in the workLoop, commit the entire Fiber tree after execution and render it to the page.

/** * Determine whether there is time to continue *@param {*} deadline* /
function workLoop(deadline) {
  // Determine the remaining time
  let shouldYield = false;
  while(nextUnitOfWork && ! shouldYield) {// Returns the next unit of work
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }
  if (nextUnitOfWork) {
    // Unit of work not executed continues to the browser
    requestIdleCallback(workLoop);
  }
  if(! nextUnitOfWork && wipRoot) {// Perform the commit render
    commitRoot()
  }
}

/** * Submit render */
function commitRoot() {
  commitWork(wipRoot.child)
  wipRoot = null
}
/** * The actual DOM of the node is submitted to the parent DOM *@param {*} fiber 
 */
function commitWork(fiber) {
  if(! fiber) {return
  }
  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  // Operates on child and sibling nodes
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}
Copy the code

Add the Diff comparison

To do this, you need to save the previous Fiber data and add the currentRoot variable. Nodes are not modified during the comparison phase. So define deletions to collect nodes to be deleted during the render phase.

// Last submitted tree
let currentRoot = null
// The node to be deleted after comparison
let deletions = null
/ * * * *@param {*} Element virtual DOM *@param {*} The container vessel * /
ReactDOM.render = function (element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    alternate: currentRoot, // Save the last committed tree at the root node
  };
  nextUnitOfWork = wipRoot;
  deletions = [];
  requestIdleCallback(workLoop);
}

/** * Submit render */
function commitRoot() {
  // Process the node to be deleted
  deletions.forEach(commitWork)
  // Process the node
  commitWork(wipRoot.child)
  currentRoot = wipRoot // Save the submitted tree
  wipRoot = null
}
Copy the code

Now it is time to modify the performUnitOfWork function to add the reconcileChildren function to realize the operation of reconcileChildren’s fiber nodes, and to label the nodes at this stage to determine their operation type.

/** * Operation node *@param {*} fiber* /
function performUnitOfWork(fiber) {
  // Create a real node
  if(! fiber.dom) { fiber.dom = createDom(fiber); }// // into the parent container
  // if (fiber.parent) {
  // fiber.parent.dom.appendChild(fiber.dom);
  // }

  // Create a DOM for each child node
  const elements = fiber.props.children;
  // Compare child nodes
  reconcileChildren(fiber, elements);
  // let index = 0;
  // // Save the sibling node
  // let prevSibling = null;
  / / / * *
  // * Create fiber nodes for each child node
  / / * /
  // while (index < elements.length) {
  // const element = elements[index];
  // // child node
  // const newFiber = {
  // type: element.type,
  // props: element.props,
  // parent: fiber,
  // dom: null,
  / /};

  // if (index === 0) {
  // // is set to child if it is the first element
  // fiber.child = newFiber;
  // } else {
  // // is not a sibling of the first element set to the previous
  // // sets sibling nodes for the previous node
  // prevSibling.sibling = newFiber;
  / /}
  // // Caches the last node
  // prevSibling = newFiber;
  // index++;
  // }

  // Find the next unit of work
  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      // If there are sibling nodes, return sibling nodes
      return nextFiber.sibling;
    }
    // If there are no siblings, return to the parent -- continue the loop to find the parent's siblings.nextFiber = nextFiber.parent; }}/** * compares the child node *@param {*} WipFiber Current operating node *@param {*} Elements Child node */
function reconcileChildren(wipFiber, elements) {
  let index = 0;

  // Last DOM committed
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;

  // Save sibling nodes
  let prevSibling = null;
  /** * Create fiber nodes for each child node */
  // oldFiber ! = null As new child nodes become fewer and old nodes continue to be marked with delete labels
  while(index < elements.length || oldFiber ! =null) {
    const element = elements[index];
    const newFiber = null
    // The type is the same
    let sameType = element && oldFiber && element.type === oldFiber.type;

    // Use the same type of the original node to label the modified props
    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE"}; }// Create a new node with a new label
    if(element && ! sameType) { newFiber = {type: element.type,
        props: element.props,
        dom: null.parent: wipFiber,
        alternate: null.effectTag: "PLACEMENT"}; }// Old nodes of different types are marked with delete labels
    if(oldFiber && ! sameType) { oldFiber.effectTag ="DELETION";
      deletions.push(oldFiber);// Collect the nodes to be deleted
    }

    // If no sibling node is set to null, the operation of the current node's old child node is complete
    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }

    if (index === 0) {
      // If it is the first element, set it to a child node
      wipFiber.child = newFiber;
    } else {
      // The first element is not set as a sibling of the previous one
      // Set sibling nodes for the previous node
      prevSibling.sibling = newFiber;
    }
    // Cache the last nodeprevSibling = newFiber; index++; }}Copy the code

The last step is to modify the commit operation, which determines how to handle the node based on the previously defined label.

/** * modify the DOM node *@param {*} fiber 
 */
function commitWork(fiber) {
  if(! fiber) {return
  }
  const domParent = fiber.parent.dom
  // domParent.appendChild(fiber.dom)

  if(fiber.effectTag === "PLACEMENT"&& fiber.dom ! =null) {// Add an operation
    domParent.appendChild(fiber.dom)
  }else if(fiber.effectTag === "DELETION"&& fiber.dom ! =null) {// Delete a node
    domParent.removeChild(fiber.dom);
  }else if(fiber.effectTag === "UPDATE"&& fiber.dom ! =null) {// Modify the node
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  }
  
  // Handle sibling and child nodes
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

// The prefix on returns true
const isEvent = (key) = > key.startsWith("on");
// Remove children and on
const isProperty = (key) = >key ! = ="children" && !isEvent(key);
// The previous time was different from this time
const isNew = (prev, next) = > (key) = >prev[key] ! == next[key];// Filter to match a value that is not available next time
const isGone = (prev, next) = > (key) = >! (keyin next);
/** * Modify node attributes *@param {*} Dom The actual DOM * of the current node@param {*} PrevProps Props of the previous time@param {*} NextProps this time */
function updateDom(dom, prevProps, nextProps) {
  // Empty old events
  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]);
    });

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

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

  // Set the new value
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach((name) = > {
      if(dom instanceof Object &&  dom.setAttribute){
        dom.setAttribute(name, nextProps[name]);
      }else{ dom[name] = nextProps[name]; }}); }Copy the code

We implemented a new attribute comparison method, modifying the createDom function to use updateDom.

/** * Create node *@param {*} fiber* /
function createDom(fiber) {
  const dom =
    fiber.type == "TEXT"
      ? document.createTextNode("")
      : document.createElement(fiber.type);
  // Set the properties
  updateDom(dom, {}, fiber.props)
  // Get all attributes except children
  // const isProperty = (key) => key ! == "children";
  // Object.keys(fiber.props)
  // .filter(isProperty)
  // .forEach((name) => {
  // dom[name] = fiber.props[name];
  / /});
  return dom;
}
Copy the code

Join components

A component differs from a normal DOM in two ways: the Fiber node in the component does not have a real DOM, and the child nodes of the component are returned by execution. So in the performUnitOfWork function, add a way to handle real DOM creation and sub-DOM acquisition separately, depending on the fiber node type.

/** * Operation node *@param {*} fiber* /
function performUnitOfWork(fiber) {
  // // Create a real node
  // if (! fiber.dom) {
  // fiber.dom = createDom(fiber);
  // }

  // Whether it is a component
  const isFunctionComponent = fiber.type instanceof Function;
  if (isFunctionComponent) {
    / / component
    updateFunctionComponent(fiber);
  } else {
    updateHostComponent(fiber);
  }

  // // into the parent container
  // if (fiber.parent) {
  // fiber.parent.dom.appendChild(fiber.dom);
  // }

  // // creates a DOM for each child node
  // const elements = fiber.props.children;
  // // Comparison of child nodes
  // reconcileChildren(fiber, elements);
  // let index = 0;
  // // Save the sibling node
  // let prevSibling = null;
  / / / * *
  // * Create fiber nodes for each child node
  / / * /
  // while (index < elements.length) {
  // const element = elements[index];
  // // child node
  // const newFiber = {
  // type: element.type,
  // props: element.props,
  // parent: fiber,
  // dom: null,
  / /};

  // if (index === 0) {
  // // is set to child if it is the first element
  // fiber.child = newFiber;
  // } else {
  // // is not a sibling of the first element set to the previous
  // // sets sibling nodes for the previous node
  // prevSibling.sibling = newFiber;
  / /}
  // // Caches the last node
  // prevSibling = newFiber;
  // index++;
  // }

  // Find the next unit of work
  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      // If there are sibling nodes, return sibling nodes
      return nextFiber.sibling;
    }
    // If there are no siblings, return to the parent -- continue the loop to find the parent's siblings.nextFiber = nextFiber.parent; }}/** * Non-component creation *@param {*} fiber* /
function updateHostComponent(fiber) {
  if(! fiber.dom) { fiber.dom = createDom(fiber); }// Compare child nodes
  reconcileChildren(fiber, fiber.props.children);
}

/** * Component creation *@param {*} fiber* /
function updateFunctionComponent(fiber) {
  // Run the function to get the child node
  const children = [fiber.type(fiber.props)];
  // Compare child nodes
  reconcileChildren(fiber, children);
}

Copy the code

Next comes the commit phase, because the component doesn’t have a real DOM, so the child nodes need to be put into the component node, the parent node’s real DOM. Delete works the same way. If there is no real DOM, delete the real DOM of the child node.

/** * modify the DOM node *@param {*} fiber 
 */
function commitWork(fiber) {
  if(! fiber) {return
  }
  // const domParent = fiber.parent.dom
  // domParent.appendChild(fiber.dom)
  // Get the parent node
  let domParentFiber = fiber.parent;
  // Determine if the parent has a real DOM and does not continue to look up
  while(! domParentFiber.dom) { domParentFiber = domParentFiber.parent; }// Get the real DOM of the nearest parent
  const domParent = domParentFiber.dom;

  if(fiber.effectTag === "ADD"&& fiber.dom ! =null) {// Add an operation
    domParent.appendChild(fiber.dom)
  }else if(fiber.effectTag === "DELETION"&& fiber.dom ! =null) {// Delete a node
    // domParent.removeChild(fiber.dom);
    commitDeletion(fiber, domParent);
  }else if(fiber.effectTag === "UPDATE"&& fiber.dom ! =null) {// Modify the node
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  }

  // Handle sibling and child nodes
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

/** * Delete node *@param {*} fiber 
 * @param {*} domParent 
 */
function commitDeletion(fiber, domParent){
  if(fiber.dom){
    domParent.removeChild(fiber.dom)
  }else{
    commitDeletion(fiber.child, domParent)
  }
}
Copy the code

Join useState

We know that useState is called while the function is running. In order for state to always save state, we need to initialize some global variables before calling, and we need to initialize hooks data on the Fiber node to save the last state.

let wipFiber = null;// Node for this operation
let hookIndex = null;// Index of state
/** * Component creation *@param {*} fiber* /
function updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0;// Index position of hook
  wipFiber.hooks = []

  // Run the function to get the child node and execute useState in the function
  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children);
}
Copy the code

To fully implement useState, 1. Obtain and save state data. When the function calls useState, it first checks whether the previous fiber node has a value. If there is a value, use the previous value, and then put the value into the current node. (Note that a function can have multiple hooks, adding indexes by order of execution)

/** * hook function *@param {*} initial* /
ReactDOM.useState = function(initial) {
  // Get the value of the corresponding index by the order of execution
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  // Use the default value if there is no value
  const hook = {
    state: oldHook ? oldHook.state : initial,
  }
  // Save data to queue
  wipFiber.hooks.push(hook)
  // The index increment function is executed from the top down. This is why react useState cannot be added in a judgment
  hookIndex++ 
  // Return the corresponding value
  return [hook.state]
}
Copy the code

2. Modify the receiving and executing of status actions. To update the state, we define a setState action to receive the modified state. Add this action to the hooks queue. We then do something similar to what we did in the Render function, setting the fiber node for this operation as the next unit of work and entering the loop. When useState is executed again, it retrieves the previously received change state action and executes all of it, changing the current state value, and finally returning the latest state data.

/** * hook function *@param {*} initial* /
ReactDOM.useState = function(initial) {
  // Get the value of the corresponding index by the order of execution
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  // Use the default value if there is no value
  // Initializes the data in the modified state
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [],}// Get the last modification
  const actions = oldHook ? oldHook.queue : []
  // Perform operations to modify state
  actions.forEach(action= > {
    if(action instanceof Function){
      hook.state = action(hook.state)
    }else{
      hook.state = action
    }
  })

  // Change the status
  const setState = action= > {
    // Save the operation
    hook.queue.push(action)

    // Update the next unit of work after modification
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    nextUnitOfWork = wipRoot;
    deletions = [];
    // Add loop
    requestIdleCallback(workLoop);
  }

  // Save data to queue
  wipFiber.hooks.push(hook)
  hookIndex++ // Index increment
  // Returns the latest value
  return [hook.state,setState]
}
Copy the code

Test using

// ----------------- use -----------------
const APPS = () = >{
  const [state, setState] = ReactDOM.useState(1)
  return (
    <h1 class="bububu" onClick={()= > {setState(c => c + 1)}}>
      Count: {state}
    </h1>)}const APPP = () = >{
    const [state, setState] = ReactDOM.useState(1)
    return (
      <h1 class={state= = =2 ? "sss":""} onClick={()= > {setState(c => c + 1)}}>
        Countsss: {state}
      </h1>)}const APP = () = >{
    return (
      <div>
          <APPS />
          <APPP />
      </div>
    )
  }
ReactDOM.render( <APP />.document.getElementById('root'));
Copy the code

Source code address: github.com/nie-ny/reac…

Refer to the article

React Fiber This is probably the most popular way to open a React Fiber