Wu Xiaohui, front-end development engineer of Wedoctor Cloud service team, likes fitness and travel.
What is refresh rate?
Most monitors have a fixed refresh rate (for example, the latest ones are usually at 60Hz), so browser updates are best at 60fps. There is no point in having the browser redraw twice between hardware refreshes and it will only consume performance. Browsers take advantage of this interval of 16ms(a frame) to throttle the rendering appropriately, and if you do too much within 16ms, the rendering will block and the page will get stuck, so 16ms becomes a critical time for page rendering optimization.
What does a frame do
- Events: Click events, keyboard events, and scroll events
- macro: macro task, such as
setTimeout
- micro: microtasks, such as
Promise
- rAF: requestAnimationFrame
Tell the browser window. RequestAnimationFrame () – you want to perform an animation, and required the browser until the next redraw calls the specified callback function to update the animation. This method takes as an argument a callback function that is executed before the browser’s next redraw.
- Layout: CSS computing and page Layout
- Paint: draws a page
- rIC: requestIdleCallback
Window. RequestIdleCallback () method will be called function in browser free time line. This enables developers to perform background and low-priority work on the main event loop without affecting the delay of critical events such as animations and input responses. Functions are typically executed in first-come-first-called order; however, if a callback function specifies a timeout, it is possible to scramble the order of execution in order to execute the function before it times out.
So many things to do in one frame… If js execution takes longer than 16ms, it will block, and a frame will be lost
Macro tasks are always executed after microtasks, but in uncertain order
TIPS: The concept of Reconciliation: Compare the virtual DOM tree to identify the nodes that need to be changed, updated, and called Reconcliation
Coordination before React16
- Features:
- The React DOM tree is used to create stack recursions
- When an instance is updated during virtualDOM comparison, DOM operations are performed immediately.
- Synchronous updates, no interruptions
- Code sample
There is a Component that simulates the DOM Diff process:
const Component = (
<div id="A1">
<div id="B1">
<div id="C1"></div>
<div id="C2"></div> </div> <div id="B2"></div> </div> ) Copy the code
The Diff process:
The Component defined above is first converted to ReactElement by Babel to React.CreateElement. The root-like structure is shown below (the properties are much simplified and only the structure is shown below).
let root = {
key: 'A1'. children: [
{
key: 'B1'. children: [ { key: 'C1'. children: []. }, { key: 'C2'. children: []. } ]. }, { key: 'B2'. children: []. } ].}; Depth-first traversal function walk(vdom) { doWork(vdom); vdom.children.forEach(child= > { walk(child); }) } // Update operation function doWork(vdom) { console.log(vdom.key); } walk(root); Copy the code
Disadvantages:
If there is a large number of updates or a deep component tree, the stack of diff operations will become too deep to be released in time, and js will hold the main thread until the entire virtualDOM tree has been calculated before handing over execution to the rendering engine. This will lead to user interaction and page animation not responding, there will be a significant sense of lag (frame drop), affecting the user experience.
- Solution:
Divide a long task into many small pieces, and each small piece has a short running time. Although the total time is still long, other tasks are given a chance to execute after each small piece is executed, so that the only thread will not be monopolized, and other tasks still have a chance to run. So React introduced Fiber coordination in version 15 and update 16.
Concept of Fiber
Fiber is a reconstruction of the React core algorithm, and the result of the two-year reconstruction is Fiber Reconciler
Core goal: Expand its applicability to include animation, layout and gestures.
- Break down interruptible work into smaller tasks
- Prioritize what you’re doing, redo, and reuse what you did last time (half done)
- Yield back and forth between parent and child tasks to support layout refreshes during React execution
- support
render()
Return multiple elements - Better support for Error Boundary
A corresponding Fiber is generated inside each Virtual DOM node.
Fiber Pre-knowledge
How to interrupt a task: Implement a interruptible workLoop similar to Fiber.
function sleep(delay) {
for (let start = Date.now(); Date.now() - start <= delay;) {}
}
// Each subterm can be considered a fiber const works = [ (a)= > { console.log('First mission initiated'); sleep(20); console.log('First mission completed'); }, () = > { console.log('Task 2 begins'); sleep(20); console.log('End of 2nd mission'); }, () = > { console.log('Task three begins'); sleep(20); console.log('End of mission 3'); }, ]; window.requestIdleCallback(workLoop, { timeout: 1000}); function workLoop(deadLine) { console.log('Remaining time of this frame left'.parseInt(deadLine.timeRemaining())); / * * deadLine { * timeRemaining(), returns how much MS is left in this frame for the user to use* didTimeout Returns whether the CB task times out*}* / while ((deadLine.timeRemaining() > 0 || deadLine.didTimeout) && works.length > 0) { TimeRemaining () performUnitOfWord(); } if (works.length > 0) { window.requestIdleCallback(workLoop, { timeout: 1000}); } } function performUnitOfWord() { works.shift()(); // Execute with the first element } Copy the code
- Singly linked lists
- A data structure for storing data
- Data is represented in the form of nodes, and each node is composed of elements + Pointers (subsequent element storage location). An element is the storage unit for storing data
- Single-linked list is an important data structure in Fiber. A lot of asynchronous update logic is implemented by single-linked list (the update ue update list in setState is also based on single-linked list).
Simulate a logic similar to setState batch update in React.
/ * *Fiber uses linked lists (single-linked lists) a lot, and the tail pointer doesn't point to it* /
class Update {
constructor(payload, nextUpdate) { this.payload = payload; this.nextUpdate = nextUpdate; // Pointer to the next node } } class UpdateQueue { constructor(payload) { this.baseState = null; / / the original state this.firstUpdate = null; // First update this.lastUpdate = null; // Last update } enqueueUpdate(update) { if (this.firstUpdate === null) { this.firstUpdate = this.lastUpdate =update; } else { this.lastUpdate.nextUpdate = update; this.lastUpdate = update; } } // Get the old state, iterate through the list, and update it forceUpdate() { let currentState = this.baseState || {}; // Initial state let currentUpdate = this.firstUpdate; while (currentUpdate) { let nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload; currentState = { . currentState,. nextState, }; // Use the current update to get the latest status currentUpdate = currentUpdate.nextUpdate; // find the next node } this.firstUpdate = this.lastUpdate = null; // Update ends to clear the list this.baseState = currentState; return currentState; } } // Lists can be broken and resumed // Each time setState is stored in a linked list and merged // enqueueUpdate is analogous to the setState operation let queue = new UpdateQueue(); queue.enqueueUpdate(new Update({ name: 'Wedoctor Group' })); queue.enqueueUpdate(new Update({ number: 0 })); queue.enqueueUpdate(new Update(state= > ({ number: state.number + 1 }))); queue.enqueueUpdate(new Update(state= > ({ number: state.number + 1 }))); console.log(queue) queue.forceUpdate(); Copy the code
Consider: why is setState updated asynchronously in a composite event? Explanation: SetState does not update any of the states in the UpdataQueue. Instead, the value (or function) that needs to be updated is added to the list of UpdataQueue, and is processed when forceUpdate is executed. The state is updated after the processing, so we don’t get the state we expected until forceUpdate is executed.
The Fiber in the React
- Fiber’s two execution phases
- Reconcile(render) : For the virtualDOM operation phase, corresponding to the new scheduling algorithm, the Diff Fiber Tree is used to find the update work to be done and the Fiber Tree is generated. This is a JS calculation process, the calculation results can be cached, the calculation process can be interrupted, or can be resumed. So when React introduced the Fiber Reconciler scheduling algorithm, it mentioned that the new algorithm has the new features of separable and interruptable tasks, because this part of the work is a pure JS calculation process, which can be cached, interrupted and recovered.
- Commit: In the render phase, when the update work is received, the update is committed and the corresponding render module (react-DOM) is called for rendering. To prevent page jitter, this process is synchronized and cannot be interrupted.
- React defines a component that creates Fiber.
const Component = (
<div id="A1">
A1
<div id="B1">
B1 <div id="C1">C1</div> <div id="C2">C2</div> </div> <div id="B2">B2</div> </div> ) Copy the code
- The Component defined above is a Component that Babel parses by default by calling the react.createElement () method, resulting in a virtualDOM structure like the one shown in the code below that is passed to the reactdom.render () method for scheduling.
{
"type":"div". "key":null. "ref":null. "props": {
"id":"A1". "children": [ "A1". { "type":"div". "key":null. "ref":null. "props": { "id":"B1". "children": [ "B1". { "type":"div". "key":null. "ref":null. "props": { "id":"C1". "children":"C1" }, "_owner":null. "_store": { } }, { "type":"div". "key":null. "ref":null. "props": { "id":"C2". "children":"C2" }, "_owner":null. "_store": { } } ] }, "_owner":null. "_store": { } }, { "type":"div". "key":null. "ref":null. "props": { "id":"B2". "children":"B2" }, "_owner":null. "_store": { } } ] }, "_owner":null. "_store": { } } Copy the code
- The Render method takes the Virtual DOM, creates fibers (the Render phase) for each Virtual DOM, and connects them according to the relational connections.
- The fiber structure
class FiberNode {
constructor(tag, pendingProps, key, mode) {
// Instance properties
this.tag = tag; // Flag different component types, such as classComponent, functionComponent
this.key = key; // The react element key is the same as the JSX key, which is the final ReactElement key
this.elementType = null; // The first argument to createElement, type on ReactElement this.type = null; // Represents the actual type of fiber. ElementType is basically the same this.stateNode = null; // The instance object, such as the class component new, is mounted on this property, or FiberRoot if it is RootFiber // fiber this.return = null; // Parent node, pointing to the previous fiber this.child = null; // The child node points to the first fiber below itself this.sibling = null; // Sibling component, pointing to a sibling node this.index = 0; If there are no siblings, each child is given an index, and the index and key are diff together this.ref = null; // ref attribute on reactElement this.pendingProps = pendingProps; / / new props this.memoizedProps = null; / / the old props this.updateQueue = null; // A single setState execution on the update queue on fiber will attach a new update to the property, and each update will eventually form a linked list, which will eventually be updated in batches this.memoizedState = null; // For memoizedProps, the state rendered last time is equivalent to the current state, understood as the relationship between prev and next this.mode = mode; // Represents how the children of the current component are rendered // effects this.effectTag = NoEffect; // Indicates what update fiber is currently doing this.nextEffect = null; // Point to the next fiber update this.firstEffect = null; // Point to the first of all child nodes in fiber that needs to be updated this.lastEffect = null; // Point to the last fiber of all the child nodes that needs to be updated this.expirationTime = NoWork; // Expiration time, which represents at what point in the future the task should be completed this.childExpirationTime = NoWork; // child expiration time this.alternate = null; // References between the current tree and the workInprogress tree } } Copy the code
Fiber has many properties. All sub-nodes of Fiber are connected through child, return, siblint, and alternate connection is the state of each update, which is used to compare each state update and cache. We use node ID to identify each Fiber component. Converting to Fiber will eventually generate the structure as shown below, which is also similar to virtualDOM structure. The order of construction is first child => Sibling => return. If the current node has no child, the node completes.
Fiber tree
- Collect rely on
The dependency collection is completed simultaneously in the render phase of generating Fiber. The linked list is constructed according to the order of completion of each node. Each component with Fiber points to the next component that needs to be updated through its nextEffect. Each parent node has firstEffect and lastEffect to connect the first and last update of its child node, resulting in an update linked list like the one below.
Side effects linked list (Update linked list)
- Commit update commit
After Fiber is created for all nodes, the commit phase will start from root’s fistEffect (the first side effect phase for all nodes), then look for firstEffect’s nextEffect node, and so on, all updates will be completed in one go. Then clear the update list, complete the update, this process cannot be interrupted.
conclusion
React update all nodes need to create Fiber for the first time. It will be updated later: Fiber based diff, React composite events, various types of components (class components, Function components), hooks, event priority (context time) internal scheduling.