The original address
Background knowledge
Fiber architecture has two main phases: Reconciliation/Render and Commit. The Reconciliation stage is usually classified as the Render stage in the source code. React iterates through the component tree to do some work:
- Update the state and props
- Call the lifecycle hook
- The child component (class component) is obtained by calling the Render method in the parent component, and the function component is directly obtained by calling the render method
- Compare the resulting child component to the previously rendered component, the DIff
- Calculate the DOM that needs to be updated
All of these activities involve the inner workings of Fiber. The specific work is performed differently based on the React Element type. For example, for a Class Component, React needs to instantiate the Class (new Component(props)). For a Function Component, React doesn’t need to instantiate the Class. If you are interested, here you can see all the work target types defined in Fiber. These are the activities that Andrew mentioned in his speech.
When dealing with UIs, the problem is that if too much work is executed all at once, It can cause animations to drop frames… The problem when dealing with UIs is that if a lot of work is done at once, it can cause animation frames to drop…
So what about the ‘all at once’ part? Basically, if React traverses the entire tree of components synchronously, it will need to do things like render updates for each component. Therefore, when the component tree is large, the execution time of this part of the code will exceed 16ms, which will cause the browser rendering quality to drop the frame, and the naked eye can see the sense of lag.
So what to do?
Newer browsers (and React Native) implement APIs that help address this exact problem… Relatively new browsers have implemented a new API that can help solve this problem…
The new API he mentioned is a global method called requestIdleCallback that calls function queues when the browser is idle. It can be used like this:
requestIdleCallback((deadline) = >{
console.log(deadline.timeRemaining(), deadline.didTimeout)
});
Copy the code
If you open your browser now and return the above code, Chrome’s log will print 49.885000000000005 false. Basically, the browser is telling you that you now have 49.885000000000005 MS to do whatever you need to do, and the time is not running out. Otherwise, deadline.didTimeout is true. Keep in mind that the timeRemaining is always changed once the browser starts to do some work.
requestIdleCallback is actually a bit too restrictive and is not executed often enough to implement smooth UI rendering, So React Team had to implement their own version._ requestIdleCallback has too many limitations on real-time to be executed consecutively multiple times to achieve smooth UI rendering. So the React team was forced to implement its own version.
Now if we put all the activities React needs to perform on a component into the performWork function, our code would look like this if we used requestIdleCallback to handle scheduling:
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
We perform work on one component, and when we’re done we return a reference to the next component to be processed. This is what Andrew talked about earlier:
In order to use those APIs, you need a way to break rendering work into incremental units, You need a way to break up the rendering effort into incremental units.
To solve this problem, React was forced to implement a new algorithm from the ground up, moving from a synchronous recursive model based on internal stack calls to an asynchronous model based on linked lists and Pointers. Andrew wrote about this section:
If you rely only on the [built-in] call stack, it will keep doing work until the stack is empty… Wouldn’t it be great if we could interrupt the call stack at will and manipulate stack frames Manually? That’s the purpose of React Fiber. Fiber is re-implementation of the stack, specialized for React components You can think of a single fiber as a virtual stack frame. If you rely on the internal call stack, it will work until the stack is empty… Wouldn’t it be great if we could pause the call stack and change it? That’s what Fiber is all about. Fiber is a reimplemented stack for the React component. You can even think of a single fiber as a stack of virtual frames.
That’s what I’m going to explain next.
A little bit about stacks
I’m guessing you’re familiar with the concept of a stack. If you put a breakpoint in your code and run it in the browser, you’ll see it. Here’s a description from Wikipedia:
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. In computer science, a call stack is a data structure that holds information about the active subroutines in a computer program… The main reason is that the call stack can track the control position returned by the active subroutine after completion of execution… , a call stack consists of multiple stack data frames… , each data frame corresponds to a call to a subroutine that has not yet finished executing. For example, a subroutine called DrawLine is running, which was called earlier by the quilt program DrawSquare, so the structure of the top of the stack looks something like the image below.
_
How does the stack relate to React
As we mentioned earlier in this article, React performs operations such as update comparisons for components while traversing the entire component tree. React’s previous coordinators used a synchronous recursive model based on the browser’s internal stack. Here is a description of this section and a discussion of recursion in the official documentation:
By default, when you recurse the child of a DOM node, React will iterate through both child lists at the same time, generating new altered child nodes if there are any differences.
If you think about it, each recursive call adds a data frame to the stack. And the whole process is synchronized. Suppose we have the following component tree:
render
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 = (a)= > [b1, b2, b3];
b1.render = (a)= > [];
b2.render = (a)= > [c1];
b3.render = (a)= > [c2];
c1.render = (a)= > [d1, d2];
c2.render = (a)= > [];
d1.render = (a)= > [];
d2.render = (a)= > [];
Copy the code
React needs to iterate over the component tree and do some work for each component. For simplicity, this part of the job is designed to print the name of the current component and retrieve its child components in the log.
The recursive traversal
The main function to iterate over this tree is called walk. Here’s how it works:
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 resulting output:
a1, b1, b2, c1, d1, d2, b3, c2
Copy the code
The recursive approach is intuitively suitable for traversing the entire component tree. But we found that it actually has some limitations. The most important thing is that it cannot break the tree of components to be traversed into incremental units, meaning that it cannot pause the traversal work on a particular component and then continue. React traverses the entire component tree until the stack is empty.
So, how does React implement traversal without using recursion? It actually uses a single-linked list traversal algorithm that makes traversal pauses possible.
List traversal
To implement this algorithm, we need a data structure that contains the following three fields:
- Child – reference to the first child node
- Sibling — reference to the first sibling node
- Return — a reference to the parent node
The context for the new coordination algorithm in React is a data structure called Fiber, which must contain the three fields mentioned above. The underlying implementation uses the React Element data to create Fiber nodes one by one.
The following diagram shows the hierarchy of fieBR nodes connected by linked lists and attributes:
class Node {
constructor(instance) {
this.instance = instance;
this.child = null;
this.sibling = null;
this.return = null; }}Copy the code
The following function takes an array of Nodes and joins them together. We’ll use it to connect nodes returned by the Render method.
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
This function iterates from the last element of the array and concatenates them to form a single linked list. It returns a reference to the first node in the list. The following demo shows how it works:
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
Here we also implement a utility function that assists the node in doing some work. In our case, it prints the name of the component in the log. It also takes the child components of the component and concatenates them:
function doWork(node) {
console.log(node.instance.name);
const children = node.instance.render();
return link(node, children);
}
Copy the code
Ok, now we can implement the core traversal algorithm. It is essentially a depth-first algorithm:
function walk(o) {
let root = o;
let current = o;
while (true) {
// Do some work for the current node, connect it to its child node, and return the first child node reference
let child = doWork(current);
// If the child node exists, set it to the node to be processed next by the doWork method
if (child) {
current = child;
continue;
}
// If you have already returned to the node, the traversal is complete, then exit directly
if (current === root) {
return;
}
// If the sibling of the current node does not exist, return to the parent node, continue to find the sibling of the parent node, and so on
while(! current.sibling) {// If you have already returned to the node, the traversal is complete, then exit directly
if(! current.return || current.return === root) {return;
}
// current points to the parent node
current = current.return;
}
// current points to a sibling nodecurrent = current.sibling; }}Copy the code
If we look at the implementation of the above algorithm in the call stack, it looks something like the following:
doWork
For the React component, Fiber is a stack reimplementation. You can even think of a single Fiber as a virtual stack frame.
We control the entire stack by keeping node references (the top of the stack).
function walk(o) {
let root = o;
let current = o;
while (true) {... current = child; . current = current.return; . current = current.sibling; }}Copy the code
We can notify the traversal operation at any time again, or we can restart it later. That’s what we want, and now we can use the new requestIdleCallback API for scheduling.
Work loops in React
Here’s how the React loop works:
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
And as you can see, this implementation corresponds nicely to the algorithm I talked about above. It always saves a reference to the Current Fiber Node into the nextUnitOfWork variable, which acts as the top of the stack.
Typically in the case of interactive UI updates such as click,input, etc., the algorithm synchronously traverses the entire component tree, performing the work for each Fiber Node. Of course, it can also be traversed asynchronously. After executing Fiber Node, it will check if there is any time left. The shouldYield function evaluates based on the variables deadlineDidExpire and deadline and returns true or false, which is used to decide whether to pause traversal. These two variables are also constantly updated during React traversal execution.
conclusion
The translator to add
In simple terms, The Fiber architecture allows React to get rid of the constraints of the browser stack, allowing users to choose whether or not to perform rendering according to the idle state of the browser. The overall rendering time is not shortened, but lengthened. The entire rendering process is divided into multiple processes. These processes are scattered throughout the browser’s free time, making the UI much smoother for the user.