面試官:說說對React Fiber架構(gòu)的理解?解決了什么問題?
本文轉(zhuǎn)載自微信公眾號「JS每日一題」,作者灰灰。轉(zhuǎn)載本文請聯(lián)系JS每日一題公眾號。
一、問題
JavaScript引擎和頁面渲染引擎兩個線程是互斥的,當(dāng)其中一個線程執(zhí)行時,另一個線程只能掛起等待
如果 JavaScript 線程長時間地占用了主線程,那么渲染層面的更新就不得不長時間地等待,界面長時間不更新,會導(dǎo)致頁面響應(yīng)度變差,用戶可能會感覺到卡頓
而這也正是 React 15 的 Stack Reconciler所面臨的問題,當(dāng) React在渲染組件時,從開始到渲染完成整個過程是一氣呵成的,無法中斷
如果組件較大,那么js線程會一直執(zhí)行,然后等到整棵VDOM樹計算完成后,才會交給渲染的線程
這就會導(dǎo)致一些用戶交互、動畫等任務(wù)無法立即得到處理,導(dǎo)致卡頓的情況
二、是什么
React Fiber 是 Facebook 花費兩年余時間對 React 做出的一個重大改變與優(yōu)化,是對 React 核心算法的一次重新實現(xiàn)。從Facebook在 React Conf 2017 會議上確認(rèn),React Fiber 在React 16 版本發(fā)布
在react中,主要做了以下的操作:
- 為每個增加了優(yōu)先級,優(yōu)先級高的任務(wù)可以中斷低優(yōu)先級的任務(wù)。然后再重新,注意是重新執(zhí)行優(yōu)先級低的任務(wù)
- 增加了異步任務(wù),調(diào)用requestIdleCallback api,瀏覽器空閑的時候執(zhí)行
- dom diff樹變成了鏈表,一個dom對應(yīng)兩個fiber(一個鏈表),對應(yīng)兩個隊列,這都是為找到被中斷的任務(wù),重新執(zhí)行
從架構(gòu)角度來看,F(xiàn)iber 是對 React核心算法(即調(diào)和過程)的重寫
從編碼角度來看,F(xiàn)iber是 React內(nèi)部所定義的一種數(shù)據(jù)結(jié)構(gòu),它是 Fiber樹結(jié)構(gòu)的節(jié)點單位,也就是 React 16 新架構(gòu)下的虛擬DOM
一個 fiber就是一個 JavaScript對象,包含了元素的信息、該元素的更新操作隊列、類型,其數(shù)據(jù)結(jié)構(gòu)如下:
- type Fiber = {
- // 用于標(biāo)記fiber的WorkTag類型,主要表示當(dāng)前fiber代表的組件類型如FunctionComponent、ClassComponent等
- tag: WorkTag,
- // ReactElement里面的key
- key: null | string,
- // ReactElement.type,調(diào)用`createElement`的第一個參數(shù)
- elementType: any,
- // The resolved function/class/ associated with this fiber.
- // 表示當(dāng)前代表的節(jié)點類型
- type: any,
- // 表示當(dāng)前FiberNode對應(yīng)的element組件實例
- stateNode: any,
- // 指向他在Fiber節(jié)點樹中的`parent`,用來在處理完這個節(jié)點之后向上返回
- return: Fiber | null,
- // 指向自己的第一個子節(jié)點
- child: Fiber | null,
- // 指向自己的兄弟結(jié)構(gòu),兄弟節(jié)點的return指向同一個父節(jié)點
- sibling: Fiber | null,
- index: number,
- ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject,
- // 當(dāng)前處理過程中的組件props對象
- pendingProps: any,
- // 上一次渲染完成之后的props
- memoizedProps: any,
- // 該Fiber對應(yīng)的組件產(chǎn)生的Update會存放在這個隊列里面
- updateQueue: UpdateQueue<any> | null,
- // 上一次渲染的時候的state
- memoizedState: any,
- // 一個列表,存放這個Fiber依賴的context
- firstContextDependency: ContextDependency<mixed> | null,
- mode: TypeOfMode,
- // Effect
- // 用來記錄Side Effect
- effectTag: SideEffectTag,
- // 單鏈表用來快速查找下一個side effect
- nextEffect: Fiber | null,
- // 子樹中第一個side effect
- firstEffect: Fiber | null,
- // 子樹中最后一個side effect
- lastEffect: Fiber | null,
- // 代表任務(wù)在未來的哪個時間點應(yīng)該被完成,之后版本改名為 lanes
- expirationTime: ExpirationTime,
- // 快速確定子樹中是否有不在等待的變化
- childExpirationTime: ExpirationTime,
- // fiber的版本池,即記錄fiber更新過程,便于恢復(fù)
- alternate: Fiber | null,
- }
三、如何解決
Fiber把渲染更新過程拆分成多個子任務(wù),每次只做一小部分,做完看是否還有剩余時間,如果有繼續(xù)下一個任務(wù);如果沒有,掛起當(dāng)前任務(wù),將時間控制權(quán)交給主線程,等主線程不忙的時候在繼續(xù)執(zhí)行
即可以中斷與恢復(fù),恢復(fù)后也可以復(fù)用之前的中間狀態(tài),并給不同的任務(wù)賦予不同的優(yōu)先級,其中每個任務(wù)更新單元為 React Element 對應(yīng)的 Fiber節(jié)點
實現(xiàn)的上述方式的是requestIdleCallback方法
window.requestIdleCallback()方法將在瀏覽器的空閑時段內(nèi)調(diào)用的函數(shù)排隊。這使開發(fā)者能夠在主事件循環(huán)上執(zhí)行后臺和低優(yōu)先級工作,而不會影響延遲關(guān)鍵事件,如動畫和輸入響應(yīng)
首先 React 中任務(wù)切割為多個步驟,分批完成。在完成一部分任務(wù)之后,將控制權(quán)交回給瀏覽器,讓瀏覽器有時間再進(jìn)行頁面的渲染。等瀏覽器忙完之后有剩余時間,再繼續(xù)之前 React 未完成的任務(wù),是一種合作式調(diào)度。
該實現(xiàn)過程是基于 Fiber節(jié)點實現(xiàn),作為靜態(tài)的數(shù)據(jù)結(jié)構(gòu)來說,每個 Fiber 節(jié)點對應(yīng)一個 React element,保存了該組件的類型(函數(shù)組件/類組件/原生組件等等)、對應(yīng)的 DOM 節(jié)點等信息。
作為動態(tài)的工作單元來說,每個 Fiber 節(jié)點保存了本次更新中該組件改變的狀態(tài)、要執(zhí)行的工作。
每個 Fiber 節(jié)點有個對應(yīng)的 React element,多個 Fiber節(jié)點根據(jù)如下三個屬性構(gòu)建一顆樹:
- // 指向父級Fiber節(jié)點
- this.return = null
- // 指向子Fiber節(jié)點
- this.child = null
- // 指向右邊第一個兄弟Fiber節(jié)點
- this.sibling = null
通過這些屬性就能找到下一個執(zhí)行目標(biāo)
參考文獻(xiàn)
https://juejin.cn/post/6926432527980691470
https://zhuanlan.zhihu.com/p/137234573
https://vue3js.cn/interview