Principle of Fiber
What was the problem with React before Fiber appeared
Prior to React 16, the process of updating VirtualDOM was implemented using the Stack architecture, which is circular recursion. The problem with this comparison method is that once the task starts, it cannot be interrupted. If there are a large number of components in the application and the VirtualDOM level is relatively deep, the main thread will be occupied for a long time. The main thread can only be released after the whole VirtualDOM tree comparison and update is completed, and then the main thread can perform other tasks. This will lead to some user interaction, animation and other tasks can not be performed immediately, the page will have a lag, very impact on the user experience. Core problems: recursion cannot be interrupted; task execution takes a long time; JavaScript is single-threaded and mutually exclusive with Native GUI; other tasks cannot be performed in the process of comparing VirtualDOM, resulting in task delay, page lag, and poor user experience.
Simple implementation of the Stack architecture
Let’s implement a simple process of getting JSX, converting JSX into A DOM, and then adding it to the page
const jsx = (
<div id="a1">
<div id="b1">
<div id="c1"></div>
<div id="c2"></div>
</div>
<div id="b2"></div>
</div>
)
function render(vdom, container) {
// Create the element
const element = document.createElement(vdom.type)
// Add attributes to the element
Object.keys(vdom.props)
.filter(propName= >propName ! = ="children") // Filter the children attribute
.forEach(propName= > (element[propName] = vdom.props[propName]))
// Create child elements recursively
if (Array.isArray(vdom.props.children)) {
vdom.props.children.forEach(child= > render(child, element))
}
// Add the element to the page
container.appendChild(element)
}
render(jsx, document.getElementById("root"))
Copy the code
As you can see, the JSX code is translated into real DOM and added to the page
How does Fiber solve the performance problem
- React abandons recursive calls in the Fiber architecture and uses loops to simulate recursion because loops can be broken at any time.
- Fiber breaks large render tasks down into smaller tasks
- React uses requestIdleCallback to use idle browser time to perform small tasks. After React executes a task unit, it checks to see if there are other high priority tasks. If so, it gives up the occupied thread and executes the high priority task first.
requestIdleCallback
Let’s start with requestIdleCallback’s explanation on MDN
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-in-first-called order; however, if the callback function specifies a timeout for execution, it is possible to disrupt the execution order in order to execute the function before the timeout.
The grammar of the requestIdleCallback
RequestIdleCallback takes two arguments, a callback function named IdleDeadline and an optional argument
The IdleDeadline argument has a timeRemaining() method that returns a time DOMHighResTimeStamp, a value of floating point type, which represents the estimated number of milliseconds left in the current idle period. If the idle period has ended, it has a value of 0. Your callback function (passed to requestIdleCallback) can repeatedly access this property to determine whether the idle time of the current thread can be used to perform more tasks before it ends.
The role of requestIdleCallback
Browser pages are drawn frame by frame by engine, and when the number of frames per second reaches 60, the page is smooth. As anyone who has played an FPS knows, when this number is less than 60, the human eye can detect a lag. 60 frames per second, and the time allotted to each frame is 1000/60 ≈ 16 ms. If the execution time of each frame is less than 16 ms, it means that the browser has free time. Can you use the free time of the browser to process tasks, so that the main task is not completed all the time? RequestIdleCallback is all about taking advantage of free time in the browser to perform tasks.
Above said a bunch of, some people may have meng, you don’t nonsense, directly on the code to give me an example on the line. Let’s see what’s so amazing about requestIdleCallback
<style>
#box {
padding: 20px;
background: palegoldenrod;
}
</style>
<! -- body -->
<div id="box"></div>
<button id="btn1">Perform a computing task</button>
<button id="btn2">Changing the background color</button>
<script>
const box = document.getElementById('box')
const btn1 = document.getElementById('btn1')
const btn2 = document.getElementById('btn2')
let number = 999999
let value = 0
function calc() {
while (number > 0) {
value = Math.random() < 0.5 ? Math.random() : Math.random()
console.log(value)
number--
}
}
btn1.onclick = function () {
calc()
}
btn2.onclick = function () {
box.style.background = 'green'
}
</script>
Copy the code
In the above code, we create a random number through a long loop to increase the amount of computation in the browser. You can try this demo with your local IDE, and you will find that when you click on the execute calculation task and then click on the Change background color button, the color of the box will not change immediately. Instead, it waits a few seconds (or even slower if the computer is poor) for the change to occur. Because the rendering of the Native GUI and v8 engine is mutually exclusive, there is some delay in rendering the page.
<style>
#box {
padding: 20px;
background: palegoldenrod;
}
</style>
<! -- body -->
<div id="box"></div>
<button id="btn1">Perform a computing task</button>
<button id="btn2">Changing the background color</button>
<script>
const box = document.getElementById('box')
const btn1 = document.getElementById('btn1')
const btn2 = document.getElementById('btn2')
let number = 999999
let value = 0
function calc(deadline) {
while (number > 0 && deadline.timeRemaining() > 0) {
value = Math.random() < 0.5 ? Math.random() : Math.random()
console.log(value)
number--
}
requestIdleCallback(calc)
}
btn1.onclick = function () {
requestIdleCallback(calc)
}
btn2.onclick = function () {
box.style.background = 'green'
}
</script>
Copy the code
The above code is optimized using requestIdleCallback. After running, click on the change background color button and you can immediately see the color change. This is what requestIdleCallback does.
Fiber principle analysis
Let’s see how Fiber works by implementing a simple version of Fiber
What is the Fiber
For all that chatter, what exactly is Fiber? Fiber is an execution unit of React. After React 16, React split the entire rendering task into small tasks for processing, and each small task refers to the construction of Fiber nodes. The split tasks are executed during free time in the browser. After each unit is completed, React checks to see if there is time left and, if so, switches control of the main thread.
Fiber is a data structure that supports the operation of the Fiber build task. Once a Fiber mission is complete, how do you find the next Fiber mission to execute? React uses a linked list structure to find the next task unit to execute. Fiber is essentially a JavaScript object that has a child property for the node’s child, a sibling property for the node’s next sibling, and a return property for the node’s parent.
// Simple version of the Fiber object
type Fiber = {
// Div, span, component constructor
type: any,
/ / the DOM object
stateNode: any,
// Point to its parent Fiber object
return: Fiber | null.// Point to its first child, the Fiber object
child: Fiber | null.// Point to the next sibling iber object
sibling: Fiber | null,}Copy the code
Implement a simple version of Fiber
Fiber works in two phases: The Render phase and the COMMIT phase.
- Render phase: Build the Fiber object, build the list, mark the DOM operations to be performed in the list, interrupt.
- Commit phase: DOM operations are performed according to the built linked list, which cannot be interrupted.
const jsx = (
<div id="a1">
<div id="b1">
<div id="c1"></div>
<div id="c2"></div>
</div>
<div id="b2"></div>
</div>
)
const container = document.getElementById("root")
// Build the Fiber object for the root element
const workInProgressRoot = {
stateNode: container,
props: {
children: [jsx]
}
}
// The next task to perform
let nextUnitOfWork = workInProgressRoot
function workLoop(deadline) {
// 1. Do you have any spare time
// 2. Whether there is a task to perform
while (nextUnitOfWork && deadline.timeRemaining() > 0) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}
// Indicates that all tasks have been executed
if(! nextUnitOfWork) {// Enter the second phase of DOM execution
commitRoot()
}
}
function performUnitOfWork(workInProgressFiber) {
// 1. Create a DOM object and store it in the stateNode property
// 2. Build a subfiber of the current Fiber
// The process of going down
beginWork(workInProgressFiber)
// If the current Fiber has children
if (workInProgressFiber.child) {
// Return child builds child's child
return workInProgressFiber.child
}
while (workInProgressFiber) {
// Go up and build the list
completeUnitOfWork(workInProgressFiber)
// If there is a sibling
if (workInProgressFiber.sibling) {
// Return children of the sibling build sibling
return workInProgressFiber.sibling
}
// Update the parent
workInProgressFiber = workInProgressFiber.return
}
}
// Build a subset
function beginWork(workInProgressFiber) {
// 1. Create a DOM object and store it in the stateNode property
if(! workInProgressFiber.stateNode) {/ / create the DOM
workInProgressFiber.stateNode = document.createElement(
workInProgressFiber.type
)
// Add properties to the DOM
for (let attr in workInProgressFiber.props) {
if(attr ! = ="children") {
workInProgressFiber.stateNode[attr] = workInProgressFiber.props[attr]
}
}
}
// 2. Build a subfiber of the current Fiber
if (Array.isArray(workInProgressFiber.props.children)) {
let previousFiber = null
workInProgressFiber.props.children.forEach((child, index) = > {
let childFiber = {
type: child.type,
props: child.props,
effectTag: "PLACEMENT".return: workInProgressFiber
}
if (index === 0) {
// Create a subset where only the first child is a subset
workInProgressFiber.child = childFiber
} else {
// If it is not the first, build the sibling of the subset
previousFiber.sibling = childFiber
}
previousFiber = childFiber
})
}
// console.log(workInProgressFiber)
}
function completeUnitOfWork(workInProgressFiber) {
// Get the parent of the current Fiber
const returnFiber = workInProgressFiber.return
// Whether the parent exists
if (returnFiber) {
// The Fiber that needs to perform DOM operations
if (workInProgressFiber.effectTag) {
if(! returnFiber.lastEffect) { returnFiber.lastEffect = workInProgressFiber.lastEffect }if(! returnFiber.firstEffect) { returnFiber.firstEffect = workInProgressFiber.firstEffect }if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = workInProgressFiber
} else {
returnFiber.firstEffect = workInProgressFiber
}
returnFiber.lastEffect = workInProgressFiber
}
}
}
function commitRoot() {
let currentFiber = workInProgressRoot.firstEffect
while (currentFiber) {
currentFiber.return.stateNode.appendChild(currentFiber.stateNode)
currentFiber = currentFiber.nextEffect
}
}
// Perform the task while the browser is idle
requestIdleCallback(workLoop)
Copy the code
Build the Fiber list
The above function completeUnitOfWork is used to build the Fiber list, and will only be rendered in the list
- What is the building order of a linked list?
The order of the list is determined by the order of DOM operations, c1 is the first to perform the DOM operation so it’s the beginning of the chain, and A1 is the last element to be added to Root, so it’s the end of the chain.
- How do I add new elements to the end of the chain?
Stores the next item in the chain through nextEffect in the list structure.
During the process of building the list, you need to store the most recent items in the list through a variable that is used each time a new item is added and updated after each operation is complete. This variable is called lastEffect in the source code.
LastEffect is stored on the parent of the current Fiber object. When the parent changes, lastEffect moves up first and then appends nextEffect in order to avoid any confusion in the link order
- Where to store the list?
The list needs to be stored in Root because when it goes to phase 2, the commitRoot method commits Root to phase 2. In the source code, there is a property called firstEffect under Root Fiber for storing lists. In the process of building the list, how can WE store C1 in Root when C1 starts and Root ends? It’s actually done by moving the end of the chain up.
conclusion
In fact, the overall idea of Fiber is to use loops and lists instead of recursion to optimize performance. Fiber architecture has two stages: Render phase is responsible for building Fiber objects and lists, while COMMIT phase is responsible for building DOM. Just for the sake of understanding, here’s the full implementation of Fiber github.com/maoxiaoxing…