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