Principle of Fiber

What was the problem with React before Fiber appeared

Prior to React 16, the process of updating VirtualDOM was implemented using the Stack architecture, which is circular recursion. The problem with this comparison method is that once the task starts, it cannot be interrupted. If there are a large number of components in the application and the VirtualDOM level is relatively deep, the main thread will be occupied for a long time. The main thread can only be released after the whole VirtualDOM tree comparison and update is completed, and then the main thread can perform other tasks. This will lead to some user interaction, animation and other tasks can not be performed immediately, the page will have a lag, very impact on the user experience. Core problems: recursion cannot be interrupted; task execution takes a long time; JavaScript is single-threaded and mutually exclusive with Native GUI; other tasks cannot be performed in the process of comparing VirtualDOM, resulting in task delay, page lag, and poor user experience.

Simple implementation of the Stack architecture

Let’s implement a simple process of getting JSX, converting JSX into A DOM, and then adding it to the page

const jsx = (
  <div id="a1">
    <div id="b1">
      <div id="c1"></div>
      <div id="c2"></div>
    </div>
    <div id="b2"></div>
  </div>
)

function render(vdom, container) {
  // Create the element
  const element = document.createElement(vdom.type)
  // Add attributes to the element
  Object.keys(vdom.props)
    .filter(propName= >propName ! = ="children") // Filter the children attribute
    .forEach(propName= > (element[propName] = vdom.props[propName]))
  // Create child elements recursively
  if (Array.isArray(vdom.props.children)) {
    vdom.props.children.forEach(child= > render(child, element))
  }
  // Add the element to the page
  container.appendChild(element)
}

render(jsx, document.getElementById("root"))
Copy the code

As you can see, the JSX code is translated into real DOM and added to the page

How does Fiber solve the performance problem

  1. React abandons recursive calls in the Fiber architecture and uses loops to simulate recursion because loops can be broken at any time.
  2. Fiber breaks large render tasks down into smaller tasks
  3. React uses requestIdleCallback to use idle browser time to perform small tasks. After React executes a task unit, it checks to see if there are other high priority tasks. If so, it gives up the occupied thread and executes the high priority task first.

requestIdleCallback

Let’s start with requestIdleCallback’s explanation on MDN

Window. RequestIdleCallback () method will be called function in browser free time line. This enables developers to perform background and low-priority work on the main event loop without affecting the delay of critical events such as animations and input responses. Functions are typically executed in first-in-first-called order; however, if the callback function specifies a timeout for execution, it is possible to disrupt the execution order in order to execute the function before the timeout.

The grammar of the requestIdleCallback

RequestIdleCallback takes two arguments, a callback function named IdleDeadline and an optional argument

The IdleDeadline argument has a timeRemaining() method that returns a time DOMHighResTimeStamp, a value of floating point type, which represents the estimated number of milliseconds left in the current idle period. If the idle period has ended, it has a value of 0. Your callback function (passed to requestIdleCallback) can repeatedly access this property to determine whether the idle time of the current thread can be used to perform more tasks before it ends.

The role of requestIdleCallback

Browser pages are drawn frame by frame by engine, and when the number of frames per second reaches 60, the page is smooth. As anyone who has played an FPS knows, when this number is less than 60, the human eye can detect a lag. 60 frames per second, and the time allotted to each frame is 1000/60 ≈ 16 ms. If the execution time of each frame is less than 16 ms, it means that the browser has free time. Can you use the free time of the browser to process tasks, so that the main task is not completed all the time? RequestIdleCallback is all about taking advantage of free time in the browser to perform tasks.

Above said a bunch of, some people may have meng, you don’t nonsense, directly on the code to give me an example on the line. Let’s see what’s so amazing about requestIdleCallback

<style>
  #box {
    padding: 20px;
    background: palegoldenrod;
  }
</style>

<! -- body -->
<div id="box"></div>
  <button id="btn1">Perform a computing task</button>
  <button id="btn2">Changing the background color</button>
<script>
  const box = document.getElementById('box')
  const btn1 = document.getElementById('btn1')
  const btn2 = document.getElementById('btn2')

  let number = 999999
  let value = 0

  function calc() {
    while (number > 0) {
      value = Math.random() < 0.5 ? Math.random() : Math.random()
      console.log(value)
      number--
    }
  }

  btn1.onclick = function () {
    calc()
  }

  btn2.onclick = function () {
    box.style.background = 'green'
  }
</script>
Copy the code

In the above code, we create a random number through a long loop to increase the amount of computation in the browser. You can try this demo with your local IDE, and you will find that when you click on the execute calculation task and then click on the Change background color button, the color of the box will not change immediately. Instead, it waits a few seconds (or even slower if the computer is poor) for the change to occur. Because the rendering of the Native GUI and v8 engine is mutually exclusive, there is some delay in rendering the page.

<style>
  #box {
    padding: 20px;
    background: palegoldenrod;
  }
</style>
<! -- body -->
<div id="box"></div>
<button id="btn1">Perform a computing task</button>
<button id="btn2">Changing the background color</button>
<script>
  const box = document.getElementById('box')
  const btn1 = document.getElementById('btn1')
  const btn2 = document.getElementById('btn2')

  let number = 999999
  let value = 0

  function calc(deadline) {
    while (number > 0 && deadline.timeRemaining() > 0) {
      value = Math.random() < 0.5 ? Math.random() : Math.random()
      console.log(value)
      number--
    }
    requestIdleCallback(calc)
  }

  btn1.onclick = function () {
    requestIdleCallback(calc)
  }

  btn2.onclick = function () {
    box.style.background = 'green'
  }
</script>
Copy the code

The above code is optimized using requestIdleCallback. After running, click on the change background color button and you can immediately see the color change. This is what requestIdleCallback does.

Fiber principle analysis

Let’s see how Fiber works by implementing a simple version of Fiber

What is the Fiber

For all that chatter, what exactly is Fiber? Fiber is an execution unit of React. After React 16, React split the entire rendering task into small tasks for processing, and each small task refers to the construction of Fiber nodes. The split tasks are executed during free time in the browser. After each unit is completed, React checks to see if there is time left and, if so, switches control of the main thread.

Fiber is a data structure that supports the operation of the Fiber build task. Once a Fiber mission is complete, how do you find the next Fiber mission to execute? React uses a linked list structure to find the next task unit to execute. Fiber is essentially a JavaScript object that has a child property for the node’s child, a sibling property for the node’s next sibling, and a return property for the node’s parent.

// Simple version of the Fiber object
type Fiber = {
  // Div, span, component constructor
  type: any,
  / / the DOM object
  stateNode: any,  
  // Point to its parent Fiber object
  return: Fiber | null.// Point to its first child, the Fiber object
  child: Fiber | null.// Point to the next sibling iber object
  sibling: Fiber | null,}Copy the code

Implement a simple version of Fiber

Fiber works in two phases: The Render phase and the COMMIT phase.

  • Render phase: Build the Fiber object, build the list, mark the DOM operations to be performed in the list, interrupt.
  • Commit phase: DOM operations are performed according to the built linked list, which cannot be interrupted.
const jsx = (
  <div id="a1">
    <div id="b1">
      <div id="c1"></div>
      <div id="c2"></div>
    </div>
    <div id="b2"></div>
  </div>
)

const container = document.getElementById("root")

// Build the Fiber object for the root element
const workInProgressRoot = {
  stateNode: container,
  props: {
    children: [jsx]
  }
}

// The next task to perform
let nextUnitOfWork = workInProgressRoot

function workLoop(deadline) {
  // 1. Do you have any spare time
  // 2. Whether there is a task to perform
  while (nextUnitOfWork && deadline.timeRemaining() > 0) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
  }
  // Indicates that all tasks have been executed
  if(! nextUnitOfWork) {// Enter the second phase of DOM execution
    commitRoot()
  }
}

function performUnitOfWork(workInProgressFiber) {
  // 1. Create a DOM object and store it in the stateNode property
  // 2. Build a subfiber of the current Fiber
  // The process of going down
  beginWork(workInProgressFiber)
  // If the current Fiber has children
  if (workInProgressFiber.child) {
    // Return child builds child's child
    return workInProgressFiber.child
  }

  while (workInProgressFiber) {
    // Go up and build the list
    completeUnitOfWork(workInProgressFiber)

    // If there is a sibling
    if (workInProgressFiber.sibling) {
      // Return children of the sibling build sibling
      return workInProgressFiber.sibling
    }
    // Update the parent
    workInProgressFiber = workInProgressFiber.return
  }
}

// Build a subset
function beginWork(workInProgressFiber) {
  // 1. Create a DOM object and store it in the stateNode property
  if(! workInProgressFiber.stateNode) {/ / create the DOM
    workInProgressFiber.stateNode = document.createElement(
      workInProgressFiber.type
    )
    // Add properties to the DOM
    for (let attr in workInProgressFiber.props) {
      if(attr ! = ="children") {
        workInProgressFiber.stateNode[attr] = workInProgressFiber.props[attr]
      }
    }
  }
  // 2. Build a subfiber of the current Fiber
  if (Array.isArray(workInProgressFiber.props.children)) {
    let previousFiber = null
    workInProgressFiber.props.children.forEach((child, index) = > {
      let childFiber = {
        type: child.type,
        props: child.props,
        effectTag: "PLACEMENT".return: workInProgressFiber
      }
      if (index === 0) {
        // Create a subset where only the first child is a subset
        workInProgressFiber.child = childFiber
      } else {
        // If it is not the first, build the sibling of the subset
        previousFiber.sibling = childFiber
      }
      previousFiber = childFiber
    })
  }
  // console.log(workInProgressFiber)
}

function completeUnitOfWork(workInProgressFiber) {
  // Get the parent of the current Fiber
  const returnFiber = workInProgressFiber.return
  // Whether the parent exists
  if (returnFiber) {
    // The Fiber that needs to perform DOM operations
    if (workInProgressFiber.effectTag) {
      if(! returnFiber.lastEffect) { returnFiber.lastEffect = workInProgressFiber.lastEffect }if(! returnFiber.firstEffect) { returnFiber.firstEffect = workInProgressFiber.firstEffect }if (returnFiber.lastEffect) {
        returnFiber.lastEffect.nextEffect = workInProgressFiber
      } else {
        returnFiber.firstEffect = workInProgressFiber
      }
      returnFiber.lastEffect = workInProgressFiber
    }
  }
}

function commitRoot() {
  let currentFiber = workInProgressRoot.firstEffect
  while (currentFiber) {
    currentFiber.return.stateNode.appendChild(currentFiber.stateNode)
    currentFiber = currentFiber.nextEffect
  }
}

// Perform the task while the browser is idle
requestIdleCallback(workLoop)
Copy the code

Build the Fiber list

The above function completeUnitOfWork is used to build the Fiber list, and will only be rendered in the list

  1. What is the building order of a linked list?

The order of the list is determined by the order of DOM operations, c1 is the first to perform the DOM operation so it’s the beginning of the chain, and A1 is the last element to be added to Root, so it’s the end of the chain.

  1. How do I add new elements to the end of the chain?

Stores the next item in the chain through nextEffect in the list structure.

During the process of building the list, you need to store the most recent items in the list through a variable that is used each time a new item is added and updated after each operation is complete. This variable is called lastEffect in the source code.

LastEffect is stored on the parent of the current Fiber object. When the parent changes, lastEffect moves up first and then appends nextEffect in order to avoid any confusion in the link order

  1. Where to store the list?

The list needs to be stored in Root because when it goes to phase 2, the commitRoot method commits Root to phase 2. In the source code, there is a property called firstEffect under Root Fiber for storing lists. In the process of building the list, how can WE store C1 in Root when C1 starts and Root ends? It’s actually done by moving the end of the chain up.

conclusion

In fact, the overall idea of Fiber is to use loops and lists instead of recursion to optimize performance. Fiber architecture has two stages: Render phase is responsible for building Fiber objects and lists, while COMMIT phase is responsible for building DOM. Just for the sake of understanding, here’s the full implementation of Fiber github.com/maoxiaoxing…