After learning this article, you will learn:

  1. Understand how Context works

  2. The React component’s render timing is mastered at the source level to write a high-performance React component

  3. Understand shouldComponentUpdate, React. Memo, PureComponent and other performance optimization means to achieve the source level

I will try to make it easy to understand. However, to fully understand the content of the article, you need to know the following beforehand:

  1. General workflow of the Fiber architecture

  2. Priorities and updates in React source code

If you don’t already know how to React, read the React Technology Debunked.

Timing of component render

The implementation of the Context is tied to the render of the component. Before explaining its implementation, let’s take a look at render’s timing.

In other words, when does the component render?

The answer to this question has already been discussed in the React component when render. Here’s a recap:

In React, whenever updates are triggered (such as calls to this.setState, useState), the corresponding Fiber nodes are created for the components.

Fiber nodes are linked to each other to form a Fiber tree.

There are two ways to create a Fiber node:

  1. Ubiquitously, the fiber node corresponding to the previous update of this component was reused as the fiber node of this update.

  2. Render, generate a new fiber node after diff algorithm. Component render (such as ClassComponent’s render method call, FunctionComponent execution) occurs in this step.

React regenerates a Fiber tree every time it is updated.

React performance isn’t exactly great. However, as you can see, not all components in the Fiber tree generation process will render, and some components that meet the optimization conditions will enter the bailout logic.

For example, for the following Demo:

function Son() {
  console.log('child render! ');
  return <div>Son</div>;
}


function Parent(props) {
  const [count, setCount] = React.useState(0);

  return (
    <div onClick={()= > {setCount(count + 1)}}>
      count:{count}
      {props.children}
    </div>
  );
}


function App() {
  return (
    <Parent>
      <Son/>
    </Parent>
  );
}

const rootEl = document.querySelector("#root");
ReactDOM.render(<App/>, rootEl);
Copy the code

Online Demo Address

Click on the div child of the Parent component to trigger the update, but child Render! It doesn’t print.

This is because the Son component enters the bailout logic.

The conditions of the bailout

To enter bailout logic, four conditions should be met simultaneously:

  1. oldProps === newProps

That is, the props updated this time are all equal to the props updated last time.

Notice that this is congruent comparison.

We know that the component Render returns JSX, which is the syntactic sugar for react. createElement.

So the result of render is actually an execution of React. CreateElement, which is an object containing the props property.

This update is the result of the React. CreateElement, which is a new reference to the oldProps. = = newProps.

  1. context valueThere is no change

We know that in the current React version, there are both old and new contexts.

  1. workInProgress.type === current.type

Fiber. type remains the same before and after the update, such as div does not change to P.

  1. ! IncludesSomeLane (renderLanes, updateLanes)?

Is there an update on the fiber tree, and if so, is the priority of the update consistent with that of the entire fiber tree?

If consistent, an update exists on the component, then render logic is required.

The optimization of Bailout didn’t stop there. If all nodes of a fiber subtree are not updated, even if all subsequences of fiber follow the bailout logic, there is still a traversal cost.

Therefore, in bailout, all descendants of the fiber are checked to see if fiber satisfies condition 4 (the time complexity of the check is O(1)).

If no descendant fiber updates need to be executed, bailout returns NULL. The entire subtree is skipped.

Not bailout nor render, just as if there were no existence. The corresponding DOM does not change.

Implementation of the old Context API

Now we have a general idea of Render’s timing. With this concept in mind, you can understand how the ContextAPI was implemented and why it was refactored.

Let’s start with the deprecated implementation of the old ContextAPI.

The generation process of Fiber tree is interruptible recursion realized through traversal, so it is divided into two stages: recursion and regression.

The Context is stored on the stack.

In the recursion phase, the Context is constantly being pushed. So Concumer can go up the Context stack and find the corresponding Context value.

In the return phase, the Context keeps going off the stack.

So why was the old ContextAPI scrapped? Because it doesn’t work with performance optimizations like shouldComponentUpdate or Memo.

The realization of the shouldComponentUpdate

To explore deeper reasons, we need to understand the principle of shouldComponentUpdate, referred to as SCU.

SCU was used to reduce unnecessary render, in other words: let the component that was supposed to render do bailout logic.

Well, we just described what a bailout would have to do. So which of these four conditions does SCU operate on?

OldProps === newProps

This component bailout condition changes when shouldComponentUpdate is used:

oldProps === newProps

++ SCU === false

Similarly, a bailout condition also changed when PureComponenet and React.memo were used:

oldProps === newProps

++ shallow compare oldProps to newsProps

So let’s go back to the old ContextAPI.

When these performance optimization means:

  • Causes the component to hit the bailout logic

  • At the same time, if the subtrees of the component all satisfy the bailout condition 4

Then the fiber subtree will not continue traversing generation.

In other words, you don’t have to push and push the Context.

In this case, even if the context value changes, descendant components will not be able to detect it.

Implementation of the new Context API

Knowing the pitfalls of the old ContextAPI, let’s look at how the new ContextAPI is implemented.

When through:

ctx = React.createContext();
Copy the code

After creating the context instance, you need to supply the value using the Provider and subscribe to the value using Consumer or useContext.

Such as:

ctx = React.createContext();

const NumProvider = ({children}) = > {
  const [num, add] = useState(0);

  return (
    <Ctx.Provider value={num}>
      <button onClick={()= > add(num + 1)}>add</button>
      {children}
    </Ctx.Provider>)}Copy the code

Use:

const Child = () = > {
  const {num} = useContext(Ctx);
  return <p>{num}</p>
}
Copy the code

When traversing components to generate corresponding fibers, the ctx. Provider component is traversed, and the CTX. Provider component checks whether the context value changes.

If the context value changes, the CTx. Provider performs a depth-first traversal of the subtree to find the Consumer matching the Provider.

In the previous example, the fiber corresponding to the Child component of useContext(Ctx) is eventually found and an update is triggered for that fiber.

Notice how clever the implementation is:

An update is typically generated by a component calling a method that triggers the update. For the NumProvider component above, clicking button to call Add triggers an update.

The essence of triggering the update is that the component does not satisfy the bailout condition 4 when it creates the corresponding fiber:

! IncludesSomeLane (renderLanes, updateLanes)?

Enter render logic.

Here, the context value in ctx. Provider changes, and the ctx. Provider goes down to the component Child that consumes the Context value and triggers an update for its fiber.

Then the Child corresponding to fiber does not meet condition 4.

This solves the old ContextAPI problem:

Provider to Child does not satisfy condition 4 of fiber:

All descendant nodes in the subtree satisfy condition 4

Therefore, even if a component entered bailout logic in the middle of the traversal, null would not be returned, that is, the traversal of these subtrees would not be ignored.

The final traversal reaches the Child, which does not satisfy condition 4, and enters the render logic to call the corresponding function of the component.

const Child = () = > {
  const {num} = useContext(Ctx);
  return <p>{num}</p>
}
Copy the code

In the function call useContext is called to find the corresponding updated Context value from the Context stack and return it.

conclusion

React Performance is key: reduce unnecessary render.

As we have seen above, the essence of the component is to satisfy four conditions, thus entering the bailout logic.

The ContextAPI essentially makes the Consumer component fail condition 4.

We also know that React traverses the whole tree each time, but it has bailout optimization logic, not all components will render.

In extreme cases, some subtrees may even be skipped (bailout returns NULL).