I. Overview
Before React 16, the update of VirtualDOM was implemented using the Stack architecture, which is a recursive way. However, this comparison method has obvious defects, that is, once the task starts, it cannot be interrupted. If the number of components in the application is relatively large, the level of VirtualDOM will be deeper, and the result is that the main thread is occupied for a long time. This will block rendering and cause stuttering.
In order to avoid problems such as freezing, we must ensure that the calculation time during the update operation cannot exceed 16ms. If it exceeds 16ms, we need to pause first, let the browser render, and then continue to perform the update calculation. The Fiber architecture was created to support “interruptible rendering”.
In React, Fiber uses a new data structure fiber tree, which can convert the virtual dom tree into a linked list, and then perform the traversal operation. The linked list supports breakpoint restart when performing the traversal operation. The schematic diagram is as follows.
2. Fiber Architecture
2.1 Execution unit
In the official introduction, Fiber is understood as a data structure, but we can also understand it as an execution unit.
Fiber can be understood as an execution unit. Each time an execution unit is executed, React Fiber will check how much time is left. If there is no time, it will give up control, and then the browser will perform the rendering operation. The interaction flow between React Fiber and the browser is shown in the figure below.
It can be seen that React first requests scheduling from the browser. If the browser still has free time after executing a frame, it will determine whether there is a task to be executed. If it does not exist, it will directly give the control to the browser; The corresponding task will be executed. After executing a new task unit, it will continue to judge whether there is still time. If there is time and there is a task to be executed, it will continue to execute the next task. Otherwise, the control will be handed over to the browser for rendering. This process is cyclically.
Therefore, we can understand Fiber as an execution unit, and this execution unit must be completed at one time and cannot be suspended. Moreover, after this small execution unit completes the post-execution calculation, it can hand over control to the browser to respond to the user, thereby improving the rendering efficiency.
2.2 Data Structure
In the official documentation, Fiber is interpreted as a data structure, that is, a linked list structure. In the linked list structure, each Virtual DOM can be represented as a fiber, as shown in the following figure.
Usually, a fiber includes attributes such as child (the first child node), sibling (sibling node), and return (parent node). The implementation of the React Fiber mechanism depends on the above data structure.
2.3 Fiber linked list structure
Through the introduction, we know that Fiber uses a linked list structure, to be precise, a single linked list tree structure, see the ReactFiber.js source code for details. In order to easily understand the Fiber traversal process, let’s take a look at the Fiber linked list structure.
In the above example, each unit contains two elements, payload (data) and nextUpdate (pointer to the next unit). The definition structure is as follows:
class Update { constructor(payload, nextUpdate) { this.payload = payload //payload 数据 this.nextUpdate = nextUpdate //指向下一个节点的指针 } }
Next define a queue to connect each unit in series. To this end, we need to define two pointers: the head pointer firstUpdate and the tail pointer lastUpdate, which are used to point to the first unit and the last unit, and then add the baseState property to store the state in React.
class UpdateQueue { constructor() { this.baseState = null // state this.firstUpdate = null // 第一个更新 this.lastUpdate = null // 最后一个更新 } }
Next, define two more methods: enqueueUpdate() for inserting node cells and forceUpdate() for updating the queue. Moreover, when inserting a node unit, you need to consider whether a node already exists. If it does not exist, you can directly point firstUpdate and lastUpdate to this node. The update queue is to traverse this linked list and update the value of state according to the content of the payload
class UpdateQueue { //..... enqueueUpdate(update) { // 当前链表是空链表 if (!this.firstUpdate) { this.firstUpdate = this.lastUpdate = update } else { // 当前链表不为空 this.lastUpdate.nextUpdate = update this.lastUpdate = update } } // 获取state,然后遍历这个链表,进行更新 forceUpdate() { let currentState = this.baseState || {} let currentUpdate = this.firstUpdate while (currentUpdate) { // 判断是函数还是对象,是函数则需要执行,是对象则直接返回 let nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload currentState = { ...currentState, ...nextState } currentUpdate = currentUpdate.nextUpdate } // 更新完成后清空链表 this.firstUpdate = this.lastUpdate = null this.baseState = currentState return currentState } }
Finally, we write a test case: instantiate a queue, add many nodes to it, and then update the queue.
let queue = new UpdateQueue() queue.enqueueUpdate(new Update({ name: 'www' })) queue.enqueueUpdate(new Update({ age: 10 })) queue.enqueueUpdate(new Update(state => ({ age: state.age + 1 }))) queue.enqueueUpdate(new Update(state => ({ age: state.age + 1 }))) queue.forceUpdate() console.log(queue.baseState); //输出{ name:'www',age:12 }
2.4 Fiber Node
The splitting unit of the Fiber framework is fiber (a node on the fiber tree). In fact, the split node is the node of the virtual DOM. We need to generate the fiber tree according to the virtual DOM. The data structure of the Fiber node is as follows:
{ type: any, //对于类组件,它指向构造函数;对于DOM元素,它指定HTML tag key: null | string, //唯一标识符 stateNode: any, //保存对组件的类实例,DOM节点或与fiber节点关联的其他React元素类型的引用 child: Fiber | null, //大儿子 sibling: Fiber | null, //下一个兄弟 return: Fiber | null, //父节点 tag: WorkTag, //定义fiber操作的类型, 详见https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js nextEffect: Fiber | null, //指向下一个节点的指针 updateQueue: mixed, //用于状态更新,回调函数,DOM更新的队列 memoizedState: any, //用于创建输出的fiber状态 pendingProps: any, //已从React元素中的新数据更新,并且需要应用于子组件或DOM元素的props memoizedProps: any, //在前一次渲染期间用于创建输出的props // …… }
Finally, all fiber nodes form a tree linked list through the following attributes: child, sibling and return.
Other properties are memoizedState (the state of the fiber that creates the output), pendingProps (the props to be changed), memoizedProps (the props of the output created by the last rendering), pendingWorkPriority (defining the priority of the fiber work), etc. are not introduced too much. .
2.5 API
2.5.1 requestAnimationFrame
requestAnimationFrame is an API provided by the browser to draw animation, which requires the browser to call the specified callback function to update the animation before the next redraw (ie the next frame).
For example, use requestAnimationFrame to add 1px to the width of the square and stop until the width reaches 100px. The code is as follows.
<body> <div id="div" class="progress-bar "></div> <button id="start">开始动画</button> </body> <script> let btn = document.getElementById('start') let div = document.getElementById('div') let start = 0 let allInterval = [] const progress = () => { div.style.width = div.offsetWidth + 1 + 'px' div.innerHTML = (div.offsetWidth) + '%' if (div.offsetWidth < 100) { let current = Date.now() allInterval.push(current - start) start = current requestAnimationFrame(progress) } } btn.addEventListener('click', () => { div.style.width = 0 let currrent = Date.now() start = currrent requestAnimationFrame(progress) }) </script>
Running the above code, you can see that the browser will increase the width of the div by 1px after each frame runs until it reaches 100px.
2.5.2 requestIdleCallback
requestIdleCallback is also Fiber’s base API. requestIdleCallback enables developers to perform background and low-priority work on the main event loop without affecting delay-critical events such as animations and input responses. After the normal frame task is completed, it does not exceed 16ms, indicating that there is excess idle time, and the task registered in requestIdleCallback will be executed at this time.
The specific execution process is that the developer uses the requestIdleCallback method to register the corresponding task, and informs the browser that the priority of the task is not high. If there is idle time in each frame, the registered task can be executed. In addition, the developer can pass in the timeout parameter to define the timeout period. If the timeout period is reached, the browser must execute it immediately. The usage method is as follows:
window.requestIdleCallback(callback, { timeout: 1000 })。
After the browser finishes executing the method, if there is no time left, or there is no next executable task, React should return control and also use requestIdleCallback to apply for the next time slice. The specific process is as follows:
Among them, the callback of requestIdleCallback will receive the default parameter deadline, which contains the following two properties:
- timeRamining: Returns how much time is left in the current frame for the user to use.
- didTimeout: Returns whether the callback task times out.
3. Fiber execution process
The overall execution process of Fiber can be divided into two stages: rendering and scheduling, namely the render stage and the commit stage. Among them, the render phase is interruptible and needs to find out the changes of all nodes; while the commit phase is uninterruptible and only performs operations.
3.1 render stage
The main task of this stage is to find out the changes of all nodes, such as node addition, deletion, attribute change, etc. These changes are collectively referred to as side effects in React. In this stage, a Fiber tree will be constructed to split tasks according to the dimension of virtual Dom nodes, that is, a virtual Dom node corresponds to a task, and the final output is a side effect list (effect list) .
3.1.1 Traversal process
At this stage, React Fiber will convert the virtual DOM tree into a Fiber tree. This Fiber tree is composed of nodes. Each node has child, sibling, and return attributes. The post-order traversal method is used when traversing the Fiber tree. The process is as follows:
Traverse from the vertex;
If there is an eldest son, traverse the eldest son first; if there is no eldest son, it means that the traversal is complete;
The eldest son: a. If there is a younger brother, return the younger brother and skip to 2 b. If there is no younger brother, return to the parent node, and mark the completion of the parent node traversal, skip to 2 d. If there is no parent node, mark the end of the traversal
The following is a schematic diagram of post-order traversal:
At this point, the tree structure is defined as follows:
const A1 = { type: 'div', key: 'A1' } const B1 = { type: 'div', key: 'B1', return: A1 } const B2 = { type: 'div', key: 'B2', return: A1 } const C1 = { type: 'div', key: 'C1', return: B1 } const C2 = { type: 'div', key: 'C2', return: B1 } const C3 = { type: 'div', key: 'C3', return: B2 } const C4 = { type: 'div', key: 'C4', return: B2 } A1.child = B1 B1.sibling = B2 B1.child = C1 C1.sibling = C2 B2.child = C3 C3.sibling = C4 module.exports = A1
3.1.2 Collect the effect list
The next step is to collect the changes generated by the nodes and convert the results into an effect list. The steps are as follows:
- If the current node needs to be updated, tag the current node state (props, state, context, etc.);
- Create fibers for each child node. If no child fiber is generated, end the node, merge the effect list into return, and use the sibling node of this node as the next traversal node; otherwise, use the child node as the next traversal node;
- If there is remaining time, start the next node, otherwise wait for the next time the main thread is idle before starting the next node;
- If there is no next node, enter the pendingCommit state, at which time the effect list is collected and ends.
If it is implemented in code, it first needs to traverse the array of child virtual DOM elements and create a child fiber for each virtual DOM element.
const reconcileChildren = (currentFiber, newChildren) => { let newChildIndex = 0 let prevSibling // 上一个子fiber // 遍历子虚拟DOM元素数组,为每个虚拟DOM元素创建子fiber while (newChildIndex < newChildren.length) { let newChild = newChildren[newChildIndex] let tag // 打tag,定义 fiber类型 if (newChild.type === ELEMENT_TEXT) { // 这是文本节点 tag = TAG_TEXT } else if (typeof newChild.type === 'string') { // 如果type是字符串,则是原生DOM节点 tag = TAG_HOST } let newFiber = { tag, type: newChild.type, props: newChild.props, stateNode: null, // 还未创建DOM元素 return: currentFiber, // 父亲fiber effectTag: INSERT, // 副作用标识,包括新增、删除、更新 nextEffect: null, // 指向下一个fiber,effect list通过nextEffect指针进行连接 } if (newFiber) { if (newChildIndex === 0) { currentFiber.child = newFiber // child为大儿子 } else { prevSibling.sibling = newFiber // 让大儿子的sibling指向二儿子 } prevSibling = newFiber } newChildIndex++ } }
This method will collect all the side effects under the fiber node and form an effect list. Each fiber has two properties:
- firstEffect: Points to the first child fiber with side effects.
- lastEffect: Points to the last child fiber with side effects.
And what we need to collect is the intermediate nextEffect, and finally form a singly linked list.
// 在完成的时候要收集有副作用的fiber,组成effect list const completeUnitOfWork = (currentFiber) => { // 后续遍历,儿子们完成之后,自己才能完成。最后会得到以上图中的链条结构。 let returnFiber = currentFiber.return if (returnFiber) { // 如果父亲fiber的firstEffect没有值,则将其指向当前fiber的firstEffect if (!returnFiber.firstEffect) { returnFiber.firstEffect = currentFiber.firstEffect } // 如果当前fiber的lastEffect有值 if (currentFiber.lastEffect) { if (returnFiber.lastEffect) { returnFiber.lastEffect.nextEffect = currentFiber.firstEffect } returnFiber.lastEffect = currentFiber.lastEffect } const effectTag = currentFiber.effectTag if (effectTag) { // 说明有副作用 // 每个fiber有两个属性: // 1)firstEffect:指向第一个有副作用的子fiber // 2)lastEffect:指向最后一个有副作用的子fiber // 中间的使用nextEffect做成一个单链表 if (returnFiber.lastEffect) { returnFiber.lastEffect.nextEffect = currentFiber } else { returnFiber.firstEffect = currentFiber } returnFiber.lastEffect = currentFiber } } }
Finally, define a recursive function that starts from the root node, traverses all fiber nodes, and finally produces an effect list.
const performUnitOfWork = (currentFiber) => { beginWork(currentFiber) if (currentFiber.child) { return currentFiber.child } while (currentFiber) { completeUnitOfWork(currentFiber) if (currentFiber.sibling) { return currentFiber.sibling } currentFiber = currentFiber.return } }
3.2 Commit stage
The commit phase needs to execute the side effects calculated in the previous phase and need to be processed at one time. This phase cannot be suspended, otherwise there will be discontinuous UI updates. At this stage, all updates need to be committed to the DOM tree according to the effect list.
3.2.1 Update the view according to the effect list
At this stage, the view is updated according to the effect list of a fiber. This time, only three operations of adding a node, deleting a node, and updating a node are listed.
const commitWork = currentFiber => { if (!currentFiber) return let returnFiber = currentFiber.return let returnDOM = returnFiber.stateNode // 父节点元素 if (currentFiber.effectTag === INSERT) { // 如果当前fiber的effectTag标识位INSERT,则代表其是需要插入的节点 returnDOM.appendChild(currentFiber.stateNode) } else if (currentFiber.effectTag === DELETE) { // 如果当前fiber的effectTag标识位DELETE,则代表其是需要删除的节点 returnDOM.removeChild(currentFiber.stateNode) } else if (currentFiber.effectTag === UPDATE) { // 如果当前fiber的effectTag标识位UPDATE,则代表其是需要更新的节点 if (currentFiber.type === ELEMENT_TEXT) { if (currentFiber.alternate.props.text !== currentFiber.props.text) { currentFiber.stateNode.textContent = currentFiber.props.text } } } currentFiber.effectTag = null }
Write a recursive function that starts from the root node and completes all updates according to the effect list.
/** * 根据一个 fiber 的 effect list 更新视图 */ const commitRoot = () => { let currentFiber = workInProgressRoot.firstEffect while (currentFiber) { commitWork(currentFiber) currentFiber = currentFiber.nextEffect } currentRoot = workInProgressRoot // 把当前渲染成功的根fiber赋给currentRoot workInProgressRoot = null }
3.2.2 View Update
The next step is to execute the work in a loop. When the effect list of each fiber is calculated, commitRoot is called to complete the view update.
const workloop = (deadline) => { let shouldYield = false // 是否需要让出控制权 while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) shouldYield = deadline.timeRemaining() < 1 // 如果执行完任务后,剩余时间小于1ms,则需要让出控制权给浏览器 } if (!nextUnitOfWork && workInProgressRoot) { console.log('render阶段结束') commitRoot() // 没有下一个任务了,根据effect list结果批量更新视图 } // 请求浏览器进行再次调度 requestIdleCallback(workloop, { timeout: 1000 }) }
At this point, the refresh operation of the view is completed according to the collected change information, and the entire refresh process of Fiber is realized.
4. Summary
Compared with the traditional Stack architecture, Fiber divides the work into multiple work units, and each work unit decides whether to give up control to the browser to perform rendering according to the remaining time after the execution is completed. And it sets the priority of each unit of work, suspends, reuses and aborts the unit of work. Each Fiber node is a node on the fiber tree, connected by children, siblings, and back references to form a complete fiber tree.