Before you begin, tell me what problems the article will help you solve.
- Why did Facebook refactor React
- What is React Fiber
- React Fiber’s core algorithm — How does React interrupt the restart task
- React Fiber
preface
The source code for this article is based on React V17.0.2
why React Fiber
- Browser rendering process
Start with how the browser works. As we all know, the browser is multi-process multi-thread, multi-process including the main process, rendering process, plug-in process, GPU process, etc., as a front-end developer, we mainly pay attention to the rendering process, here is the page rendering, HTML parsing, CSS parsing, JS execution place. There are multiple threads in the rendering process. This time, the core focuses on the two threads of page rendering, the GUI thread and the JS thread.
GUI thread is responsible for rendering the browser interface, including parsing HTML, CSS, layout drawing, etc. The JS thread contains the parsing engine for the JS code we usually write, most notably Google’s V8. It is important to note that the JS engine and GUI rendering are mutually exclusive, because JS can change HTML or CSS styles, and if executed at the same time, the page rendering will be chaotic, so when the JS engine executes, the GUI rendering thread will be suspended, waiting for the JS engine to execute immediately.
- The GPU to render
We often see animation, video is essentially by the picture quickly flashed, deceive the human eyes, let a person think that is a continuous animation, to contain more images per second of animation more fluent, normal 60 pictures can let the human feel is smooth animation, so most of the current equipment FPS is 60, namely, 60 frames per second. So Chrome will need to render the image below in 16ms so that the user doesn’t feel like dropping frames.
Therefore, if the JS execution time is too long, basically more than 10ms, the user will feel obvious lag, which will affect the user experience (JS execution in the following is based on 16ms as the cut-off point, not counting the subsequent rendering, the actual execution time must be less than 16ms). React execution requires diff of two trees. Although React has optimized the diff algorithm according to the characteristics of HTML, if the comparison level between two trees is deeper, it will still be far more than 16ms.
React Fiber
Based on this, how do you solve the problem? In the figure above, React acts as JS, and all synchronization operations are performed at the beginning. After React execution is complete, subsequent operations such as HTML parsing and layout rendering will be performed. The easiest thing to think of is to optimize JS execution speed to reduce the React thread footprint to under 16ms. In React execution, the diff algorithm is the most time-consuming. React is optimized for HTML scenarios. There is no better algorithm in the industry to shorten the time of diFF algorithm, so the execution time is still very long when the tree level is very deep.
In modern browsers, the requestIdleCallback method is used to let developers know how much time is left after the browser has performed all operations on the current frame.
requestIdleCallback
requestIdleCallback((deadline) = > {
while ((deadline.timeRemaining() > 0|| deadline.didTimeout) && nextComponent) { nextComponent = performWork(nextComponent); }});Copy the code
An aside:
RequestIdleCallback ((Deadline)) is the callback parameter of requestIdleCallback. The time may be different on different pages. It can even exceed 16ms (49.9ms on React), but you can see why here
In addition, the React source code does not use requestIdleCallback because of its limitations. Instead, it implements a similar mechanism.
Using this method we know how much time is left in each frame, so we can work on the remaining time, and if the current frame is not enough time, we can put the remaining work into the next frame’s requestIdleCallback. This is what React means in time slicing.
Therefore, to use this method, we need to change the DIff algorithm based on synchronous recursive traversal based on the JS built-in stack call to asynchronous incremental update. According to the head of React
If you just rely on js’s built-in call stack, it will execute until the stack is empty… Wouldn’t it be perfect if we could interrupt at will and manually call the stack? That’s what React Fiber is all about. Fiber is a reimplementation of the stack for the React Component. You can think of a Fiber as a task in a virtual stack.
In human terms, the original recursion of the tree is deep recursion traversal. Now I need to re-implement the recursion algorithm, so that I can traverse the React component node by node without relying on the stack call, and can interrupt and start from the current at any time during the process.
stack Reconciliation vs Fiber Reconciliation
stack Reconciliation
Suppose we have the following HTML structure
The js objects converted to the React component are as follows
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
Normally, we would recursively traverse the “tree” like this, which is how we recursively traverse the DOM tree in the earliest versions of React
function walk(instance) {
console.log(instance.name);
let children = instance.render();
children.forEach((child) = > {
walk(child);
});
}
walk(a1);
Copy the code
As you can see, this way, you can traverse the entire tree, but it doesn’t do the interrupted recursion that we talked about earlier, so if you interrupt the recursion of the tree, next time you have to traverse the entire tree from the root again. This obviously doesn’t work, it just keeps recursing until the stack is empty. How does React Fiber interrupt the restart task?
The answer is a single list tree traversal. In simple terms, the original nested structure of the tree itself is changed into a single list tree.
Fiber Reconciliation
React: How does React traverse a tree using a linked list? To implement this algorithm, let’s first look at the data structure we need
- Child refers to the first child of the node
- Sibling, points to the next sibling of the node
- Return, pointing to the parent node of the node
It’s the same DOM tree that we had before, but now it looks like this
We will not describe the process of constructing Fiber tree. We will go through the algorithm (parent node first, depth first)
let root = fiber;
let node = fiber;
while (true) {
if (node.child) {
node = node.child;
continue;
}
if (node === root) {
return;
}
while(! node.sibling) {if(! node.return || node.return === root) {return;
}
node = node.return;
}
node = node.sibling;
}
Copy the code
As you can see, after obtaining the root node, and constantly traverse the child nodes, until the deepest node, and then start from the child nodes of the deepest node traversal brother, if you don’t have siblings parent node, the node is returned if there is a brother nodes, finish the child nodes of the brothers to each node traversal, until the last child node, and then returned to the parent node. This is repeated until the root node is returned.
Here are Fiber’s data objects in the React source code. After all, Fiber is an object. React createElement creates an extra layer of data structure to support the above single-linked list traversal.
Fiber data structure
The React source code contains Fiber properties.
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode
) {
// Instance
this.tag = tag; // What type of Fiber/component is the WorkTag 0-24
this.key = key; // Unique identifier
this.elementType = null;
this.type = null;
this.stateNode = null; / / stateNode: class div
// Fiber data structure
this.return = null; / / the parent node
this.child = null; // First child node
this.sibling = null; // Sibling nodes
this.index = 0; //
this.ref = null;
this.pendingProps = pendingProps; //newprops
this.memoizedProps = null; // oldProps props of the previous session
// Update ue data structure:
// {
// baseState: fiber.memoizedState,
// firstBaseUpdate: null,
// lastBaseUpdate: null,
// shared: {
// pending: null,
// interleaved: null,
// lanes: NoLanes,
/ /},
// effects: null,
// };
this.updateQueue = null; // Batch queue
this.memoizedState = null; //oldState
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags; // mark the fiber change mode
this.subtreeFlags = NoFlags;
this.deletions = null;
// Priority scheduling
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null; //work-in-progress current alternate
}
Copy the code
Improved performance from Fiber
- Take a look at the Demo before and after refactoring to see how the experience improves
- It lays the foundation for the subsequent React Concurrent mode
Fiber flow process
A simple flow chart is drawn to illustrate the flow process of Fiber.
Illustration:
React handles the Fiber loop logic in the performUnitOfWork and completeUnitOfWork methods, and handles the component logic in beginWork and completeWork. The beginwork will process state updates, corresponding life cycle calls in this phase, and the reconcile process (marking Fiber nodes with new, delete, move, etc. You can see all flags here). In the completeWork phase, all flags flags are bubbled up to the parent node. To facilitate updates during the COMMIT phase.
I remember Dan Abramov having a graphic metaphor for Effect List. I can write it down.
You can think of the React Fiber as a Christmas tree, and the Effect List is the lights hanging on the Tree
React source code – too long to look at series
Here is the core source code for Fiber in React – a lot of code that is not relevant to this article has been removed, so you can choose to use it or not.
Contains comments to the code and its location in the React repository. I don’t want you to read the code comments.
/ / https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L1635
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while(workInProgress ! = =null&&! shouldYield()) { performUnitOfWork(workInProgress); }}Copy the code
performUnitOfWork
/ / https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L1642
function performUnitOfWork(unitOfWork: Fiber) :void {
const current = unitOfWork.alternate;
let next;
// always return unitofwork. child without processing sibling
next = beginWork(current, unitOfWork, subtreeRenderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
// Return to the next fiber to be processed
if (next === null) {
// Go to the lowest leaf node of the link and handle the Sibling node in this function
completeUnitOfWork(unitOfWork);
} else{ workInProgress = next; }}Copy the code
beginWork
/ / https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberBeginWork.old.js#L3083
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
) :Fiber | null {
let updateLanes = workInProgress.lanes;
// There are a lot of tags, so I'm just going to keep the usual FunctionComponent and ClassComponent, so I'll just look at updateClassComponent
switch (workInProgress.tag) {
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
// The return value is workinprogress.child, as seen in finishClassComponent
returnupdateClassComponent( current, workInProgress, Component, resolvedProps, renderLanes ); }}}function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes
) {
const instance = workInProgress.stateNode;
let shouldUpdate;
// The update lifecycle and batch updates are processed in this phase,
if (instance === null) {
if(current ! = =null) {
// A class component without an instance only mounts if it suspended
// inside a non-concurrent tree, in an inconsistent state. We want to
// treat it like a new mount, even though an empty version of it already
// committed. Disconnect the alternate pointers.
current.alternate = null;
workInProgress.alternate = null;
// Since this is conceptually a new fiber, schedule a Placement effect
workInProgress.flags |= Placement;
}
// In the initial pass we might need to construct the instance.
constructClassInstance(workInProgress, Component, nextProps);
mountClassInstance(workInProgress, Component, nextProps, renderLanes);
shouldUpdate = true;
} else if (current === null) {
// In a resume, we'll already have an instance we can reuse.ShouldUpdate = resumeMountClassInstance(workInProgress, Component, nextProps, renderLanes); }else {
// Life cycle and batch updates are handled in this phase
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderLanes
);
}
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderLanes
);
return nextUnitOfWork;
}
function finishClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
shouldUpdate: boolean,
hasContext: boolean,
renderLanes: Lanes
) {
const instance = workInProgress.stateNode;
// Rerender
ReactCurrentOwner.current = workInProgress;
let nextChildren;
nextChildren = instance.render();
// Initialize or execute dom diff // reactChildfiber.old.js
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
//child
return workInProgress.child;
}
Copy the code
completeUnitOfWork
/ / https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L1670
function completeUnitOfWork(unitOfWork: Fiber) :void {
// Attempt to complete the current unit of work, then move to the next
// sibling. If there are no more siblings, return to the parent fiber.
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
let next;
// Return value is always null
next = completeWork(current, completedWork, subtreeRenderLanes);
const siblingFiber = completedWork.sibling;
if(siblingFiber ! = =null) {
// If there is more work to do in this returnFiber, do that next.
workInProgress = siblingFiber;
return;
}
// Otherwise, return to the parent
completedWork = returnFiber;
// Update the next thing we're working on in case something throws.
workInProgress = completedWork;
} while(completedWork ! = =null);
}
/ / https://github.com/facebook/react/blob/v17.0.2/packages/react-reconciler/src/ReactFiberCompleteWork.old.js#L645
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) :Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case FunctionComponent:
bubbleProperties(workInProgress);
return null;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
bubbleProperties(workInProgress);
return null; }}Copy the code
The last
Learn how to debug React. Here is the code repository for learning how to debug React. If you are interested in debugging React and do not want to set up a debugging environment, you can clone it directly here. In addition, there are some simple comments that may help those in need.
Because the React source code involves a very large system, it is very painful to read the source code directly, so THANK you for sharing the React source code earlier, which gave me the motivation to continue reading.
Finally, thank you for reading ~, if you find any mistakes in the article, please kindly feedback to me, I will correct in time.
trailer
There are a few topics I want to learn about React, and I will probably write them down later
- Scheduler Scheduling principle
- Understanding of Concurrent Mode
- Principle of Hooks
Refer to the link
Here are some good links to learn React. Not only do I want to close React Fiber, I will update it when I see good articles later. Recommendations are also welcome
Lin Clark – A Cartoon Intro to Fiber – React Conf 2017
React director sebmarkbage’s initial vision for fiber was the React fiber data structure
React Why use a single linked list to traverse the component tree
React scheduling algorithm
About the React lanes
React Scheduling