This article is written by Han Ting, a member of the team. We have authorized the exclusive use of Doodle Big Front-end, including but not limited to editing and original annotation rights.
React has been one of the most popular frameworks on the front end since it was opened in May 2013. The framework itself has the following features.
- Declarative
- Component-based
- Learn Once, Write Anywhere
In addition, it is fast and efficient, mainly thanks to the application of Virtual Dom. Virtual Dom is an abstract description of HTML Dom nodes, which exists in structural objects in JS. During rendering, Diff algorithm is used to find nodes that need to be changed for updating, thus saving unnecessary updates.
React fast response is mainly limited by CPU bottlenecks, as shown in the following example:
function App() {
const len = 3000;
return (
<ul>
{Array(len).fill(0).map((_, i) => <li>{i}</li>)}
</ul>
}
const rootEl = document.querySelector("#root");
ReactDOM.render(<App/>, rootEl);
Copy the code
When a lot of nodes need to be rendered, there will be a lot of JS calculation, because the GUI rendering thread and JS execution thread are mutually exclusive, so the browser interface rendering behavior will stop during THE JS calculation, resulting in the page feeling stuck.
The refresh frequency of mainstream browsers is 60Hz, that is, every (1000ms / 60Hz) 16.6ms browser refresh, which means that the time of rendering a frame must be controlled within 16ms to ensure that frames are not dropped.
The following operations need to be completed during this period:
- Script execution (JavaScript)
- CSS Object Model
- Layout (Layout)
- Paint
- Composite
JS->Style->Layout->Paint->Composite First, how does React do this
// react/packages/scheduler/src/forks/SchedulerHostConfig.default.js
// Scheduler periodically yields in case there is other work on the main
// thread, like user events. By default, it yields multiple times per frame.
// It does not attempt to align with frame boundaries, since most tasks don't
// need to be frame aligned; for those that do, use requestAnimationFrame.
let yieldInterval = 5;
let deadline = 0;
Copy the code
React uses this time (5ms) to update components each time. When the time exceeds this, React assigns execution rights to the browser. React itself waits for the next frame to continue the interrupted work. Break time-consuming tasks into each frame and execute small tasks at a time. The summary is to turn synchronous updates into interruptible asynchronous updates
React v15 Stack Reconciler
ReactDOM.render(<App />, rootEl);
Copy the code
The React DOM hands the
- 【 function 】 -> props
- 【 class 】 -> new App(props) to instantiate the App and call the lifecycle method
ComponentWillMount (), after which the Render () method is called to retrieve the rendered elements
Tips: A common question during an interview is whether function components and class components are instantiated. The answer is right up there
The process is a recursive process based on the depth of the tree (which continues when custom components are encountered, all the way down to the most primitive HTML tags). The Stack Reconciler’s recursion cannot be interrupted or paused once it is on the call Stack, if the components are deeply nested or in extremely large numbers, Failure to do so within 16ms is bound to result in browser frame loss and stutter. As mentioned above, the solution was to turn synchronous updates into interruptible asynchronous updates. However, the Version 15 architecture did not support asynchronous updates, so the React team decided to roll up their sleeves and rewrite. After two years of work, the React team finally released a working version in 2017/3.
React Fiber
The virtual DOM tree is constructed in the first rendering, and dom change is obtained through the diff virtual DOM tree in the subsequent update (setState). Finally, DOM change is applied to the real DOM tree. The inability to interrupt the Stack Reconciler’s top-down recursion (mount/update) results in the layout/animation/interactive responses on the mainline not being processed in a timely manner, causing the stall.
These issues, Fiber Reconciler, can be addressed.
Fiber, the smallest working unit, generates a FiberNode each time it is first built through reactdom.rende.
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag; // FiberNode types. There are currently 25 FiberNode types. The most common ones are FunctionComponent and ClassComponent
this.key = key; // Same as the key in Element
this.elementType = null;
this.type = null; //Function|String|Symbol|Number|Object
this.stateNode = null; / / FiberRoot | DomElement accordingly | ReactComponentInstance binding of other objects
// Fiber
this.return = null; / / FiberNode | null parent FiberNode
this.child = null; / / FiberNode | null first child FiberNode
this.sibling = null;/ / FiberNode | null adjacent next siblings
this.index = 0; // The current location in parent fiber
this.ref = null; // same as ref in Element
this.pendingProps = pendingProps; // Object New props
this.memoizedProps = null; // Object new props after processing
this.updateQueue = null; // UpdateQueue Specifies the state to be changed
this.memoizedState = null; //Object new state after processing
this.dependencies = null;
this.mode = mode; // number
// Normal mode, synchronous rendering, for use in act15-16 production environment
// Concurrent mode, asynchronous rendering, for production use in Act17
// Strict mode, used to check for deprecated apis, used by the react16-17 development environment
// Performance test mode, used to detect where there are performance problems, react16-17 development environment use
// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null; // The diff process in the Render phase detects that fiber's child nodes need to be removed if any
this.lanes = NoLanes; // If fiber.lanes is not empty, the fiber node has been updated
this.childLanes = NoLanes; // Determine whether the current subtree has an important basis for updating, if there is an update, continue to build, otherwise directly reuse the existing fiber tree
this.alternate = null; / / FiberNode | null standby nodes, cache before, Fiber node associated with double caching mechanism, subsequent interpretation
}
Copy the code
All Fiber objects are FiberNode instances, identified by a tag. Initialize the FiberNode node with createFiber as follows
const createFiber = function(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) :Fiber {
// $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
return new FiberNode(tag, pendingProps, key, mode);
};
Copy the code
Fiber’s solution to this problem is to break the render/update process into a series of small tasks, execute one piece at a time, and see if there is time left to move on to the next task. If so, continue, if not, suspend and return the thread of execution.
Fiber Tree
React creates a Fiber Tree with different Element types corresponding to different types of Fiber nodes. In subsequent updates, each re-rendering will recreate the Element, but Fiber will not be re-created. It only updates its own properties.
As the name implies, a Fiber Tree is formed by multiple Fiber nodes, and the structure of the Fiber Tree is developed to meet the characteristics of incremental update of Fiber.
First of all, each node is unified. There are two attributes, FirstChild and NextSibiling. The first one points to the first son node of the node, and the second one points to the next brother node. Meanwhile, Fiber Tree adds three additional instances to the Instance layer:
- Effect: Each workInProgress tree node has an Effect list to store diFF results, which will be collected by updateQueue after the update
- WorkInProgress: Reconcile process snapshots, work process nodes, not visible to the user
- Fiber: Describes the context information required for incremental updates
Let’s focus on what does workInProgress do? So let’s look at the code first how is it created
// This is used to create an alternate fiber to do work on.
export function createWorkInProgress(current: Fiber, pendingProps: any) :Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
// The following two sentences are crucial
workInProgress.alternate = current;
current.alternate = workInProgress;
// do something else ...
} else {
// do something else ...
}
// do something else ...
return workInProgress;
}
Copy the code
If the current node alternate is null, create a new workInProgress Fiber tree using createFiber. Dom update is completed through the replacement of current and workInProgress. In simple terms, when the workInProgress Tree is built in memory, Fiber Tree is replaced directly, which is the double buffering mechanism just mentioned.
When the in-memory workInProgress tree is built directly, it replaces the Fiber tree that the page needs to render, which is the mount process.
When one of the nodes on the page changes, a new Render phase starts and a workInProgress tree of the heart is constructed. There is an optimization point here because each node has an alternate property pointing to each other. During construction, it will try to reuse the existing in-node attributes of the current Current Fiber tree. Whether to reuse depends on the diff algorithm.
During the update process, React creates an effect list on the fiber that actually changed in the Filbert Tree and executes it in the Commit phase. Dom update is implemented only for the fiber that actually changes, avoiding the performance waste caused by traversing the whole Fiber tree. Each time a Fiber node’s flags field is not NoFlags, the Fiber node is added to the Effect list, and dom tree changes are performed based on the effectTag type of each effect.
Recursive Fiber node
Each node in Fiber architecture undergoes two recursive processes, beginWork/completeWork.
1, beginWork
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) :Fiber | null {
// do something else
}
Copy the code
- Current: Fiber node last updated for the current component, workinprogress.alternate
- WorkInProgress: Fiber node for the current component memory
- Renderlanes: Relative priority
Due to the existence of double caching mechanism, we can use current === null to determine whether the component is mounted or uplate. When mounting, different types of sub-fiber nodes will be created according to fiber.tag. When didReceiveUpdate === false, you can reuse the subfiber node from the previous update.
if(current ! = =null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if( oldProps ! == newProps || hasLegacyContextChanged() || (__DEV__ ? workInProgress.type ! == current.type :false)
) {
didReceiveUpdate = true;
} else if(! includesSomeLane(renderLanes, updateLanes)) { didReceiveUpdate =false;
switch (workInProgress.tag) {
// do something else
}
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else {
didReceiveUpdate = false; }}else {
didReceiveUpdate = false;
}
Copy the code
2, completeWork
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) :Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;
case ClassComponent: {
// do something else
return null;
}
case HostRoot: {
// do something else
updateHostContainer(workInProgress);
return null;
}
case HostComponent: {
// do something else
return null;
}
// do something else
}
Copy the code
The parameters passed in are the same as beginWork, and without much explanation, the completeWork calls different processing logic depending on the tag. You can also use current === NULL to determine whether the current node is in the mount or update phase. Since completeWork is a function of the “attribution” stage, each appendAllChildren call inserts the generated descendant node into the currently generated DOM node, creating a complete DOM tree.
3, effectList
Every Fiber node that completes the completeWork and has an effectTag is stored in the effectList. The first and last Fiber nodes of the effectList are stored in the fiber.firstEffect /fiber.lastEffect properties, respectively.
EffectList allows the COMMIT phase to simply traverse the effectList, improving performance, and the Render phase is over.
Write in the last
I think the React Fiber is a kind of the concept of architecture, to solve the problem from React16 architecture is divided into three layers: the Scheduler/Reconciler/the Renderer
It uses the idle time of the browser to complete the loop simulation recursive process. All operations are carried out in memory. Only when all components complete Rconciler work, Renderer will be used for one rendering display to improve efficiency.