Indepth. dev/inside-fibe… indepth.dev/inside-fibe

This article gives readers an in-depth look at Fiber, a new feature of React. Let’s learn two key stages of the new harmonic algorithm. We’ll take a closer look at how React updates state, props, and handles children.

preface

React is a Javascript library for building UI interfaces. Its core mechanism is to track changes in component state and map the updated state to the UI. In React we call this process reconciliation. When we call the setState method, React checks whether the state or props has changed and decides whether to re-render the component.

The React documentation provides a great overview of the coordinating algorithm mechanisms for the React element, the lifecycle and render methods, and the diffing algorithm applied to component subclasses. The immutable React element returned from the Render method is what is commonly referred to as the “virtual DOM”. The term “virtual DOM” helped people understand React in the early days, but it also caused some confusion, so it’s no longer used in the Official React documentation. So in this article, I’ll call it the React element tree.

In addition to the React element tree, React also has a tree representing internal instances (components, DOM nodes, and so on) that are used to maintain state. Starting with version 16.0 React introduced a new set of methods and algorithms for implementing this internal state tree (called Fiber). If you want to see the advantages of introducing Fiber, read how and why React uses linked lists in Fiber.

React Internals This is the first article to teach you how React works. In this article, I’ll take you through some of the important concepts and data structures associated with the new coordination algorithm. Once we have enough background, we will explore algorithms and the main methods used to traverse and process fiber trees. Later articles in this series will show how React uses algorithms to implement initial rendering and handle updates to state and props. Later, we’ll learn about scheduler, the child element coordination process, and the process of building an Effect List.

I’m going to give you some cutting-edge knowledge. I encourage you to read and understand the rationale behind React as we currently use it, and feel the wonder of it. This series of articles is a great guide if you’re considering contributing to the React source code. I’m a big believer in reverse engineering, so this article has many links to the source code for Act16.6.0.

No doubt there is a lot to cover in this article. So even if you don’t understand it right away, don’t feel pressured. It will all be worth the extra time. Note that you don’t have to have used React; this article is purely about the internal implementation of Reac.

Basic knowledge of

For the rest of the presentation, the following demo will be used: a number accumulator, click the button, and the array increases by 1.

Here is the code implementation:

class ClickCounter extends React.Component {
    constructor(props) {
    super(props);
    this.state = {count: 0};
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick() {
      this.setState((state) = > {
          return {count: state.count + 1};
      });
  }

  render() {
      return [
          <button key="1" onClick={this.handleClick}>Update counter</button>.<span key="2">{this.state.count}</span>]}}Copy the code

For a simple example, the Render function of the ClickCounter component returns two elements, Button and SPAN. As soon as we click on the button, the component state is updated so that the number of span elements on the page accumulates.

React performs a series of activities during the mediation process. Here’s a list of react’s higher-order actions after the demo’s first render and state update. – Update ClickCounter component state count – retrieve and compare ClickCounter subclasses and subclass properties – Update span element properties there are other activities that are also performed during mediation such as calling time period methods or updating refs. All of these activities are unified in Fiber under the name Work. The type of work usually varies depending on the React element type. For example, React requires the creation of an instance for a class component, while function components do not. As you know, in React we have many types of elements: Class components, function components, host components (DOM nodes), Portals, and so on. The React element type is defined by the first argument passed to the createElement function. The createElement function is usually used to create an element in the Render method.

Before we start explaining these activities and exploring fiber’s core algorithm, let’s familiarize ourselves with the data types used in the React source code.

React element to Fiber node

Each component in React corresponds to a UI rendering, called a View or template, which is returned from the Render method. Here is a template for our ClickCounter component.

<button key="1" onClick={this.onClick}>Update counter</button>
<span key="2">{this.state.count}</span>
Copy the code

The React elements

When a template is compiled by the JSX compiler, we get a set of React elements. The React component’s Render method actually returns the React elements instead of the HTML. We don’t have to write JSX, so the Render method of the ClickCounter component could be written as:

class ClickCounter {...render() {
        return [
            React.createElement(
                'button',
                {
                    key: '1'.onClick: this.onClick
                },
                'Update counter'
            ),
            React.createElement(
                'span',
                {
                    key: '2'
                },
                this.state.count
            )
        ]
    }
}
Copy the code

The render method calls the React. CreateElement method, and we get the following data structure:

[{$$typeof: Symbol(react.element),
        type: 'button'.key: "1".props: {
            children: 'Update counter'.onClick: () = >{... }}}, {$$typeof: Symbol(react.element),
        type: 'span'.key: "2".props: {
            children: 0}}]Copy the code

React uses the $$Typeof attribute to uniquely identify React elements. Type, key, and props are used to describe this element. These values are passed to the react. creaateElement argument. Notice that React represents text as children, and that the onClick event is also one of the props of the button element. There are other attributes, such as ref, that are beyond the scope of this article.

The React element of ClickCounter does not have any props or keys:

{
    $$typeof: Symbol(react.element),
    key: null.props: {},
    ref: null.type: ClickCounter
}
Copy the code

Fiber node

During the Reconciliation process, each React element returned by the Render method is merged into the Fiber node tree. Each React element corresponds to a fiber node. Unlike react elements, fiber nodes are not recreated every time they are rendered. Fiber is an immutable data structure that contains the state and DOM of a component.

As we mentioned above, Fiber performs different activities depending on the React element type. For example, ClickCounter calls the declare cycle function and the render function, whereas the span host component (DOM) performs a DOM change. So, in short, each React element is converted to a fiber node of the corresponding type. Different types mean different operations need to be performed.

If you think of fiber as a data structure, it stands for work to be done, i.e., a unit of work. In addition, Fiber provides an easy way to track, schedule, pause, and terminate work.

The first time a React element is converted to a fiber node, the React to the element’s data into to the createFiberFromTypeAndProps function to generate a fiber. In the final update, React will reuse fiber nodes and update only the properties that need to be updated based on the react element data. Depending on the key property, React may remove the node from the hierarchy if the Render function no longer returns the React element.

Look at the ChildReconciler method, which lists React operations on existing Fiber nodes and the corresponding methods.

React creates a Fiber node for each React element, so we have a fiber node tree corresponding to the React node tree. In our demo, the ClickCounter fiber node looks like this:

All fiber nodes are linked by a linked list with child, sibiling, and return attributes. For more details, check out this article: The How and Why on React’s Usage of Linked List in Fiber

Current tree and workInProgress tree

After the first render, React gets a Fiber tree that reflects the state of the application to render the UI. This tree is commonly called Current. When React renders, a workInProgress tree is created. This fiber tree reflects the future STATE of the UI.

According to all fiber nodes in the workInProgress tree, all work will be performed. Of course, when React traverses the Current tree, it creates a corresponding node for each existing Fiber node to form the workInProgress tree. This node is created from the React element returned in the Render method. Once the update is executed and all the associated work is done, React will have an alternate tree ready to render the UI to the screen. Once the workInProgress tree is rendered on the screen, it becomes the Current tree.

Consistency is one of the core principles of React. React always updates the DOM once — it doesn’t show partial results. The workInProgress tree is like a draft — it’s not visible to the user, so React processes all the components before rendering the final result to the screen.

In the source code, you’ll see many ways to get fiber nodes from both the Current tree and the workInProgress tree. This is the method signature of one of these functions.

function updateHostComponent(current, workInProgress, renderExpirationTime) {... }Copy the code

Each fiber node’s alternate domain holds an index pointing to the corresponding alternate node in the other tree. Nodes in the current tree can point to the workInProgress tree and vice versa.

Side-effects

We can think of the React component as a function that computes UI rendering using state and props. Every action such as changing the DOM or invoking the lifecycle should be considered a side-effect. Or, to put it simply, an effect. Effects is described in the React documentation as follows:

You’ve probably used the React component to fetch data, subscribe, or manually alter the DOM. We call these operations side-effects, which means side effects, or effects for short. Because they can affect other components and cannot end up in the rendering.

You can see how the updates to the state and props cause side-effects. And since applying Effects is a class of work, a fiber node is a convenient mechanism for tracking effects in addition to updates. Each fiber node can have an effects corresponding to it. They are encoded in the effectTag field. So The Effects in Fiber basically defines the work that needs to be done on the instance after the update. For host components (DOM elements), work includes adding, updating, and removing elements. For class components, React may need to update the Refs and call the componentDidMount and componentDidUpdate lifecycle methods. There are other effects that correspond to different types of fiber nodes.

Effects list

React processes updates very quickly and employs some interesting tricks for doing so. One of the tricks is to create a linear fiber node list with effects for fast traversal. Traversing a linear list is much faster than traversing a tree. And there is no need to spend time on nodes without side-effect.

The purpose of this list is to mark nodes that have DOM updates or other effects. This list is a subset of the finishedWork Tree and is represented by the nextEffect property. Instead of using the Child attribute as in the Current and workInProgress trees. Dan Abramov gives an analogy to the Effects List. He thinks of the Effects List as a Christmas tree 🎄, which binds all the influential nodes together using lights from a book. To visualize this metaphor, imagine a tree full of fiber nodes. The highlighted nodes represent some work that needs to be done. For example, our update results in C2 about to be inserted into the DOM, D2 and C1 about to change properties, and B2 about to call a lifecycle method. The effect list will link them together so React can skip other nodes later:

You can see how the nodes with effects are connected together. When traversing a node, React uses the firstEffect pointer to mark where the list starts. So the figure above can be simplified as follows:

Root of the Fiber tree

Each React application has one or more DOM elements that act as containers. In our case, the div element with the ID container is our Container.

const domContainer = document.querySelector('#container');
ReactDOM.render(React.createElement(ClickCounter), domContainer);
Copy the code

React creates a Fiber root object for each container. You can access it as follows:

const fiberRoot = query('#container')._reactRootContainer._internalRoot
Copy the code

The Fiber root is where React saves references to the Fiber tree. This reference is stored in the Current property of fiber root.

const hostRootFiberNode = fiberRoot.current
Copy the code

The root fiber node of the Fiber tree has a very special type called HostRoot. It is created internally and acts as the parent of your highest-level component. FiberRoot can be returned via the stateNode property of the HostRoot Fiber node

fiberRoot.current.stateNode === fiberRoot; // true
Copy the code

You can explore the Fiber tree by accessing the HostRootfiber node at the highest level of fiber root. Alternatively, you can get a separate Fiber node from the component instance as follows:

compInstance._reactInternalFiber
Copy the code

Fiber node Structure

Let’s now look at the ClickCounter component’s Fiber node structure:

{
    stateNode: new ClickCounter,
    type: ClickCounter,
    alternate: null.key: null.updateQueue: null.memoizedState: {count: 0},
    pendingProps: {},
    memoizedProps: {},
    tag: 1.effectTag: 0.nextEffect: null
}
Copy the code

There is also the fiber node structure of the SPAN DOM element:

{
    stateNode: new HTMLSpanElement,
    type: "span".alternate: null.key: "2".updateQueue: null.memoizedState: null.pendingProps: {children: 0},
    memoizedProps: {children: 0},
    tag: 5.effectTag: 0.nextEffect: null
}
Copy the code

There are many properties on the Fiber node. The purposes of alternate, effectTag, and nextEffect have been discussed previously. Now let’s see what the other properties do.

stateNode

The stateNode property holds references to component class instances, or DOM nodes, or other React elements of fiber nodes. In general, this property is used to store the local state of a fiber node.

type

Type defines the Fiber node as a function component or class component. For class components, Type points to the constructor. For the DOM element, it describes the HTMLtag. I usually use this attribute to learn what element is associated with the Fiber node.

tag

The tag defines the type of fiber. Tag is used in the coordination algorithm to decide what type of work to do. As mentioned earlier, work is different for different types of React elements. CreateFiberFromTypeAndProps method convert a elements React to the corresponding type of fiber node. In our case, ClickCounter has a tag attribute of 1, which means ClassComponent. For the SPAN element, its tag is 5, representing the HostComponent.

updateQueue

A queue with status updates, callback functions, and DOM updates.

memoizedState

MemoizedState is the state used to create the output fiber. When processing updates, it reflects the state currently rendered on the screen.

pendingProps

PendingProps is the props of Fiber to create output based on the previous render.

key

The key attribute is a unique tag with a set of child elements that helps React figure out which elements have changed and are added to or removed from a list. This involves the “Lists and keys” function of React.

You can see a complete fiber node structure here. I left out a bunch of attributes in this explanation. In particular, I skipped the Pointers to child, Sibling, and return that form the data structure of a tree. For more on this section, check out the previous article. A class of attributes such as expirationTime, childExpirationTime, and Mode are specifically for Scheduler.

General algorithm

React performs work in two main phases: the Render phase and the COMMIT phase.

During the first render period, React is based on setState and React. Render to perform component updates and figure out which updates need to be applied to the UI. If this is the first render, React creates a new Fiber node for each element returned by the Render method. In future updates, the existing React element fiber node will be reused and updated. At the end of this period, you get a Fiber node marked side-Effects. These effects describe the work that needs to be done in the next commit phase. In the Render phase, React gets a Fiber tree labeled with effects and applies those effects to the instance. During this period, the Effect list is traversed and DOM updates and other user-visible changes are performed.

It is important to know that work can be executed asynchronously during the first render period. React can process one or more Fiber nodes, depending on how much time is available, and then temporarily put work into something else. React then comes back to its unfinished work. Sometimes React needs to drop some work and start from scratch. The work can be suspended because the work done at this stage does not cause visible changes to the user. In contrast, the commit phase, which I’ll cover next, is always synchronous. This is because the work done in this phase produces updates visible to the user. For example, DOME update. That’s why React puts the work in a separate stage.

Calling the declaration cycle function is one of the works React performs. Some methods are called in the Render phase, and others are called in the Commit phase. Here are some declaration cycle methods that are called in the Render phase:

  • [UNSAFE_]componentWillMount (deprecated)
  • [UNSAFE_]componentWillReceiveProps (deprecated)
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • [UNSAFE_]componentWillUpdate (deprecated)
  • render

As you know, some of the older lifecycle methods that need to be executed during the Render phase have already been marked UNSAFE (since React16.3). These methods are documented as legacy lifecycles. They will be deprecated in the 16.x version, and life-cycles without the UNSAFE prefix will be removed from the 17.0 version. You can learn more about recommended migration methods here.

Are you curious about the reason for the removal?

This is because the Render phase does not generate side-effects. React for asynchronous update processing of components, such as DOM updates. (Maybe even multithreading). The lifecycle, however, being marked UNSAFE is often misunderstood and misused. Developers tend to do side-effects in these lifecycle functions. This approach can cause problems with the new asynchronous rendering methods. Although only lifecycle methods not marked with the UNSAFE prefix will be removed, even with the UNSAFE flag, they will cause problems in the upcoming Concurrent Mode (Concurrent Mode, which you may choose not to use).

These are the list of lifecycle methods that are executed during the COMMIT phase:

  • getSnapshotBeforeUpdate
  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

Because these methods are executed during the commit phase of synchronization, they may contain some side-effects and manipulate the DOM. Ok, so we now have enough background to take a look at a generalized algorithm. See how it traverses the tree and performs work. Let’s find out.

Render Phase

The coordination algorithm starts at the highest level HostRoot Fiber node by calling renderRoot method. React, however, skips fiber nodes that have already been processed and finds fiber nodes with unfinished work. For example, if you call setState deep in the component tree, React will start at the root node but it will quickly skip the parent and go straight to the component that called setState.

Main Steps of the Work Loop

All fiber nodes are processed in the Work loop. Here is the sync part of the loop:

function workLoop(isYieldy) {
  if(! isYieldy) {while(nextUnitOfWork ! = =null) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); }}else {...}
}
Copy the code

In the code above, nextUnitOfWork saves a reference to the Fiber node in the workInProgress tree that has work to complete. When React traverses the fiber node in the tree, it will use this variable to record the fiber node that has not completed its work. When the current node is processed, this variable either contains a reference to the next fiber node or points to NULL. If null, React exits the work loop and prepares to commit all changes.

There are four main ways to traverse the tree, initialize, or terminate work:

  • performUnitOfWork
  • beginWork
  • completeUnitOfWork
  • completeWork

We can demonstrate how these methods are applied by watching the following animation of traversing the Fiber tree. For these methods, I used a simplified implementation to show our demo. Each method takes a fiber node as an input parameter and processes it. And as React moves down the tree, you can see the activated Fiber nodes change. And you can see exactly how the algorithm goes from one branch to the other. He will first complete the work of the child node and then move to the parent. (Depth-first DFS traversal)

Note that straight links represent siblings and curved links represent children. For example, B1 has no child node, and B2 has only one child node, C1.

In this video, you can pause to revisit and observe the status of the current node and method. Abstractly, you can think of begin as walking into a component and complete as walking out of it. Here are detailed examples and code implementations. Let’s start with the formUnitofWork and beginWork methods:

function performUnitOfWork(workInProgress) {
    let next = beginWork(workInProgress);
    if (next === null) {
        next = completeUnitOfWork(workInProgress);
    }
    return next;
}

function beginWork(workInProgress) {
    console.log('work performed for ' + workInProgress.name);
    return workInProgress.child;
}
Copy the code

The performUnitOfWork method takes a Fiber node of the workInProgress tree and begins by calling the beginWork function. The beginWork function will complete all the work needed to change the fiber node. To simplify the demonstration, we’ll just use console.log here. The beginWork function always returns a pointer to the next child or null.

If there is a next child, that child is assigned to the nextUnitOfWork variable in the workLoop method. However, if there are no children, React knows it’s traversing the end of the limb so it will terminate the current node.

function completeUnitOfWork(workInProgress) {
    while (true) {
        let returnFiber = workInProgress.return;
        let siblingFiber = workInProgress.sibling;

        nextUnitOfWork = completeWork(workInProgress);

        if(siblingFiber ! = =null) {
            // If there is a sibling, return it
            // to perform work for this sibling
            return siblingFiber;
        } else if(returnFiber ! = =null) {
            // If there's no more work in this returnFiber,
            // continue the loop to complete the parent.
            workInProgress = returnFiber;
            continue;
        } else {
            // We've reached the root.
            return null; }}}function completeWork(workInProgress) {
    console.log('work completed for ' + workInProgress.name);
    return null;
}
Copy the code

You can see at the heart of this method is a big while loop. React enters this method when the workInProgress fiber node has no children. When the current Fiber node completes work, it checks to see if any sibling nodes exist. If there is, React exits the method and returns a pointer to the sibling node. The sibling node will be assigned to the nextUnitOfWork variable, and React will start branching the node that started with the sibling. It is important to understand that at this point, the previous sibling node only has completed work, and the parent node has not completed work. Only when all of the child nodes on the branches/branches are complete do you start backtracking to the parent node to complete the work.

As you can see from the code implementation, completeUnitOfWork is used a lot in iterations. However, the main activity occurs in the beginWork and completeWork methods. In the next article, we’ll learn what the beginWork and completeWork of React do on the ClickCounter component and the SPAN node.

Commit Phase

This stage is started by the completeRoot method. In the COMMIT phase, you React to update the DOM and call the before and after change lifecycle methods.

When React enters this phase, there will be 2 trees and an Effects list. The first tree represents the current render state on the screen. There is also an alternate tree built during the Render phase. The second tree, called finishedWork or workInProgress in the source code, represents the future state that will be reflected on the screen. The alternate tree is connected to the first tree via child and Sibling Pointers.

After that, there is an Effects List, which is a subset of the nodes in the finishedWork tree. Each node on the Effects List is linked by the nextEffect pointer. As we said earlier, Effects List is a product of the Render phase. The main goal of the Render phase is to determine which nodes need to be inserted, updated, or removed. And which component needs to call its lifecycle methods. That’s what the Effects list tells React. All the nodes in the Effects list are the nodes that need to be traversed in the COMMIT phase.

For debugging, the current tree can be accessed via the current property of fiber root. We can access the finishedWork tree by accessing the alternate property of HostFiber node of current Tree.

The main method of running in the COMMIT phase is commitRoot. It mainly does the following:

  • In aSnapshot effectTag the node invocationgetSnapshotBeforeUpdateLifecycle approach
  • In aDeletion effectTag the node invocationcomponentWillUnmountLifecycle approach
  • Complete all DOM inserts, updates, and deletions
  • thefinishedWorkUpdated tocurrent
  • In aPlacement effectTag the node invocationcomponentDidMountLifecycle approach
  • In aUpdate effectTag the node invocationcomponentDidUpdateLifecycle approach

After calling the pre-mutation method getSnapshotBeforeUpdate, React commits all side-effects in the tree. This operation has two phases. The first phase completes all DOM (host) inserts, updates, deletes, and unmounts the ref. React then assigns the finishedWork tree to FiberRoot. Mark the workInProgress tree as current Tree. This sequence of operations occurs during the first cycle of the Commit phase. All previous trees are current trees in the componentWillUnmount phase, and finishedWork trees become Current trees in the componentDidMount/Update phase. In the second phase React calls all lifecycle methods and ref callbacks. These methods are isolated and executed so that all substitutions, updates, and deletions are performed on the entire tree.

Here are the methods that do all the things we talked about in the previous paragraph:

function commitRoot(root, finishedWork) {
    commitBeforeMutationLifecycles()
    commitAllHostEffects();
    root.current = finishedWork;
    commitAllLifeCycles();
}
Copy the code

Each of its children iterates through the nodes of the Effects-list, checking for the effects type. The corresponding method then executes the node marked with the corresponding effect.

Pre-mutation Lifecycle Methods

As you can see from the following example, the code traverses the Effects tree and checks for a Snapshot effect on each node:

function commitBeforeMutationLifecycles() {
    while(nextEffect ! = =null) {
        const effectTag = nextEffect.effectTag;
        if (effectTag & Snapshot) {
            constcurrent = nextEffect.alternate; commitBeforeMutationLifeCycles(current, nextEffect); } nextEffect = nextEffect.nextEffect; }}Copy the code

For a class component, having a Snapshot effect means that the getSnapshotBeforeUpdate lifecycle method needs to be called.

DOM updates

CommitAllHostEffects is the way React performs DOM updates. This method basically defines the type of operation to be performed on a Node and performs that type of operation.

function commitAllHostEffects() {
    switch (primaryEffectTag) {
        casePlacement: { commitPlacement(nextEffect); . }casePlacementAndUpdate: { commitPlacement(nextEffect); commitWork(current, nextEffect); . }caseUpdate: { commitWork(current, nextEffect); . }caseDeletion: { commitDeletion(nextEffect); . }}}Copy the code

Interestingly, in the commitDeletion method, React calls the componentWillUnmount method as part of the delete process.

Post-mutation Lifecycle Methods

React calls all remaining life cycle methods in the commitAllLifecycles method: componentDidUpdate and componentDidMount.

At last. If you have any thoughts or questions about this article, feel free to leave them in the comments section. Welcome to the next article in this series: an in-depth look at the updates to state and Props in React. I’ve also written many other articles that delve into how scheduler, subclass reconciliation, and effect lists are built. I’m also planning a video based on this article showing how to debug applications.