The React concept

We believe React is the preferred way to build large, responsive Web applications in JavaScript. It does well on Facebook and Instagram.

React is a JS library that builds user pages. Recent major updates to React focus on the goal of responding quickly and optimising the user experience. For example, we render 4500 SPAN tags.

class App extends React.Component {
  state = { number: 0 }
  render(){
    const { number } = this.state;
    return  (
      <div>
        {
          Array.from(new Array(4500)).map((item, index) => {
            return <span id={index}>{number}</span>})}</div>)}}Copy the code

(Old version renders 4500 spans)

(New version renders 4500 spans)

You can see that with the older version of React it took 140ms of JS execution to render 4500 spans. And we all know that JS is a single thread, the implementation of a large number of operations occupy JS at the same time, but also will inevitably cause the page lag. This violates the idea of a fast React.

React renders a large number of span tags, breaking up a large rendering task into smaller tasks (around 5ms). React executes a task in each frame (16ms) of the browser, leaving the rest of the time to complete the rest of the page. This solves the browser page lag problem.

How does React implement this function? I think the main points are as follows:

  • Replace the previous Virtual DOM with a new Fiber structure
  • Asynchronous interruptible updates are implemented based on Fiber architecture and Scheduel
  • React’s internal priority and Lane mechanism successfully completes various tasks in React.

This is my first post after learning the React source code. I will introduce you to the specific meaning of the above points in as easy to understand examples as possible. The source code level will be explained in subsequent articles.

The traditional VDOM

A brief introduction to VDOM. Vdom is the abbreviation of Virtual DOM(Virtual DOM). It refers to the DOM structure simulated by JS, and the comparison of DOM changes is done on the JS layer. The advantage of VDOM is that the DOM diff operation can be put into JS memory, and DOM reuse can be achieved. Reduce unnecessary redrawing to improve efficiency.

Let’s briefly consider a diff process for rendering VDOM to DOM.

React continuously diff the old and new VDOM from the root node and continue to recursively diff the child nodes if the nodes are the same. If not, delete the old DOM from the old DOM and render the new DOM. This allows you to compare the entire DOM tree with only one walk through the tree.

The disadvantage of using the Virtual DOM is that it uses depth-first recursion to compare the DOM to its corresponding child nodes. Once you get into recursion, you can’t pause. If the VDOM tree is very large (like 4500 spans above), the browser thread is blocked by the diff function.

Finally, a brief summary of the VDOM update process used by Act15. The VDOM update process can be understood as follows: DIFF old and new nodes from the root node in a depth-first manner, and update the corresponding DOM as soon as the differences are found.

The new Fiber architecture

There will be 3-5 articles on Fiber architecture.

React uses a new Fiber architecture instead of a Vnode to address the problem of uninterruptible VDOM. A dual cache mechanism is used for asynchronous interruptible updates.

What is the Fiber

Fiber is also a JS implementation of the DOM structure. You can think of it as a new definition of VDOM after React uses Fiber. Fiber is actually a VDOM. For example, a component APP is defined as follows.

function App() {
  return (
    <div>
      i am
      <span>KaSong</span>
    </div>)}Copy the code

In Fiber, the child attribute is used to represent the byte point of the node. Sibling indicates the Sibling node and return indicates the parent node.This structure facilitates the realization of interruptible traversal in Fiber structure.

Update process in Fiber architecture

In contrast to the VDOM update process in Act15, the Fiber architecture divides the entire update process into two phases to solve the problem of the uninterruptible DIFF process.

  1. Render phase: asynchronous interruptible diff The new and old DOM, does not update immediately after finding differences. Instead, click on the Fiber nodetag(Update, Placement, Delete).
  2. Commit phase: Traverses the Fiber with the tag and updates the DOM based on the tag type.

The new architecture has the advantage of separating the diff from the rendering process. In addition, asynchronous interruptibility is realized based on Schedule, which solves the problem that complex operations occupy a large number of JS threads.

Double cache mechanism

The dual buffering mechanism is a way to manage updates in React and an important mechanism to improve user experience. The main idea behind the dual-cache mechanism is that when React starts updating, it stores two JS trees in JS memory. Are current tree and workInProgress tree respectively.

  • Current tree: The DOM tree corresponding to the DOM structure rendered for the current page.
  • WorkInProgress tree: A tree copied from the Current tree. The operation of fiber during diFF is mainly carried out in the workInProgress tree.

At the start of the Commit phase,fiberRootNodenodecurrentThe pointer points to the leftRootThe tree, on the leftRootThe tree iscurrentThe tree.currentalternatePointing to the forworkInProgressThe tree. Indicated as follows

When the COMMIT phase is about to end, React re-renders the DOM through the WorkInProgress tree. The fiberRootNode’s Current node is pointed to the tree to the right, thus switching the Current and workInProgress trees in the dual caching mechanism.

function commitRootImpl(root, renderPriorityLevel) {
    root.current = finishedWork;
} 
Copy the code

At the same time, an important function of the dual caching mechanism is to place DOM computation in the workInProgress tree. The browser’s currently rendered VDOM is saved in the current tree. You can seamlessly switch between old and new DOM.

Asynchronous interruptible implementation based on Schedule

This is just a simple principle analysis of Schedule, and a special article will be output to explain it later.

As you can see from the above, the core of the Fiber structure is the asynchronous interruptibility of the COMMIT phase. So what is asynchronous interruptible? Here is a simple example to explain.

Imagine using JS to calculate the sum from 1 to 3000. However, in JS operation, adding is a very “time consuming” operation, and each operation takes 1ms. As a result, JS process is occupied in the calculation process and cannot respond to other events, resulting in a lag.

const add = (a, b) = > {
    sleep(1);
    return
}

let count = 0;
for(let i = 1; 1< =100; i++){
    count = add(count, i);
}
Copy the code

React implements a Schedule that takes up to 5ms of time in a browser render frame and the rest of the time is used for click-time processing and other operations. React Schedule implements the add function

const syncSleep = (time: number) = >  {
  const start = new Date().valueOf();
  while (new Date().valueOf() - start < time) {}
}



// Simple implementation of add
const add = (a: number, b: number) = > {
    syncSleep(1);
    return a + b;
}



// Implement a summation function that returns the next calculation function when the calculation has not reached the boundary state. Otherwise return NULL
const Accumulate = () = > {
    let count = 0;
    let i = 1;
    let ac = () = > {
        if(i <= 200) {console.log(i)
            count =  add(count, i);
            i++;
            return ac
        }else{
            // The task is complete
            console.log("count:", count)
        }
        return null
    }
    return ac;
}

// Use MessageChannel's onMessage to trigger JS macro tasks
// Why use MessageChannel instead of setTimeout is because of the setTimeout 4ms bug
/ / https://juejin.cn/post/6846687590616137742, and 4 ms in a JS browsers render the frame
// The impact is relatively large

const channel = new MessageChannel();

// The expiration time of the task in the current JS browser render frame
letexpireTime! :number;

// The next task of the summation function
letnextTask! :Function; = Accumulate()

const workLoop = (task: Function) = > {

    let taskForNextTime = task;
    // If the current time is greater than the expiration time, end the while loop
    while(new Date().valueOf() < expireTime && task){
        taskForNextTime = task();
    }
    return taskForNextTime;
}

// handleWorkStart is triggered in a new JS browser render frame
const handleWorkStart = () = > {
    console.log("A new JS browser render frame")
    // Set an expiration time, the expiration time is the current time +5,
    // New Date().valueof () does not perform well, performance should be used instead
    expireTime = new Date().valueOf() + 5;

    // Execute workLoop, nextTask is null if the task is complete, otherwise it is an executable function
    nextTask = workLoop(nextTask);

    // If the task is not completed, use postMessage,
    // handleWorkStart will continue in the next JS browser render frame
    if(nextTask){
        channel.port2.postMessage(null)}}// Trigger handleWorkStart when a postMessage is received
channel.port1.onmessage = handleWorkStart

// Simulation triggers a cumulative task
channel.port2.postMessage(null)
Copy the code

If you replace Accumulate with the React function corresponding to the Fiber node, this becomes a simple implementation of the React Schedule.

Schedule is a relatively independent module in React. React has nothing to do with React. It’s a general design idea. If you encounter large scale operations blocking browser threads with JS, consider the Schedule implementation and tweak it.

React Internal priorities

(React priority comparison flowchart)

In React, such as setState/forceUpdate in ClassComponent. Either useState/useReducer in functionComponent can trigger the component’s render once. React uses the Update data structure to accommodate these situations, and the above methods create an Update object internally.

export type Update<State> = {
  eventTime: number.lane: Lane,
  tag: 0 | 1 | 2 | 3.payload: any.callback: (() = > mixed) | null.next: Update<State> | null};Copy the code

Update stores the priority of the current Update in Lane. Update uses binary to classify priorities into the following 31 categories. From the priority name and the corresponding bit, the more to the right, the higher the priority. High-priority updates interrupt low-priority updates.

export const NoLanes: Lanes = / * * / 0b0000000000000000000000000000000;

export const NoLane: Lane = / * * / 0b0000000000000000000000000000000;

export const SyncLane: Lane = / * * / 0b0000000000000000000000000000001;

export const SyncBatchedLane: Lane = / * * / 0b0000000000000000000000000000010;

export const InputDiscreteHydrationLane: Lane = / * * / 0b0000000000000000000000000000100;

const InputDiscreteLanes: Lanes = / * * / 0b0000000000000000000000000011000;

const InputContinuousHydrationLane: Lane = / * * / 0b0000000000000000000000000100000;

const InputContinuousLanes: Lanes = / * * / 0b0000000000000000000000011000000;

export const DefaultHydrationLane: Lane = / * * / 0b0000000000000000000000100000000;

export const DefaultLanes: Lanes = / * * / 0b0000000000000000000111000000000;

const TransitionHydrationLane: Lane = / * * / 0b0000000000000000001000000000000;

const TransitionLanes: Lanes = / * * / 0b0000000001111111110000000000000;

const RetryLanes: Lanes = / * * / 0b0000011110000000000000000000000;

export const SomeRetryLane: Lanes = / * * / 0b0000010000000000000000000000000;

export const SelectiveHydrationLane: Lane = / * * / 0b0000100000000000000000000000000;

const NonIdleLanes = / * * / 0b0000111111111111111111111111111;

export const IdleHydrationLane: Lane = / * * / 0b0001000000000000000000000000000;

const IdleLanes: Lanes = / * * / 0b0110000000000000000000000000000;

export const OffscreenLane: Lane = / * * / 0b1000000000000000000000000000000;
Copy the code

For example, there is the following demo. SetState ({number: 1}) at componentDidMount 1000ms, and BTN click at 1040 ms.

class App extends React.Component {
  state = { number: 0 }
  btnRef = null;
  handleClick = () = > {
    this.setState((prev) = > ({ number: prev.number + 2}}))componentDidMount(){
    setTimeout(() = > {
      this.setState({ number: 1})},1000)
    setTimeout(() = > {
      console.log("btn click")
      this.btnRef.click();
    }, 1040)}render(){
    const { number } = this.state;
    return  (
      <div>
          <button 
           ref={(ref)= > { this.btnRef = ref }} 
           onClick={this.handleClick}
          > 
           add 2
          </button>
          <div> 
            {
              Array.from(new Array(4500)).map((item, index) => {
                return <span id={index}>{number}</span>})}</div>
        </div>)}}Copy the code

The process is as follows:

  1. Trigger setState at 1000 ms to create alaneDefaultLanes: binary values of 0 b0000000000000000000111000000000Update1.
  2. The browserUpdate1Corresponding update.
  3. 1040 ms triggers the onclick event to create alaneInputDiscreteLanes: binary values of 0 b0000000000000000000000000011000Update2.
  4. It is created at 1000msUpdateIt’s not done yet. Compare priority discoveryUpdate2Priority overUpdate1. To give upUpdate1, processing 1040ms triggers firstUpdate2
  5. completeUpdate2After that, the browser schedules the remaining tasks for processingUpdate1.

React internal priorities allow React to process more urgent tasks first, such as clicking on events/time should take precedence over normal time processing. This is very much in line with the React fast response goals! The actual implementation of internal priorities is explained in more detail below.

Refer to the link

  1. React Technology Revealed: react.iamkasong.com/
  2. React principle: juejin.cn/post/691707…
  3. React Scheduler uses MessageChannel: juejin.cn/post/695380…