詳解React?Fiber架構原理
一、概述
在 React 16 之前,VirtualDOM 的更新采用的是Stack架構實現(xiàn)的,也就是循環(huán)遞歸方式。不過,這種對比方式有明顯的缺陷,就是一旦任務開始進行就無法中斷,如果遇到應用中組件數(shù)量比較龐大,那么VirtualDOM 的層級就會比較深,帶來的結果就是主線程被長期占用,進而阻塞渲染、造成卡頓現(xiàn)象。
為了避免出現(xiàn)卡頓等問題,我們必須保障在執(zhí)行更新操作時計算時不能超過16ms,如果超過16ms,就需要先暫停,讓給瀏覽器進行渲染,后續(xù)再繼續(xù)執(zhí)行更新計算。而Fiber架構就是為了支持“可中斷渲染”而創(chuàng)建的。
在React中,F(xiàn)iber使用了一種新的數(shù)據(jù)結構fiber tree,它可以把虛擬dom tree轉換成一個鏈表,然后再執(zhí)行遍歷操作,而鏈表在執(zhí)行遍歷操作時是支持斷點重啟的,示意圖如下。
二、Fiber架構
2.1 執(zhí)行單元
官方介紹中,F(xiàn)iber 被理解為是一種數(shù)據(jù)結構,但是我們也可以將它理解為是一個執(zhí)行單元。
Fiber 可以理解為一個執(zhí)行單元,每次執(zhí)行完一個執(zhí)行單元,React Fiber就會檢查還剩多少時間,如果沒有時間則將控制權讓出去,然后由瀏覽器執(zhí)行渲染操作。React Fiber 與瀏覽器的交互流程如下圖。
可以看到,React 首先向瀏覽器請求調度,瀏覽器在執(zhí)行完一幀后如果還有空閑時間,會去判斷是否存在待執(zhí)行任務,不存在就直接將控制權交給瀏覽器;如果存在就會執(zhí)行對應的任務,執(zhí)行完一個新的任務單元之后會繼續(xù)判斷是否還有時間,有時間且有待執(zhí)行任務則會繼續(xù)執(zhí)行下一個任務,否則將控制權交給瀏覽器執(zhí)行渲染,這個流程是循環(huán)進行的。
所以,我們可以將Fiber 理解為一個執(zhí)行單元,并且這個執(zhí)行單元必須是一次完成的,不能出現(xiàn)暫停。并且,這個小的執(zhí)行單元在執(zhí)行完后計算之后,可以移交控制權給瀏覽器去響應用戶,從而提升了渲染的效率。
2.2 數(shù)據(jù)結構
在官方的文檔中,F(xiàn)iber 被解釋為是一種數(shù)據(jù)結構,即鏈表結構。在鏈表結構中,每個 Virtual DOM 都可以表示為一個 fiber,如下圖所示。
通常,一個 fiber包括了 child(第一個子節(jié)點)、sibling(兄弟節(jié)點)、return(父節(jié)點)等屬性,React Fiber 機制的實現(xiàn),就是依賴于上面的數(shù)據(jù)結構。
2.3 Fiber鏈表結構
通過介紹,我們知道Fiber使用的是鏈表結構,準確的說是單鏈表樹結構,詳見ReactFiber.js源碼。為了放便理解 Fiber 的遍歷過程,下面我們就看下Fiber鏈表結構。
在上面的例子中,每一個單元都包含了payload(數(shù)據(jù))和nextUpdate(指向下一個單元的指針)兩個元素,定義結構如下:
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é)點單元時需要考慮是否已經存在節(jié)點,如果不存在直接將firstUpdate、lastUpdate指向此節(jié)點即可。更新隊列是遍歷這個鏈表,根據(jù)payload中的內容去更新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ù)結構如下:
{ type: any, //對于類組件,它指向構造函數(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)更新,回調函數(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來構成一個樹鏈表。
其他的屬性還有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 ,它要求瀏覽器在下次重繪之前(即下一幀)調用指定的回調函數(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>
運行上面的代碼,就可以看到瀏覽器會在每一幀運行結束后,將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)先級不高,如果每一幀內存在空閑時間,就可以執(zhí)行注冊的這個任務。另外,開發(fā)者是可以傳入timeout參數(shù)去定義超時時間的,如果到了超時時間,那么瀏覽器必須立即執(zhí)行,使用方法如下:
window.requestIdleCallback(callback, { timeout: 1000 })
瀏覽器執(zhí)行完方法后,如果沒有剩余時間了,或者已經沒有下一個可執(zhí)行的任務了,React應該歸還控制權,并同樣使用requestIdleCallback去申請下一個時間片。具體的流程如下圖:
其中,requestIdleCallback的callback中會接收到默認參數(shù) deadline ,其中包含了以下兩個屬性:
- timeRamining:返回當前幀還剩多少時間供用戶使用。
- didTimeout:返回 callback 任務是否超時。
三、Fiber執(zhí)行流程
Fiber的執(zhí)行流程總體可以分為渲染和調度兩個階段,即render階段和commit 階段。其中,render 階段是可中斷的,需要找出所有節(jié)點的變更;而commit 階段是不可中斷的,只會執(zhí)行操作。
3.1 render階段
此階段的主要任務就是找出所有節(jié)點產生的變更,如節(jié)點的新增、刪除、屬性變更等。這些變更, React 統(tǒng)稱為副作用,此階段會構建一棵Fiber tree,以虛擬Dom節(jié)點的維度對任務進行拆分,即一個虛擬Dom節(jié)點對應一個任務,最后產出的結果是副作用列表(effect list)。
3.1.1 遍歷流程
在此階段,React Fiber會將虛擬DOM樹轉化為Fiber tree,這個Fiber tree是由節(jié)點構成的,每個節(jié)點都有child、sibling、return屬性,遍歷Fiber tree時采用的是后序遍歷方法,遍歷的流程如下:
從頂點開始遍歷;
如果有大兒子,先遍歷大兒子;如果沒有大兒子,則表示遍歷完成;
大兒子: a. 如果有弟弟,則返回弟弟,跳到2 b. 如果沒有弟弟,則返回父節(jié)點,并標志完成父節(jié)點遍歷,跳到2 d. 如果沒有父節(jié)點則標志遍歷結束
下面是后序遍歷的示意圖:
此時,樹結構的定義如下:
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 收集effect list
接下來,就是收集節(jié)點產生的變更,并將結果轉化成一個effect list,步驟如下:
- 如果當前節(jié)點需要更新,則打tag更新當前節(jié)點狀態(tài)(props, state, context等);
- 為每個子節(jié)點創(chuàng)建fiber。如果沒有產生child fiber,則結束該節(jié)點,把effect list歸并到return,把此節(jié)點的sibling節(jié)點作為下一個遍歷節(jié)點;否則把child節(jié)點作為下一個遍歷節(jié)點;
- 如果有剩余時間,則開始下一個節(jié)點,否則等下一次主線程空閑再開始下一個節(jié)點;
- 如果沒有下一個節(jié)點了,進入pendingCommit狀態(tài),此時effect list收集完畢,結束。
如果用代碼來實現(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ù)遍歷,兒子們完成之后,自己才能完成。最后會得到以上圖中的鏈條結構。 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é)點遍歷一遍,最終產出一個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后,調用 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階段結束') commitRoot() // 沒有下一個任務了,根據(jù)effect list結果批量更新視圖 } // 請求瀏覽器進行再次調度 requestIdleCallback(workloop, { timeout: 1000 }) }
到此,根據(jù)收集到的變更信息完成了視圖的刷新操作,F(xiàn)iber的整個刷新流程也就實現(xiàn)了。
四、總結
相比傳統(tǒng)的Stack架構,F(xiàn)iber 將工作劃分為多個工作單元,每個工作單元在執(zhí)行完成后依據(jù)剩余時間決定是否讓出控制權給瀏覽器執(zhí)行渲染。 并且它設置每個工作單元的優(yōu)先級,暫停、重用和中止工作單元。 每個Fiber節(jié)點都是fiber tree上的一個節(jié)點,通過子、兄弟和返回引用連接,形成一個完整的fiber tree。
到此這篇關于React Fiber架構原理剖析的文章就介紹到這了,更多相關React Fiber原理內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
關于react-router/react-router-dom v4 history不能訪問問題的解決
這篇文章主要給大家介紹了關于react-router/react-router-dom v4 history不能訪問問題的解決方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧。2018-01-01React-router4路由監(jiān)聽的實現(xiàn)
這篇文章主要介紹了React-router4路由監(jiān)聽的實現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08ES6 class類鏈式繼承,實例化及react super(props)原理詳解
這篇文章主要介紹了ES6 class類鏈式繼承,實例化及react super(props)原理,結合實例形式詳細分析了ES6 中class類鏈式繼承,實例化及react super(props)原理相關概念、原理、定義與使用技巧,需要的朋友可以參考下2020-02-02React18從0實現(xiàn)dispatch?update流程
這篇文章主要為大家介紹了React18從0實現(xiàn)dispatch?update流程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01