詳解React?Fiber架構(gòu)原理
一、概述
在 React 16 之前,VirtualDOM 的更新采用的是Stack架構(gòu)實現(xiàn)的,也就是循環(huán)遞歸方式。不過,這種對比方式有明顯的缺陷,就是一旦任務開始進行就無法中斷,如果遇到應用中組件數(shù)量比較龐大,那么VirtualDOM 的層級就會比較深,帶來的結(jié)果就是主線程被長期占用,進而阻塞渲染、造成卡頓現(xiàn)象。
為了避免出現(xiàn)卡頓等問題,我們必須保障在執(zhí)行更新操作時計算時不能超過16ms,如果超過16ms,就需要先暫停,讓給瀏覽器進行渲染,后續(xù)再繼續(xù)執(zhí)行更新計算。而Fiber架構(gòu)就是為了支持“可中斷渲染”而創(chuàng)建的。
在React中,F(xiàn)iber使用了一種新的數(shù)據(jù)結(jié)構(gòu)fiber tree,它可以把虛擬dom tree轉(zhuǎn)換成一個鏈表,然后再執(zhí)行遍歷操作,而鏈表在執(zhí)行遍歷操作時是支持斷點重啟的,示意圖如下。

二、Fiber架構(gòu)
2.1 執(zhí)行單元
官方介紹中,F(xiàn)iber 被理解為是一種數(shù)據(jù)結(jié)構(gòu),但是我們也可以將它理解為是一個執(zhí)行單元。
Fiber 可以理解為一個執(zhí)行單元,每次執(zhí)行完一個執(zhí)行單元,React Fiber就會檢查還剩多少時間,如果沒有時間則將控制權讓出去,然后由瀏覽器執(zhí)行渲染操作。React Fiber 與瀏覽器的交互流程如下圖。

可以看到,React 首先向瀏覽器請求調(diào)度,瀏覽器在執(zhí)行完一幀后如果還有空閑時間,會去判斷是否存在待執(zhí)行任務,不存在就直接將控制權交給瀏覽器;如果存在就會執(zhí)行對應的任務,執(zhí)行完一個新的任務單元之后會繼續(xù)判斷是否還有時間,有時間且有待執(zhí)行任務則會繼續(xù)執(zhí)行下一個任務,否則將控制權交給瀏覽器執(zhí)行渲染,這個流程是循環(huán)進行的。
所以,我們可以將Fiber 理解為一個執(zhí)行單元,并且這個執(zhí)行單元必須是一次完成的,不能出現(xiàn)暫停。并且,這個小的執(zhí)行單元在執(zhí)行完后計算之后,可以移交控制權給瀏覽器去響應用戶,從而提升了渲染的效率。
2.2 數(shù)據(jù)結(jié)構(gòu)
在官方的文檔中,F(xiàn)iber 被解釋為是一種數(shù)據(jù)結(jié)構(gòu),即鏈表結(jié)構(gòu)。在鏈表結(jié)構(gòu)中,每個 Virtual DOM 都可以表示為一個 fiber,如下圖所示。

通常,一個 fiber包括了 child(第一個子節(jié)點)、sibling(兄弟節(jié)點)、return(父節(jié)點)等屬性,React Fiber 機制的實現(xiàn),就是依賴于上面的數(shù)據(jù)結(jié)構(gòu)。
2.3 Fiber鏈表結(jié)構(gòu)
通過介紹,我們知道Fiber使用的是鏈表結(jié)構(gòu),準確的說是單鏈表樹結(jié)構(gòu),詳見ReactFiber.js源碼。為了放便理解 Fiber 的遍歷過程,下面我們就看下Fiber鏈表結(jié)構(gòu)。

在上面的例子中,每一個單元都包含了payload(數(shù)據(jù))和nextUpdate(指向下一個單元的指針)兩個元素,定義結(jié)構(gòu)如下:
class Update {
constructor(payload, nextUpdate) {
this.payload = payload //payload 數(shù)據(jù)
this.nextUpdate = nextUpdate //指向下一個節(jié)點的指針
}
}
接下來定義一個隊列,把每個單元串聯(lián)起來。為此,我們需要定義兩個指針:頭指針firstUpdate和尾指針lastUpdate,作用是指向第一個單元和最后一個單元,然后再加入baseState屬性存儲React中的state狀態(tài)。
class UpdateQueue {
constructor() {
this.baseState = null // state
this.firstUpdate = null // 第一個更新
this.lastUpdate = null // 最后一個更新
}
}接下來,再定義兩個方法:用于插入節(jié)點單元的enqueueUpdate()和用于更新隊列的forceUpdate()。并且,插入節(jié)點單元時需要考慮是否已經(jīng)存在節(jié)點,如果不存在直接將firstUpdate、lastUpdate指向此節(jié)點即可。更新隊列是遍歷這個鏈表,根據(jù)payload中的內(nèi)容去更新state的值
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) {
// 判斷是函數(shù)還是對象,是函數(shù)則需要執(zhí)行,是對象則直接返回
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
}
}最后,我們寫一個測試的用例:實例化一個隊列,向其中加入很多節(jié)點,再更新這個隊列。
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節(jié)點
Fiber 框架的拆分單位是 fiber(fiber tree上的一個節(jié)點),實際上拆分的節(jié)點就是虛擬DOM的節(jié)點,我們需要根據(jù)虛擬dom去生成 fiber tree。 Fiber節(jié)點的數(shù)據(jù)結(jié)構(gòu)如下:
{
type: any, //對于類組件,它指向構(gòu)造函數(shù);對于DOM元素,它指定HTML tag
key: null | string, //唯一標識符
stateNode: any, //保存對組件的類實例,DOM節(jié)點或與fiber節(jié)點關聯(lián)的其他React元素類型的引用
child: Fiber | null, //大兒子
sibling: Fiber | null, //下一個兄弟
return: Fiber | null, //父節(jié)點
tag: WorkTag, //定義fiber操作的類型, 詳見https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js
nextEffect: Fiber | null, //指向下一個節(jié)點的指針
updateQueue: mixed, //用于狀態(tài)更新,回調(diào)函數(shù),DOM更新的隊列
memoizedState: any, //用于創(chuàng)建輸出的fiber狀態(tài)
pendingProps: any, //已從React元素中的新數(shù)據(jù)更新,并且需要應用于子組件或DOM元素的props
memoizedProps: any, //在前一次渲染期間用于創(chuàng)建輸出的props
// ……
}最終, 所有的fiber 節(jié)點通過以下屬性:child,sibling 和 return來構(gòu)成一個樹鏈表。
其他的屬性還有memoizedState(創(chuàng)建輸出的 fiber 的狀態(tài))、pendingProps(將要改變的 props )、memoizedProps(上次渲染創(chuàng)建輸出的 props )、pendingWorkPriority(定義 fiber 工作優(yōu)先級)等等就不在過多的介紹了。
2.5 API
2.5.1 requestAnimationFrame
requestAnimationFrame是瀏覽器提供的繪制動畫的 API ,它要求瀏覽器在下次重繪之前(即下一幀)調(diào)用指定的回調(diào)函數(shù)以更新動畫。
例如,使用requestAnimationFrame實現(xiàn)正方形的寬度加1px,直到寬度達到100px停止,代碼如下。
<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>運行上面的代碼,就可以看到瀏覽器會在每一幀運行結(jié)束后,將div的寬度加1px,直到100px為止。
2.5.2 requestIdleCallback
requestIdleCallback 也是 Fiber 的基礎 API 。requestIdleCallback能使開發(fā)者在主事件循環(huán)上執(zhí)行后臺和低優(yōu)先級的工作,而不會影響延遲關鍵事件,如動畫和輸入響應。正常幀任務完成后沒超過16ms,說明有多余的空閑時間,此時就會執(zhí)行requestIdleCallback里注冊的任務。
具體的執(zhí)行流程是,開發(fā)者采用requestIdleCallback方法注冊對應的任務,告知瀏覽器任務的優(yōu)先級不高,如果每一幀內(nèi)存在空閑時間,就可以執(zhí)行注冊的這個任務。另外,開發(fā)者是可以傳入timeout參數(shù)去定義超時時間的,如果到了超時時間,那么瀏覽器必須立即執(zhí)行,使用方法如下:
window.requestIdleCallback(callback, { timeout: 1000 })瀏覽器執(zhí)行完方法后,如果沒有剩余時間了,或者已經(jīng)沒有下一個可執(zhí)行的任務了,React應該歸還控制權,并同樣使用requestIdleCallback去申請下一個時間片。具體的流程如下圖:

其中,requestIdleCallback的callback中會接收到默認參數(shù) deadline ,其中包含了以下兩個屬性:
- timeRamining:返回當前幀還剩多少時間供用戶使用。
- didTimeout:返回 callback 任務是否超時。
三、Fiber執(zhí)行流程
Fiber的執(zhí)行流程總體可以分為渲染和調(diào)度兩個階段,即render階段和commit 階段。其中,render 階段是可中斷的,需要找出所有節(jié)點的變更;而commit 階段是不可中斷的,只會執(zhí)行操作。
3.1 render階段
此階段的主要任務就是找出所有節(jié)點產(chǎn)生的變更,如節(jié)點的新增、刪除、屬性變更等。這些變更, React 統(tǒng)稱為副作用,此階段會構(gòu)建一棵Fiber tree,以虛擬Dom節(jié)點的維度對任務進行拆分,即一個虛擬Dom節(jié)點對應一個任務,最后產(chǎn)出的結(jié)果是副作用列表(effect list)。
3.1.1 遍歷流程
在此階段,React Fiber會將虛擬DOM樹轉(zhuǎn)化為Fiber tree,這個Fiber tree是由節(jié)點構(gòu)成的,每個節(jié)點都有child、sibling、return屬性,遍歷Fiber tree時采用的是后序遍歷方法,遍歷的流程如下:
從頂點開始遍歷;
如果有大兒子,先遍歷大兒子;如果沒有大兒子,則表示遍歷完成;
大兒子: a. 如果有弟弟,則返回弟弟,跳到2 b. 如果沒有弟弟,則返回父節(jié)點,并標志完成父節(jié)點遍歷,跳到2 d. 如果沒有父節(jié)點則標志遍歷結(jié)束
下面是后序遍歷的示意圖:

此時,樹結(jié)構(gòu)的定義如下:
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 = A13.1.2 收集effect list
接下來,就是收集節(jié)點產(chǎn)生的變更,并將結(jié)果轉(zhuǎn)化成一個effect list,步驟如下:
- 如果當前節(jié)點需要更新,則打tag更新當前節(jié)點狀態(tài)(props, state, context等);
- 為每個子節(jié)點創(chuàng)建fiber。如果沒有產(chǎn)生child fiber,則結(jié)束該節(jié)點,把effect list歸并到return,把此節(jié)點的sibling節(jié)點作為下一個遍歷節(jié)點;否則把child節(jié)點作為下一個遍歷節(jié)點;
- 如果有剩余時間,則開始下一個節(jié)點,否則等下一次主線程空閑再開始下一個節(jié)點;
- 如果沒有下一個節(jié)點了,進入pendingCommit狀態(tài),此時effect list收集完畢,結(jié)束。
如果用代碼來實現(xiàn)的話,首先需要遍歷子虛擬DOM元素數(shù)組,為每個虛擬DOM元素創(chuàng)建子fiber。
const reconcileChildren = (currentFiber, newChildren) => {
let newChildIndex = 0
let prevSibling // 上一個子fiber
// 遍歷子虛擬DOM元素數(shù)組,為每個虛擬DOM元素創(chuàng)建子fiber
while (newChildIndex < newChildren.length) {
let newChild = newChildren[newChildIndex]
let tag
// 打tag,定義 fiber類型
if (newChild.type === ELEMENT_TEXT) { // 這是文本節(jié)點
tag = TAG_TEXT
} else if (typeof newChild.type === 'string') { // 如果type是字符串,則是原生DOM節(jié)點
tag = TAG_HOST
}
let newFiber = {
tag,
type: newChild.type,
props: newChild.props,
stateNode: null, // 還未創(chuàng)建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++
}
}該方法會收集 fiber 節(jié)點下所有的副作用,并組成effect list。每個 fiber 有兩個屬性:
- firstEffect:指向第一個有副作用的子fiber。
- lastEffect:指向最后一個有副作用的子fiber。
而我們需要收集的就是中間nextEffect,最終形成一個單鏈表。
// 在完成的時候要收集有副作用的fiber,組成effect list
const completeUnitOfWork = (currentFiber) => {
// 后續(xù)遍歷,兒子們完成之后,自己才能完成。最后會得到以上圖中的鏈條結(jié)構(gòu)。
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
}
}
}最后,再定義一個遞歸函數(shù),從根節(jié)點出發(fā),把全部的 fiber 節(jié)點遍歷一遍,最終產(chǎn)出一個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階段
commit 階段需要將上階段計算出來的需要處理的副作用一次性執(zhí)行,此階段不能暫停,否則會出現(xiàn)UI更新不連續(xù)的現(xiàn)象。此階段需要根據(jù)effect list,將所有更新都 commit 到DOM樹上。
3.2.1 根據(jù)effect list 更新視圖
此階段,根據(jù)一個 fiber 的effect list列表去更新視圖,此次只列舉了新增節(jié)點、刪除節(jié)點、更新節(jié)點的三種操作 。
const commitWork = currentFiber => {
if (!currentFiber) return
let returnFiber = currentFiber.return
let returnDOM = returnFiber.stateNode // 父節(jié)點元素
if (currentFiber.effectTag === INSERT) { // 如果當前fiber的effectTag標識位INSERT,則代表其是需要插入的節(jié)點
returnDOM.appendChild(currentFiber.stateNode)
} else if (currentFiber.effectTag === DELETE) { // 如果當前fiber的effectTag標識位DELETE,則代表其是需要刪除的節(jié)點
returnDOM.removeChild(currentFiber.stateNode)
} else if (currentFiber.effectTag === UPDATE) { // 如果當前fiber的effectTag標識位UPDATE,則代表其是需要更新的節(jié)點
if (currentFiber.type === ELEMENT_TEXT) {
if (currentFiber.alternate.props.text !== currentFiber.props.text) {
currentFiber.stateNode.textContent = currentFiber.props.text
}
}
}
currentFiber.effectTag = null
}寫一個遞歸函數(shù),從根節(jié)點出發(fā),根據(jù)effect list完成全部更新。
/**
* 根據(jù)一個 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 視圖更新
接下來,就是循環(huán)執(zhí)行工作,當計算完成每個 fiber 的effect list后,調(diào)用 commitRoot 完成視圖更新。
const workloop = (deadline) => {
let shouldYield = false // 是否需要讓出控制權
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
shouldYield = deadline.timeRemaining() < 1 // 如果執(zhí)行完任務后,剩余時間小于1ms,則需要讓出控制權給瀏覽器
}
if (!nextUnitOfWork && workInProgressRoot) {
console.log('render階段結(jié)束')
commitRoot() // 沒有下一個任務了,根據(jù)effect list結(jié)果批量更新視圖
}
// 請求瀏覽器進行再次調(diào)度
requestIdleCallback(workloop, { timeout: 1000 })
}到此,根據(jù)收集到的變更信息完成了視圖的刷新操作,F(xiàn)iber的整個刷新流程也就實現(xiàn)了。
四、總結(jié)
相比傳統(tǒng)的Stack架構(gòu),F(xiàn)iber 將工作劃分為多個工作單元,每個工作單元在執(zhí)行完成后依據(jù)剩余時間決定是否讓出控制權給瀏覽器執(zhí)行渲染。 并且它設置每個工作單元的優(yōu)先級,暫停、重用和中止工作單元。 每個Fiber節(jié)點都是fiber tree上的一個節(jié)點,通過子、兄弟和返回引用連接,形成一個完整的fiber tree。
到此這篇關于React Fiber架構(gòu)原理剖析的文章就介紹到這了,更多相關React Fiber原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
關于react-router/react-router-dom v4 history不能訪問問題的解決
這篇文章主要給大家介紹了關于react-router/react-router-dom v4 history不能訪問問題的解決方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧。2018-01-01
React-router4路由監(jiān)聽的實現(xiàn)
這篇文章主要介紹了React-router4路由監(jiān)聽的實現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08
ES6 class類鏈式繼承,實例化及react super(props)原理詳解
這篇文章主要介紹了ES6 class類鏈式繼承,實例化及react super(props)原理,結(jié)合實例形式詳細分析了ES6 中class類鏈式繼承,實例化及react super(props)原理相關概念、原理、定義與使用技巧,需要的朋友可以參考下2020-02-02
React18從0實現(xiàn)dispatch?update流程
這篇文章主要為大家介紹了React18從0實現(xiàn)dispatch?update流程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01
react實現(xiàn)同頁面三級跳轉(zhuǎn)路由布局
這篇文章主要為大家詳細介紹了react實現(xiàn)同頁面三級跳轉(zhuǎn)路由布局,一個路由小案例,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-09-09

