Original link:
Indepth. Dev/the – how – and…
Front knowledge
Fiber architecture has two main rendering phases:
- reconciliation/render
- commit
The Reconciliation stage is also referred to as the render stage in the source code. In this phase, React traverses the entire component tree and does the following:
- Update the state and props
- Call the lifecycle method
- Retrieves the children of the current component
- Compare the new parent component
- Calculate the DOM updates that need to be performed during the COMMIT phase
All of these operations are called work inside Fiber. The type of work that needs to be done depends on the type of React Element. For example, React instantiates the Class Component, but not the Functional Component. Here you can see all the work types on Fiber if you’re interested. Andrew also mentioned this in his talk:
When dealing with the UI, doing too much React work at once can cause animation frames to drop…
So what does’ one-time execution ‘mean? If React traverses the entire tree of components synchronously and updates each component, the code may take longer than 16ms to execute, resulting in frame lag.
So can this problem be solved?
Newer browsers and React Native implement apis to address this issue…
The global function, requestIdleCallback, queues functions to be called when the browser is idle. The following example shows you how to use it:
requestIdleCallback((deadline) = >{
console.log(deadline.timeRemaining(), deadline.didTimeout)
});
Copy the code
If you execute the above code on the Console, Chrome will print 49.9 false. This means THAT I have 49.9ms to do whatever WORK I want and still have time to spare, otherwise deadline.didTimeout will be true. Remember that the timeRemaining changes as soon as the browser executes, so you should always check for it.
RequestIdleCallback was limited in use and could not be called frequently for smooth UI rendering, so the React team had to implement their own version.
If we put the code for the React update component into the performWork function and use requestIdleCallback to schedule it, the code would look like this:
requestIdleCallback((deadline) = > {
// while we have time, perform work for a part of the components tree
while ((deadline.timeRemaining() > 0|| deadline.didTimeout) && nextComponent) { nextComponent = performWork(nextComponent); }});Copy the code
The code above does the relevant update work for a single component and returns a reference to the next component. Instead of processing the component tree synchronously as the previous reconciliation algorithm did. Andrew also talked about this:
To use these apis, you need a way to break down the rendering effort into units
React reimplements tree traversal. The original algorithm uses a synchronous recursive strategy based on the built-in stack, while the new algorithm uses an asynchronous strategy based on linked lists and Pointers. Andrew’s article also mentioned:
If you rely only on the built-in call stack, React will work until the call stack is empty… Wouldn’t it be nice if you could interrupt the call stack and manually manipulate every frame of the call stack? That’s the idea behind React Fiber. Fiber is designed for React components and reimplements the call stack. You can also treat each Fiber as a frame.
What is a stack?
I assume that you are already familiar with the concept of call stacks. Make a breakpoint to your code, and you can see its call stack in the browser debug window. Here’s how Wikipedia explains it:
In computer science, A *call stack* is a stack data structure that stores information about the active subroutines of a computer program… the main reason for having call stack is *to keep track of the point* to which each active subroutine should return Control when it finishes… A *call stack* is composed of *stack frames*… Each stack frame corresponds to a call to a subroutine which has not yet terminated with a *return*. For example, if a subroutine named DrawLine is currently running, having been called by a subroutine DrawSquare, the top part of the call stack might be laid out like in the adjacent picture.
Why is the stack related to React?
As I mentioned in the first part of this article, React iterates through the component tree and updates components in the Reconciliation/Render phase. The previous algorithm adopts the strategy of synchronous recursive traversal of the component tree based on the built-in call stack. This article introduces the Reconciliation recursive algorithm:
In the first part of the article, we mentioned that React iterates through the component tree and updates components in the Reconciliation/Render phase. Previous Reconciler algorithms used a strategy of synchronous recursive built-in call stacks. The official documentation explains the process and explains the recursive algorithm:
By default, when recursively traversing the children of a DOM node, React only traverses both lists of child nodes at the same time, finding their differences and generating a mutation in the traverse.
If you think about it, each recursion adds a frame to the stack. And it’s synchronous. Suppose we have the following component tree:
The render function shown below returns some objects. You can think of these objects as instances of the React component:
const a1 = {name: 'a1'};
const b1 = {name: 'b1'};
const b2 = {name: 'b2'};
const b3 = {name: 'b3'};
const c1 = {name: 'c1'};
const c2 = {name: 'c2'};
const d1 = {name: 'd1'};
const d2 = {name: 'd2'};
a1.render = () = > [b1, b2, b3];
b1.render = () = > [];
b2.render = () = > [c1];
b3.render = () = > [c2];
c1.render = () = > [d1, d2];
c2.render = () = > [];
d1.render = () = > [];
d2.render = () = > [];
Copy the code
React requires traversing the entire component tree and updating each component. To simplify the process, when updating each component, only the value of the name property of the current component is printed and its children are returned. Here’s how the recursion works.
The recursive traversal
To traverse the tree recursively, call the walk function as follows:
walk(a1);
function walk(instance) {
doWork(instance);
const children = instance.render();
children.forEach(walk);
}
function doWork(o) {
console.log(o.name);
}
Copy the code
The above code will print:
a1, b1, b2, c1, d1, d2, b3, c2
Copy the code
If you’re not familiar with recursion, check out my article: Understanding Recursion in Depth.
Recursion is great for traversing tree structures. But it has one of the biggest limitations, which is that you can’t break a job down into smaller units. We cannot stop updating a component and resume it at a later time. React keeps iterating through in this way until all components are processed and the call stack is empty.
So how does React traverse the entire tree without recursing? React, in fact, uses a single-list tree traversal. This allows for temporary traversal and inhibits the growth of the call stack.
List traversal
Luckily, I found a snippet of this algorithm in Sebastian Markbage’s issue. To implement this algorithm, you need a data structure with three fields:
- Child – points to the first child node
- Sibling – Points to the first sibling node
- Return – Points to the parent node
With the new reconciliation algorithm, Fiber calls the data structure composed of the above fields. At the bottom, it represents a React Element. I’ll learn more about it in my next article.
The flowchart below shows the relationship between each node:
So we first define the data structure of the node:
class Node {
constructor(instance) {
this.instance = instance;
this.child = null;
this.sibling = null;
this.return = null; }}Copy the code
Use the link function to join the list of child nodes returned by the render function as shown below:
function link(parent, elements) {
if (elements === null) elements = [];
parent.child = elements.reduceRight((previous, current) = > {
const node = new Node(current);
node.return = parent;
node.sibling = previous;
return node;
}, null);
return parent.child;
}
Copy the code
The Link method traverses the list of nodes from back to front, joining them as a single linked list. The function finally returns a pointer to the first node in the list. The following code looks like this:
const children = [{name: 'b1'}, {name: 'b2'}];
const parent = new Node({name: 'a1'});
const child = link(parent, children);
// the following two statements are true
console.log(child.instance.name === 'b1');
console.log(child.sibling.instance === children[1]);
Copy the code
We also implemented a doWork helper function to perform operations on a single node. The function internally prints the component’s name (component.name). In addition, it retrieves the list of child nodes and concatenates them as follows:
function doWork(node) {
console.log(node.instance.name);
const children = node.instance.render();
return link(node, children);
}
Copy the code
Ok, so now we’ve implemented the core traversal algorithm. The depth-first strategy is used, and the code is as follows:
function walk(o) {
let root = o;
let current = o;
while (true) {
// perform work for a node, retrieve & link the children
let child = doWork(current);
// if there's a child, set it as the current active node
if (child) {
current = child;
continue;
}
// if we've returned to the top, exit the function
if (current === root) {
return;
}
// keep going up until we find the sibling
while(! current.sibling) {// if we've returned to the top, exit the function
if(! current.return || current.return === root) {return;
}
// set the parent as the current active node
current = current.return;
}
// if found, set the sibling as the current active nodecurrent = current.sibling; }}Copy the code
Although the above code implementation is not hard to understand, you should try it out for yourself. The idea is to keep a reference to a current node and reassign as you traverse a path in the tree until you reach the end of the loop. The return pointer is then used to return the parent node.
If we examine the call stack for the above algorithm, we see:
As you can see, the call stack does not grow as the tree traverses. But if you now put a breakpoint on the doWork method, you can see the following:
This looks a lot like the browser call stack. So with this algorithm, we effectively replace the browser’s default call stack with our own implementation. Andrew also mentioned this:
Fiber is a reimplementation of the call stack, designed for the React component. You can think of a fiber as a virtual stack frame.
Because we now manage the call stack by keeping references to the node, the node can be treated as a top-level frame:
function walk(o) {
let root = o;
let current = o;
while (true) {... current = child; . current = current.return; . current = current.sibling; }}Copy the code
We can interrupt or resume traversal at any time. This is a prerequisite for using the requestIdleCallback API.
React event loop
React 实现事件循环(work loop
)的代码 在这里:
function workLoop(isYieldy) {
if(! isYieldy) {// Flush work without yielding
while(nextUnitOfWork ! = =null) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); }}else {
// Flush asynchronous work until the deadline runs out of time.
while(nextUnitOfWork ! = =null&&! shouldYield()) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); }}}Copy the code
As you can see, the algorithm for the code above is very similar to the one I mentioned. Save a reference to the current Fiber node through the nextUnitOfWork variable.
This algorithm synchronously traverses the component tree and performs updates on each fiber node (next tunit of work). The same is true for interactive updates produced by UI events such as Click and input. In addition to synchronous traversal, the algorithm also traverses the tree asynchronously, checking to see if there is any time left after updating a Fiber node. This value was calculated in real time by deadlineDidExpire and Deadline during the React component update.
For a deeper understanding of performUnitOfWork, read this article.