文章來(lái)源于公眾號(hào):前端時(shí)光屋 作者:小豪
Fiber出現(xiàn)的背景?
在早期的 React 版本中,也就是 React16.8 版本之前。
大量的同步計(jì)算任務(wù)
阻塞了瀏覽器的UI渲染。默認(rèn)情況下,JS運(yùn)算
、頁(yè)面布局
和頁(yè)面繪制渲染
都是運(yùn)行在瀏覽器的主線程
當(dāng)中,他們之間是互斥
的關(guān)系。
如果 JS 運(yùn)算持續(xù)占用主線程,頁(yè)面就沒(méi)法得到及時(shí)的更新,當(dāng)我們調(diào)用setState
更新頁(yè)面的時(shí)候,React 會(huì)遍歷應(yīng)用的所有節(jié)點(diǎn),與老的 dom 節(jié)點(diǎn)進(jìn)行 diff 算法的對(duì)比,最小代價(jià)更新頁(yè)面,即使
這樣,整個(gè)過(guò)程也是一氣呵成,不能被打斷
的,如果頁(yè)面元素很多,整個(gè)過(guò)程占用的時(shí)間就可能超過(guò)16毫秒,出現(xiàn)掉幀的現(xiàn)象。
針對(duì)這一現(xiàn)象,React 團(tuán)隊(duì)從框架層面對(duì) web 頁(yè)面的運(yùn)行機(jī)制做了優(yōu)化,此后,Fiber
誕生了。
說(shuō)到16ms,我們來(lái)看這樣的一個(gè)概念
屏幕刷新率
- 目前大多數(shù)設(shè)備的屏幕刷新率為60次/秒
- 瀏覽器的渲染動(dòng)畫(huà)或頁(yè)面的每一幀的速率也需要跟設(shè)備屏幕的刷新率保持一致。
- 頁(yè)面是一幀一幀繪制出來(lái)的,當(dāng)每秒繪制的幀數(shù)(FPS)達(dá)到60時(shí),頁(yè)面是流暢的,小于這個(gè)值時(shí),用戶會(huì)感覺(jué)到卡頓。
- 每個(gè)幀的預(yù)算時(shí)間是16.66毫秒(1秒/60)
- 1s 60幀,所以我們書(shū)寫(xiě)代碼時(shí)盡量不讓一幀的工作量超過(guò)16ms
Fiber的誕生
解決主線程長(zhǎng)時(shí)間被 JS 暈眩占用這一問(wèn)題的基本思路,是將運(yùn)算切割為多個(gè)步驟
,分批完成。也就是說(shuō)在完成一部分任務(wù)之后, 將控制權(quán)交回
給瀏覽器,讓瀏覽器有時(shí)間再進(jìn)行頁(yè)面的渲染。等瀏覽器忙完之后,再繼續(xù)之前React未完成的任務(wù)。
舊版 React 通過(guò)遞歸
的方式進(jìn)行渲染,使用的是 JS 引擎自身的函數(shù)調(diào)用棧,它會(huì)一直執(zhí)行到??諡橹?/strong>。
而Fiber
實(shí)現(xiàn)了自己的組件調(diào)用棧,它以鏈表的形式遍歷組件樹(shù),可以靈活地暫停、繼續(xù)和丟棄執(zhí)行的任務(wù)。實(shí)現(xiàn)的方式是使用了 瀏覽器的requestIdleCallback
這一 API。官方的解釋是這樣的:
window.requestIdleCallback()會(huì)在瀏覽器
空閑時(shí)期
依次調(diào)用函數(shù),這就可以讓開(kāi)發(fā)者在主事件循環(huán)
中執(zhí)行后臺(tái)
或優(yōu)先級(jí)低
的任務(wù),而且不會(huì)像對(duì)動(dòng)畫(huà)和用戶交互這些延遲觸發(fā)產(chǎn)生關(guān)鍵的事件影響。函數(shù)一般會(huì)按先進(jìn)先調(diào)用的順序執(zhí)行,除非函數(shù)在瀏覽器調(diào)用它之前就到了它的超時(shí)時(shí)間。
requestIdleCallback的核心用法
- 希望快速響應(yīng)用戶,讓用戶覺(jué)得夠快,不能阻塞用戶的交互行為
- requestIdleCallback 使開(kāi)發(fā)者能夠在
主事件循環(huán)
上執(zhí)行后臺(tái)和低優(yōu)先級(jí)
的工作,而不會(huì)影響延遲關(guān)鍵事件,例如動(dòng)畫(huà)和輸入的響應(yīng) - 正常幀任務(wù)完成后
沒(méi)超過(guò)16ms
,說(shuō)明時(shí)間有賦予,此時(shí)就會(huì)執(zhí)行requestIdleCallback
里注冊(cè)的任務(wù)
requestIdleCallback執(zhí)行流程
Fiber是什么
Fiber是一個(gè)執(zhí)行單元
Fiber 是一個(gè)執(zhí)行單元,每次執(zhí)行完一個(gè)執(zhí)行單元, React 就會(huì)檢查現(xiàn)在還剩多少時(shí)間,如果沒(méi)有時(shí)間就將控制權(quán)讓出去
Fiber是一種數(shù)據(jù)結(jié)構(gòu)
React 目前的做法是使用鏈表, 每個(gè) VirtualDOM 節(jié)點(diǎn)內(nèi)部表示為一個(gè)Fiber
,它可以用一個(gè) JS 對(duì)象來(lái)表示:
const fiber = {
stateNode, // 節(jié)點(diǎn)實(shí)例
child, // 子節(jié)點(diǎn)
sibling, // 兄弟節(jié)點(diǎn)
return, // 父節(jié)點(diǎn)
}
Fiber之前的協(xié)調(diào)階段
- React 會(huì)
遞歸比對(duì)
VirtualDOM樹(shù),找出需要變動(dòng)
的節(jié)點(diǎn),然后同步更新它們。這個(gè)過(guò)程 React 稱為Reconcilation(協(xié)調(diào)) - 在Reconcilation期間,React 會(huì)
一直占用
著瀏覽器資源,一則會(huì)導(dǎo)致用戶觸發(fā)的事件得不到響應(yīng), 二則會(huì)導(dǎo)致掉幀,用戶可能會(huì)感覺(jué)到卡頓
let root = {
key: 'A1',
children: [
{
key: 'B1',
children: [
{
key: 'C1',
children: []
},
{
key: 'C2',
children: []
}
]
},
{
key: 'B2',
children: []
}
]
}
function walk(element) {
doWork(element);
element.children.forEach(walk);
}
function doWork(element) {
console.log(element.key);
}
walk(root);
在 Fiber 出現(xiàn)之前, React 會(huì)不斷遞歸遍歷虛擬 DOM 節(jié)點(diǎn),占用著瀏覽器資源,積極地浪費(fèi)性能,造成卡頓現(xiàn)象,且協(xié)調(diào)階段是不能
被打斷的
。
Fiber 出現(xiàn)之后,通過(guò)某些 Fiber 調(diào)度策略合理分配 CPU 資源,讓自己的
協(xié)調(diào)階段變成可被終端
,適時(shí)
地讓 CPU(瀏覽器)執(zhí)行權(quán),提高了性能優(yōu)化。
Fiber執(zhí)行階段
每次渲染有兩個(gè)階段:Reconciliation(協(xié)調(diào)\render階段)和Commit(提交階段)
- 協(xié)調(diào)階段: 這個(gè)階段
可以被中斷
, 通過(guò)Dom-Diff算法找出所有節(jié)點(diǎn)變更,例如節(jié)點(diǎn)新增
、刪除
、屬性變更
等等, 這些變更React 稱之為副作用
(Effect) - 提交階段: 將上一個(gè)階段計(jì)算出來(lái)的需要處理的副作用(Effects)一次性執(zhí)行了。這個(gè)階段必須
同步
執(zhí)行,不能被打斷
簡(jiǎn)單理解的話
- 階段1:生成Fiber樹(shù),得出需要
更新
的節(jié)點(diǎn)信息
。(可打斷
) - 階段2:將需要更新的節(jié)點(diǎn)一次性地
批量更新
。(不可打斷
)
Fiber的協(xié)調(diào)階段,可以被優(yōu)先級(jí)較高的任務(wù)(如鍵盤(pán)輸入)打斷。
階段1可被打斷的特性,讓優(yōu)先級(jí)更高的任務(wù)先執(zhí)行
,從框架層面大大降低了頁(yè)面掉幀的概率。
Fiber執(zhí)行流程
render階段
Fiber Reconciliation(協(xié)調(diào)) 在階段一進(jìn)行 Diff 計(jì)算的時(shí)候,會(huì)生成一棵 Fiber 樹(shù)
。這棵樹(shù)是在 Virtual DOM 樹(shù)
的基礎(chǔ)上增加額外的信息生成
來(lái)的,它本質(zhì)來(lái)說(shuō)是一個(gè)鏈表。
commit提交階段
Fiber 樹(shù)在首次渲染的時(shí)候會(huì)一次過(guò)生成。在后續(xù)
需要 Diff
的時(shí)候,會(huì)根據(jù)已有樹(shù)和最新 Virtual DOM 的信息,生成一棵新的樹(shù)。這顆新樹(shù)每生成一個(gè)新的節(jié)點(diǎn),都會(huì)將控制權(quán)交回給主線程,去檢查有沒(méi)有優(yōu)先級(jí)更高的任務(wù)需要執(zhí)行。如果沒(méi)有,則繼續(xù)構(gòu)建樹(shù)的過(guò)程。
1.如果過(guò)程中有優(yōu)先級(jí)更高的任務(wù)需要進(jìn)行,則 Fiber Reconciler 會(huì)丟棄正在生成的樹(shù),在空閑的時(shí)候再重新執(zhí)行一遍。
2.在構(gòu)造 Fiber 樹(shù)的過(guò)程中,F(xiàn)iber Reconciler 會(huì)將需要更新的節(jié)點(diǎn)信息保存在Effect List
當(dāng)中,在階段二執(zhí)行的時(shí)候,會(huì)批量更新相應(yīng)的節(jié)點(diǎn)。
細(xì)節(jié)拓展
render階段是如何遍歷,生成Fiber樹(shù)的?
<div>
</div>
- 從頂點(diǎn)開(kāi)始遍歷
- 如果有第一個(gè)兒子,先遍歷第一個(gè)兒子
- 如果沒(méi)有第一個(gè)兒子,標(biāo)志著此節(jié)點(diǎn)遍歷完成
- 如果有弟弟遍歷弟弟
- 如果有沒(méi)有下一個(gè)弟弟,返回父節(jié)點(diǎn)標(biāo)識(shí)完成父節(jié)點(diǎn)遍歷,如果有叔叔遍歷叔叔
- 沒(méi)有父節(jié)點(diǎn)遍歷結(jié)束
commit階段,是如何commit的?
類比 Git 分支功能,從舊樹(shù)中 fork 出來(lái)一份,在新分支進(jìn)行添加、刪除和更新操作,經(jīng)過(guò)測(cè)試后進(jìn)行提交。
以上就是W3Cschool編程獅
關(guān)于你也可以理解的React Fiber,學(xué)廢了嗎的相關(guān)介紹了,希望對(duì)大家有所幫助。