1. JSX and virtual DOM

Let’s start with the basic Hello World code at the beginning of the React official document:

ReactDOM.render(
  <h1>Hello, world!</h1>.document.getElementById('root'));Copy the code

This code means to render the H1-wrapped JSX element to the HTML element with id “root” via the reactdom.render () method. In addition to the document.getelementById () method you already know in JS, there are two other things in this code:

  • In order toh1Label wraps JSX elements
  • ReactDOM.render()methods

These two knowledge points correspond to the core problems to be solved in React:

  • Why and how is the virtual DOM (represented by JSX) used?
  • How to process the virtual DOM to render it efficiently?

1.1 What is a Virtual DOM? Why use the virtual DOM?

A virtual DOM is essentially a DOM node represented by a JavaScript object, containing the tag, props, and children of the node.

Why use the virtual DOM? Because direct operation of real DOM is tedious and inefficient, part of the expensive browser redrawing work is transferred to relatively cheap storage and computing resources through virtual DOM.

1.2 How do I convert JSX to a Virtual DOM?

Babel allows you to compile JSX into specific JavaScript objects. The example code is as follows:

// JSX
const e = (
	<div id="root">
		<h1 className="title">Title</h1>
  </div>
);
// Babel compilation results (before React17), notice the nested structure of child elements
var e = React.createElement(
	"div",
  { id: "root"},
	React.createElement(
		"h1",
		{ className: "title" },
		"Title"));// After React17, the results are different. The method of creating nodes is derived from React, but the basic principle is the same
Copy the code

1.3 How do I Render the Virtual DOM?

As you can see from the compilation of Babel in the previous section, the virtual DOM contains all the information needed to create the DOM. For the first rendering, just create the DOM node based on this information.

But the real value of the virtual DOM is in “updating” : when certain items in a list change, or several items are deleted or added, how do you minimize updating the real DOM by comparing the virtual DOM tree before and after? That’s the core goal of React.

2. React Diffing

“Diffing” refers to “finding differences”, which is to solve the core goal of React mentioned above — how to transform the old DOM tree into a new DOM tree with the minimum number of operations by comparing the old and new virtual DOM trees.

In the field of algorithms, the current optimal algorithm complexity for the transformation of two trees is O(n**3), where n is the number of nodes. That means that when you have 1,000 elements on a tree, you need a billion comparisons, which is far from efficient.

React proposed a set of heuristic algorithms with complexity O(n) based on the following two assumptions

  1. Different types of elements (i.e., tag names, component names) produce different trees.
  2. By setting thekeyProperty to identify whether a set of sibling child elements remains the same before and after rendering.

In practice, both of these assumptions hold true in the vast majority of scenarios.

2.1 Diffling algorithm description

Different types of elements/components

When an element’s label or component name changes, the entire subtree with that element as the root node is directly unloaded and replaced.

Elements of the same type

When element tags are the same, React retains the DOM node, only compares and updates the changed attributes, such as className, title, etc., and then recursively compares its children.

For the style properties, React will continue the in-depth comparison and update only the changed properties, such as color, fontSize, etc.

Components of the same type

When components of props to update component instance remains the same, the React the calling component componentWillReceiveProps componentWillUpdate () () and componentDidUpdate () life cycle method, And execute the render() method.

The Diffing algorithm recursively compares the results of the new and old render() execution.

Recursion on child nodes

When a new child node is added to the end of a set of sibling child nodes (list), the overhead of the above Diffing algorithm is low; However, when a new element is inserted at the beginning of the list, the Diffing algorithm can only compare and reconstruct all subsequentchild nodes from the new element sequentially, resulting in a great waste of overhead.

The solution is to add a key attribute to a list of items so React can easily compare inserted or deleted items.

For the key attribute, it should be stable, predictable, and unique within the list (not necessarily globally unique), using the ID of the data as the key if it has one, or hashing out a key value from some of the fields in the data.

Avoid using an array index as a key, because when an element is inserted or deleted, the mapping between the following elements and the index will be incorrect, resulting in incorrect comparisons.

Avoid unstable keys (such as random numbers) that change with each rendering, resulting in the list items being rebuilt unnecessarily.

2.2 Recursive Diffing

It can be seen from the virtual DOM object in Section 1.2 that each node of the virtual DOM tree forms a nested tree structure through the children attribute, which means that the old and new virtual DOM trees should be recursively traversed and compared.

The strategy in Section 2.1 solves the time complexity of the Diffing algorithm, but we also face another major performance problem — the rendering thread of the browser and the execution thread of JS are mutually exclusive, which means that when there are too many DOM nodes, the construction and processing of the virtual DOM tree will occupy the main thread for a long time. Some operations that require high priority processing, such as user input and smooth animation, are blocked, seriously affecting the use experience.

Time Slice

In order to solve the problem of browser main thread blocking, which leads to the time slice of the strategy – the whole working process is decomposed into small units of work, and in your leisure time in a browser to a browser to perform these work units, after each execution unit has been completed, the browser can choose to interrupt rendering and handling other work need to deal with the higher priority.

The requestIdleCallback method is provided in the browser to implement this, adding the functions to be called to the execution queue, and the browser will call them one by one without affecting the processing of critical events.

Due to browser compatibility and the instability of the requestIdleCallback method, React implements a react-specific and more complete Scheduler similar to requestIdleCallback to trigger callbacks when idle. It also provides multiple priorities for tasks to set.

Recursion and time slicing

The time slicing strategy requires us to decompose the update operation of the virtual DOM into small units of work, with the following features:

  • Paused, recoverable updates;
  • Repeatable, coverage updates that can be skipped;
  • Updates with priority.

For a recursive form of the program, these are difficult to implement. You need a data structure at the top of the recursive form of the virtual DOM tree to assist with these features.

This is the refactoring algorithm core introduced by React16 — Fiber.

3. Fiber

Conceptually, a Fiber is a refactored virtual DOM node, and a Fiber is a JS object.

A unidirectional linked list structure is formed between the Fiber nodes to implement the aforementioned features: updates can be paused/resumed, skipped, and prioritized.

3.1 Fiber node

A Fiber node is a JS object whose key attributes can be classified as follows:

  • Structure information (pointer properties that make up a linked list)
    • Return: the parent node
    • Child: first child node
    • Sibling: The first sibling node on the right
    • Alternate: State of this node on adjacent updates. Used to compare changes before and after the node. Detailed in Section 3.3
  • Component information
    • Tag: component creation type, such as FunctionComponent, ClassComponent, and HostComponent
    • Key: indicates the key attribute
    • Type: component type, Function/Class component type is the corresponding Function/Class itself, Host component type is the corresponding element TagName
    • StateNode: the corresponding real DOM node
  • Props and state information for this update
    • PendingProps, memoizedProps
    • memoizedState
    • dependencies
    • updateQueue
  • Update the tag
    • EffectTag: Node update types, such as replace, update, delete, etc
    • NextEffect, firstEffect, lastEffect
  • Priority: Lanes and childrenLanes

3.2 the Fiber tree

As mentioned earlier, the Fiber node forms a one-way linked list structure with the return, child, and slibling attributes, and is still referred to as a “tree” to correspond to the DOM tree.

Like a DOM tree:

<div>
	<h1>Title</h1>
	<section>
		<h2>Section</h2>
		<p>Content</p>
	</section>
	<footer>Footer</footer>
</div>
Copy the code

The Fiber of its section node can be expressed as:

const sectionFiber = {
	key: "SECTION_KEY".child: h2Fiber,
	sibling: footerFiber,
	return: divFiber,
	alternate: oldSectionFiber, ... otherFiberProps, }Copy the code

Overall Fiber structure:

3.3 the Fiber architecture

Fiber architecture is the virtual DOM tree based on Fiber.

As described in Section 3.1, there is an important property in the Fiber node alternate, the word meaning “alternate.”

In fact, there are at most two Fiber trees in React:

  • The constructed Fiber Tree currently displayed on the screen is called the “Current Fiber Tree,” and the Fiber node in it is abbreviated ascurrFiber;
  • The Fiber Tree currently under construction is called the “WorkInProgress Fiber Tree” and we will abbreviate its Fiber node node aswipFiber.

The alternate properties of the nodes in the two trees point to the corresponding nodes in the other tree, namely: currfiber. alternate === wipFiber; wipFiber.alternate === currFber; They are used to compare the nodes before and after the update to determine how to update the node.

In React, the root node of the entire application is fiberRoot. When the wipFiber tree is built, FiberRoot. current switches from the currFiber tree root node to the wipFiber root node to complete the update operation.

3.1 Scheduling based on Fiber — Time slice

In Section 2.2 we discussed splitting units of work and executing them in time slices to avoid blocking the main thread. In the Fiber architecture, each Fiber node is a unit of work.

In the example code below, we demonstrate this using the browser-provided requestIdleCallback method, which executes a workLoop while the browser is idle, processes a Fiber node, and then continues or pauses for the next workLoop, as the case may be.

function workLoop(deadline) {
  let shouldYield = false;
  while(nextUnitOfWork && ! shouldYield) {// Process a Fiber node and return the next Fiber node. See section 3.3
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    // Pause processing demo: cancel loop processing when time is running out
    shouldYield = deadline.timeRemaining() < 1;
  }
  // When the execution is complete (there is no next execution unit), commit the entire DOM tree
  if(! nextUnitOfWork && wipRoot) { commitRoot(); } requestIdleCallback(workLoop); } requestIdleCallback(workLoop);Copy the code

3.2 Processing sequence of Fiber nodes — DFS

As can be seen from the above, Fiber nodes are interconnected through the return Child Sibling attribute and constitute a unidirectional linked list structure as a whole. Its scheduling mode is depth-first traversal:

  1. WipFiber tree Root node as the first execution unit;
  2. If the current execution unit has a child node, the child node is regarded as the next execution unit.
  3. Repeat 2 until the current execution unit has no child.
  4. If the sibling node exists in the current execution unit, the sibling node is taken as the next execution unit and returned to 2.
  5. If the current execution unit has no child and no sibling, return to the parent node and return to 4;
  6. Repeat 5; Until you return to the Root nodefiberRoot.currentOnly the root node of the wipFiber tree.

The above steps show that the Fiber node performs depth-first traversal “processing” in the order of child → Sibling → return, and then updates the Fiber tree. So how do you “process” the Fiber node?

3.3 Processing process of Fiber node

The process for the Fiber node is to execute a performUnitOfWork method, which receives a Fiber node to be processed, and then does the following:

  1. Improved building the Fiber node: Create the DOM and get the children

    • For HostComponent and ClassComponent, create and assign DOM nodes based on the relevant properties in FiberFiber.stateNodeProperties;
    • For FunctionComponent, get its children directly through a function call:Fiber.type(Fiber.props)
    // Execute the unit of work and return the next unit of work
    function performUnitOfWork(fiber) {
      // Build the fiber for the current node
      const isFunctionComponent = fiber.type instanceof Function;
      if (isFunctionComponent) {
        updateFunctionComponent(fiber);
      } else {
        updateHostComponent(fiber);
      }
    
      // Process the child nodes and build the Fiber tree
      const elements = fiber.props.children;
      reconcileChildren(fiber, elements);
    
      // TODO:Returns the next unit of execution
      // fiber.child || fiber.sibling || fiber.return
    }
    
    // Class/Host component: create DOM
    function updateHostComponent(fiber) {
      if(! fiber.dom) { fiber.dom = createDom(fiber); } reconcileChildren(fiber, fiber.props.children); }// Update the Function component. The Function component needs to get the child components from the return value
    // Note: the Function component has no DOM
    function updateFunctionComponent(fiber) {
      // Initialize hooks
      wipFiber = fiber;
      hookIndex = 0;
      fiber.hooks = [];
      const children = [fiber.type(fiber.props)]; // The Function component returns children
      reconcileChildren(fiber, children);
    }
    // TODO:ReconcileChildren deals with child nodes, as shown in Step 3
    Copy the code
  2. Get oldFiber, the value of Fiber since the last update, through fiber.alternate, and then build and Diff the children of the current Fiber in the next step.

    function reconcileChildren(wipFiber, elements) {
      let oldFiber = wipFiber.alternate
    			&& wipFiber.alternate.child;
      // ...
    }
    Copy the code
  3. Build your Children Fibers, and for each subfiber, do the following synchronously:

    • Build a linked list of fibers: Create a Fiber for each child element and combine the parent Fiber’schildProperty points to the first subfiber, and then the subfiber in ordersiblingProperty points to the next subfiber;
    • Contrast (Diffing) between old and new Fiber nodestype props keyTo determine whether the node can be directly reused, replaced, updated, or deleted by updating the Fiber node in itseffectTagPropertiesUpdate Placement PlacementAndUpdate DeletionFor processing during the commit update phase.
    function reconcileChildren(wipFiber, elements) {
      let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
      let index = 0;
      let prevSibling = null;
    
      while(index < elements.length || oldFiber ! = =null) {
        const element = elements[index];
        let newFiber = null;
    
        // Compare oldFiber to element
        const sameType = oldFiber && element && element.type === oldFiber.type;
    
        if (sameType) {
          // update the node
          newFiber = {
            type: oldFiber.type,
            props: element.props,
            dom: oldFiber.dom,
            parent: wipFiber,
            alternate: oldFiber,
            effectTag: "UPDATE"}; }if(element && ! sameType) {// add this node
          newFiber = {
            type: element.type,
            props: element.props,
            dom: null.parent: wipFiber,
            alternate: null.effectTag: "PLACEMENT"}; }if(oldFiber && ! sameType) {// delete the oldFiber's node
          oldFiber.effectTag = "DELETION";
          deletions.push(oldFiber);
        }
    
        if (oldFiber) {
          oldFiber = oldFiber.sibling;
        }
    
        if (index === 0) {
          wipFiber.child = newFiber;
        } else {
          prevSibling = newFiber;
        }
    
        prevSibling = newFiber;
        index++;
      }
    Copy the code
  4. Return the next unit of work in DFS order as follows:

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

When the DFS process returns to the root node, it indicates that the wipFiber tree for this update is completed and the next step is to commit the update.

3.4 Commit the update phase

At the start of this phase, the new Fiber tree is built and the Fiber nodes that need to be replaced, updated, or deleted are marked in their effecttags, so the first thing to do in this phase is to manipulate the real DOM based on the effecttags.

In order to avoid going through the Fiber tree again to find the Fiber with the effectTag property, a one-way linked list of Fiber nodes that need to be updated is saved during the construction of the Fiber tree in the previous step. We store the head node of the list in the firstEffect property of the Fiber root node. We also store the props to be updated in the updateQueue property of these Fiber nodes.

In addition to updating the real DOM, the commit update phase also requires calling and handling lifecycle methods and performing Hooks operations at specific stages, which I won’t cover in this article.

Here is a reference to pomb.us/build-your-… To understand what happens after the setState method is executed:

function useState(initial) {
  // Create a new Hook and update the index. // Create a new Hook and update the index
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex];
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [].// Each time setState is executed, the action is added to the queue and executed on the next render
  };

  // On the next render, get the execution queue and execute it step by step to keep the state up to date
  const actions = oldHook ? oldHook.queue : [];
  actions.forEach((action) = > {
    hook.state = action(hook.state);
  });

  // The setState method: adds the action to the execution queue and triggers the render. The action is executed on the next render
  const setState = (action) = > {
    hook.queue.push(action);
    // Render should be retriggered after setState is executed
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    };
    nextUnitOfWork = wipRoot;
    deletions = [];
  };

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

Suggestions on Learning methods

This paper is a phased summary of my recent research on React principle. Overall, the core principles of React aren’t that difficult, but how you learn it is important. Personally, I don’t think it is necessary to learn from the source code, because the actual implementation requires too much detail and engineering considerations, and it is difficult to reverse the principles and implementation rationale from the code level.

In my learning process, the first important material was Build Your Own React, which implemented a minimum usable React based on the most basic principles and methods. I also recommend that learners start with the site and implement it step by step. It took me about four hours to complete the study, and then some time to test and Debug. The whole process is step-by-step, so I suggest that I set aside a day on the weekend to focus on it. See Github: DIDACT for the final implementation code.

The second important material is an online e-book that reveals the React technology. It uses the most basic methods to realize the functions of React in build-your-own-react. This e-book explains the real implementation method of React from the perspective of source code. With the React process in mind, learning this ebook is quite natural. The most valuable thing about it is that there are many places to directly link to the React source code, so it is also a good way to study the source code.