前言
從 React 16 開始属百,React 采用了 Fiber 機(jī)制替代了原先基于原生執(zhí)行棧遞歸遍歷 VDOM 的方案秃症,提高了頁面渲染性能和用戶體驗(yàn)候址。乍一聽 Fiber 好像挺神秘,在原生執(zhí)行棧都還沒搞懂的情況下种柑,又整出個 Fiber岗仑,還能不能愉快的寫代碼了。別慌聚请,老鐵荠雕!下面就來嘮嘮關(guān)于 Fiber 那點(diǎn)事兒。
什么是 Fiber
Fiber 的英文含義是“纖維”,它是比線程(Thread)更細(xì)的線舞虱,比線程(Thread)控制得更精密的執(zhí)行模型欢际。在廣義計算機(jī)科學(xué)概念中,F(xiàn)iber 又是一種協(xié)作的(Cooperative)編程模型矾兜,幫助開發(fā)者用一種【既模塊化又協(xié)作化】的方式來編排代碼损趋。
簡單點(diǎn)說,F(xiàn)iber 就是 React 16 實(shí)現(xiàn)的一套新的更新機(jī)制椅寺,讓 React 的更新過程變得可控浑槽,避免了之前一竿子遞歸到底影響性能的做法。
關(guān)于 Fiber 你需要知道的基礎(chǔ)知識
1 瀏覽器刷新率(幀)
頁面的內(nèi)容都是一幀一幀繪制出來的返帕,瀏覽器刷新率代表瀏覽器一秒繪制多少幀桐玻。目前瀏覽器大多是 60Hz(60幀/s),每一幀耗時也就是在 16ms 左右荆萤。原則上說 1s 內(nèi)繪制的幀數(shù)也多镊靴,畫面表現(xiàn)就也細(xì)膩。那么在這一幀的(16ms) 過程中瀏覽器又干了啥呢链韭?
通過上面這張圖可以清楚的知道偏竟,瀏覽器一幀會經(jīng)過下面這幾個過程:
接受輸入事件
執(zhí)行事件回調(diào)
開始一幀
執(zhí)行 RAF (RequestAnimationFrame)
頁面布局,樣式計算
渲染
執(zhí)行 RIC (RequestIdelCallback)
第七步的 RIC 事件不是每一幀結(jié)束都會執(zhí)行敞峭,只有在一幀的 16ms 中做完了前面 6 件事兒且還有剩余時間踊谋,才會執(zhí)行。這里提一下旋讹,如果一幀執(zhí)行結(jié)束后還有時間執(zhí)行 RIC 事件殖蚕,那么下一幀需要在事件執(zhí)行結(jié)束才能繼續(xù)渲染,所以 RIC 執(zhí)行不要超過 30ms沉迹,如果長時間不將控制權(quán)交還給瀏覽器睦疫,會影響下一幀的渲染,導(dǎo)致頁面出現(xiàn)卡頓和事件響應(yīng)不及時鞭呕。
2. JS 原生執(zhí)行棧
React Fiber 出現(xiàn)之前笼痛,React 通過原生執(zhí)行棧遞歸遍歷 VDOM。當(dāng)瀏覽器引擎第一次遇到 JS 代碼時琅拌,會產(chǎn)生一個全局執(zhí)行上下文并將其壓入執(zhí)行棧缨伊,接下來每遇到一個函數(shù)調(diào)用,又會往棧中壓入一個新的上下文进宝。比如:
function A(){
? B();
? C();
}
function B(){}
function C(){}
A();
引擎在執(zhí)行的時候刻坊,會形成如下這樣的執(zhí)行棧:?
瀏覽器引擎會從執(zhí)行棧的頂端開始執(zhí)行,執(zhí)行完畢就彈出當(dāng)前執(zhí)行上下文党晋,開始執(zhí)行下一個函數(shù)谭胚,直到執(zhí)行棧被清空才會停止徐块。然后將執(zhí)行權(quán)交還給瀏覽器。由于 React 將頁面視圖視作一個個函數(shù)執(zhí)行的結(jié)果灾而。每一個頁面往往由多個視圖組成胡控,這就意味著多個函數(shù)的調(diào)用。
如果一個頁面足夠復(fù)雜旁趟,形成的函數(shù)調(diào)用棧就會很深昼激。每一次更新,執(zhí)行棧需要一次性執(zhí)行完成锡搜,中途不能干其他的事兒橙困,只能"一心一意"。結(jié)合前面提到的瀏覽器刷新率耕餐,JS 一直執(zhí)行凡傅,瀏覽器得不到控制權(quán),就不能及時開始下一幀的繪制肠缔。如果這個時間超過 16ms夏跷,當(dāng)頁面有動畫效果需求時,動畫因?yàn)闉g覽器不能及時繪制下一幀明未,這時動畫就會出現(xiàn)卡頓槽华。不僅如此,因?yàn)槭录憫?yīng)代碼是在每一幀開始的時候執(zhí)行亚隅,如果不能及時繪制下一幀硼莽,事件響應(yīng)也會延遲庶溶。
3. 時間分片(Time Slicing)
時間分片指的是一種將多個粒度小的任務(wù)放入一個時間切片(一幀)中執(zhí)行的一種方案煮纵,在 React Fiber 中就是將多個任務(wù)放在了一個時間片中去執(zhí)行。
4. 鏈表
在 React Fiber 中用鏈表遍歷的方式替代了 React 16 之前的棧遞歸方案偏螺。在 React 16 中使用了大量的鏈表行疏。例如:
使用多向鏈表的形式替代了原來的樹結(jié)構(gòu)
例如下面這個組件:
<div id="id">
? A1
<div id="B1">
? ? B1
<div id="C1"></div>
</div>
<div id="B2">
? ? ? B2
</div>
</div>
會使用下面這樣的鏈表表示:?
副作用單鏈表
狀態(tài)更新單鏈表
...
鏈表是一種簡單高效的數(shù)據(jù)結(jié)構(gòu),它在當(dāng)前節(jié)點(diǎn)中保存著指向下一個節(jié)點(diǎn)的指針套像,就好像火車一樣一節(jié)連著一節(jié)
遍歷的時候酿联,通過操作指針找到下一個元素。但是操作指針時(調(diào)整順序和指向)一定要小心夺巩。
鏈表相比順序結(jié)構(gòu)數(shù)據(jù)格式的好處就是:
操作更高效贞让,比如順序調(diào)整、刪除柳譬,只需要改變節(jié)點(diǎn)的指針指向就好了喳张。
不僅可以根據(jù)當(dāng)前節(jié)點(diǎn)找到下一個節(jié)點(diǎn),在多向鏈表中美澳,還可以找到他的父節(jié)點(diǎn)或者兄弟節(jié)點(diǎn)销部。
但鏈表也不是完美的摸航,缺點(diǎn)就是:
比順序結(jié)構(gòu)數(shù)據(jù)更占用空間,因?yàn)槊總€節(jié)點(diǎn)對象還保存有指向下一個對象的指針舅桩。
不能自由讀取酱虎,必須找到他的上一個節(jié)點(diǎn)。
React 用空間換時間擂涛,更高效的操作可以方便根據(jù)優(yōu)先級進(jìn)行操作读串。同時可以根據(jù)當(dāng)前節(jié)點(diǎn)找到其他節(jié)點(diǎn),在下面提到的掛起和恢復(fù)過程中起到了關(guān)鍵作用歼指。
React Fiber 是如何實(shí)現(xiàn)更新過程可控爹土?
前面講完基本知識,現(xiàn)在正式開始介紹今天的主角 Fiber踩身,看看 React Fiber 是如何實(shí)現(xiàn)對更新過程的管控胀茵。
更新過程的可控主要體現(xiàn)在下面幾個方面:
任務(wù)拆分
任務(wù)掛起棺蛛、恢復(fù)正罢、終止
任務(wù)具備優(yōu)先級
1. 任務(wù)拆分
前面提到,React Fiber 之前是基于原生執(zhí)行棧攀芯,每一次更新操作會一直占用主線程附鸽,直到更新完成脱拼。這可能會導(dǎo)致事件響應(yīng)延遲,動畫卡頓等現(xiàn)象坷备。
在 React Fiber 機(jī)制中熄浓,它采用"化整為零"的戰(zhàn)術(shù),將調(diào)和階段(Reconciler)遞歸遍歷 VDOM 這個大任務(wù)分成若干小任務(wù)省撑,每個任務(wù)只負(fù)責(zé)一個節(jié)點(diǎn)的處理赌蔑。例如:
importReactfrom"react";
importReactDomfrom"react-dom"
constjsx = (
<div id="A1">
? ? A1
? ? <div id="B1">
? ? ? B1
? ? ? <div id="C1">C1</div>
? ? ? <div id="C2">C2</div>
? ? </div>
? ? <div id="B2">B2</div>
? </div>
)
ReactDom.render(jsx,document.getElementById("root"))
這個組件在渲染的時候會被分成八個小任務(wù),每個任務(wù)用來分別處理 A1(div)竟秫、A1(text)娃惯、B1(div)、B1(text)肥败、C1(div)趾浅、C1(text)、C2(div)馒稍、C2(text)皿哨、B2(div)、B2(text)纽谒。再通過時間分片证膨,在一個時間片中執(zhí)行一個或者多個任務(wù)。這里提一下佛舱,所有的小任務(wù)并不是一次性被切分完成椎例,而是處理當(dāng)前任務(wù)的時候生成下一個任務(wù)挨决,如果沒有下一個任務(wù)生成了,就代表本次渲染的 Diff 操作完成订歪。
. 掛起脖祈、恢復(fù)、終止
再說掛起刷晋、恢復(fù)盖高、終止之前,不得不提兩棵 Fiber 樹眼虱,workInProgress tree 和 currentFiber tree喻奥。
workInProgress 代表當(dāng)前正在執(zhí)行更新的 Fiber 樹。在 render 或者 setState 后捏悬,會構(gòu)建一顆 Fiber 樹撞蚕,也就是 workInProgress tree,這棵樹在構(gòu)建每一個節(jié)點(diǎn)的時候會收集當(dāng)前節(jié)點(diǎn)的副作用过牙,整棵樹構(gòu)建完成后甥厦,會形成一條完整的副作用鏈。
currentFiber 表示上次渲染構(gòu)建的 Filber 樹寇钉。在每一次更新完成后 workInProgress 會賦值給 currentFiber刀疙。在新一輪更新時 workInProgress tree 再重新構(gòu)建,新 workInProgress 的節(jié)點(diǎn)通過 alternate 屬性和 currentFiber 的節(jié)點(diǎn)建立聯(lián)系扫倡。
在新 workInProgress tree 的創(chuàng)建過程中谦秧,會同 currentFiber 的對應(yīng)節(jié)點(diǎn)進(jìn)行 Diff 比較,收集副作用撵溃。同時也會復(fù)用和 currentFiber 對應(yīng)的節(jié)點(diǎn)對象疚鲤,減少新創(chuàng)建對象帶來的開銷。也就是說無論是創(chuàng)建還是更新征懈,掛起石咬、恢復(fù)以及終止操作都是發(fā)生在 workInProgress tree 創(chuàng)建過程中揩悄。workInProgress tree 構(gòu)建過程其實(shí)就是循環(huán)的執(zhí)行任務(wù)和創(chuàng)建下一個任務(wù)卖哎,大致過程如下:
當(dāng)沒有下一個任務(wù)需要執(zhí)行的時候,workInProgress tree 構(gòu)建完成删性,開始進(jìn)入提交階段亏娜,完成真實(shí) DOM 更新。
在構(gòu)建 workInProgressFiber tree 過程中可以通過掛起蹬挺、恢復(fù)和終止任務(wù)维贺,實(shí)現(xiàn)對更新過程的管控。下面簡化了一下源碼巴帮,大致實(shí)現(xiàn)如下:
letnextUnitWork =null;//下一個執(zhí)行單元
//開始調(diào)度
function shceduler(task){
? ? nextUnitWork = task;
}
//循環(huán)執(zhí)行工作
function workLoop(deadline){
letshouldYield =false;//是否要讓出時間片交出控制權(quán)
while(nextUnitWork && !shouldYield){
? ? nextUnitWork = performUnitWork(nextUnitWork)
shouldYield = deadline.timeRemaining()<1// 沒有時間了溯泣,檢出控制權(quán)給瀏覽器
? }
if(!nextUnitWork) {
conosle.log("所有任務(wù)完成")
//commitRoot() //提交更新視圖
? }
// 如果還有任務(wù)虐秋,但是交出控制權(quán)后,請求下次調(diào)度
requestIdleCallback(workLoop,{timeout:5000})
}
/*
* 處理一個小任務(wù),其實(shí)就是一個 Fiber 節(jié)點(diǎn)垃沦,如果還有任務(wù)就返回下一個需要處理的任務(wù)客给,沒有就代表整個
*/
function performUnitWork(currentFiber){
? ....
? return FiberNode
}
掛起
當(dāng)?shù)谝粋€小任務(wù)完成后,先判斷這一幀是否還有空閑時間肢簿,沒有就掛起下一個任務(wù)的執(zhí)行靶剑,記住當(dāng)前掛起的節(jié)點(diǎn),讓出控制權(quán)給瀏覽器執(zhí)行更高優(yōu)先級的任務(wù)池充。
恢復(fù)
在瀏覽器渲染完一幀后桩引,判斷當(dāng)前幀是否有剩余時間,如果有就恢復(fù)執(zhí)行之前掛起的任務(wù)收夸。如果沒有任務(wù)需要處理坑匠,代表調(diào)和階段完成,可以開始進(jìn)入渲染階段卧惜。這樣完美的解決了調(diào)和過程一直占用主線程的問題笛辟。
那么問題來了他是如何判斷一幀是否有空閑時間的呢?答案就是我們前面提到的 RIC (RequestIdleCallback) 瀏覽器原生 API序苏,React 源碼中為了兼容低版本的瀏覽器手幢,對該方法進(jìn)行了 Polyfill。
當(dāng)恢復(fù)執(zhí)行的時候又是如何知道下一個任務(wù)是什么呢忱详?答案在前面提到的鏈表围来。在 React Fiber 中每個任務(wù)其實(shí)就是在處理一個 FiberNode 對象,然后又生成下一個任務(wù)需要處理的 FiberNode匈睁。順便提一嘴监透,這里提到的FiberNode 是一種數(shù)據(jù)格式,下面是它沒有開美顏的樣子:
class FiberNode {
constructor(tag, pendingProps, key, mode) {
// 實(shí)例屬性
this.tag = tag;// 標(biāo)記不同組件類型航唆,如函數(shù)組件胀蛮、類組件、文本糯钙、原生組件...
this.key = key;// react 元素上的 key 就是 jsx 上寫的那個 key 粪狼,也就是最終 ReactElement 上的
this.elementType =null;// createElement的第一個參數(shù),ReactElement 上的 type
this.type =null;// 表示fiber的真實(shí)類型 任岸,elementType 基本一樣再榄,在使用了懶加載之類的功能時可能會不一樣
this.stateNode =null;// 實(shí)例對象,比如 class 組件 new 完后就掛載在這個屬性上面享潜,如果是RootFiber困鸥,那么它上面掛的是 FiberRoot,如果是原生節(jié)點(diǎn)就是 dom 對象
// fiber
this.return =null;// 父節(jié)點(diǎn),指向上一個 fiber
this.child =null;// 子節(jié)點(diǎn)剑按,指向自身下面的第一個 fiber
this.sibling =null;// 兄弟組件, 指向一個兄弟節(jié)點(diǎn)
this.index =0;//? 一般如果沒有兄弟節(jié)點(diǎn)的話是0 當(dāng)某個父節(jié)點(diǎn)下的子節(jié)點(diǎn)是數(shù)組類型的時候會給每個子節(jié)點(diǎn)一個 index疾就,index 和 key 要一起做 diff
this.ref =null;// reactElement 上的 ref 屬性
this.pendingProps = pendingProps;// 新的 props
this.memoizedProps =null;// 舊的 props
this.updateQueue =null;// fiber 上的更新隊列執(zhí)行一次 setState 就會往這個屬性上掛一個新的更新, 每條更新最終會形成一個鏈表結(jié)構(gòu)澜术,最后做批量更新
this.memoizedState =null;// 對應(yīng)? memoizedProps,上次渲染的 state猬腰,相當(dāng)于當(dāng)前的 state瘪板,理解成 prev 和 next 的關(guān)系
this.mode = mode;// 表示當(dāng)前組件下的子組件的渲染方式
// effects
this.effectTag = NoEffect;// 表示當(dāng)前 fiber 要進(jìn)行何種更新
this.nextEffect =null;// 指向下個需要更新的fiber
this.firstEffect =null;// 指向所有子節(jié)點(diǎn)里,需要更新的 fiber 里的第一個
this.lastEffect =null;// 指向所有子節(jié)點(diǎn)中需要更新的 fiber 的最后一個
this.expirationTime = NoWork;// 過期時間漆诽,代表任務(wù)在未來的哪個時間點(diǎn)應(yīng)該被完成
this.childExpirationTime = NoWork;// child 過期時間
this.alternate =null;// current 樹和 workInprogress 樹之間的相互引用
? }
}
額…看著好像有點(diǎn)上頭侮攀,這是開了美顏的樣子:
是不是好看多了?在每次循環(huán)的時候厢拭,找到下一個執(zhí)行需要處理的節(jié)點(diǎn)兰英。
function performUnitWork(currentFiber){
//beginWork(currentFiber) //找到兒子,并通過鏈表的方式掛到currentFiber上供鸠,每一偶兒子就找后面那個兄弟
//有兒子就返回兒子
if(currentFiber.child){
returncurrentFiber.child;
? }
//如果沒有兒子畦贸,則找弟弟
while(currentFiber){//一直往上找
//completeUnitWork(currentFiber);//將自己的副作用掛到父節(jié)點(diǎn)去
if(currentFiber.sibling){
returncurrentFiber.sibling
? ? }
? ? currentFiber = currentFiber.return;
? }
}
在一次任務(wù)結(jié)束后返回該處理節(jié)點(diǎn)的子節(jié)點(diǎn)或兄弟節(jié)點(diǎn)或父節(jié)點(diǎn)。只要有節(jié)點(diǎn)返回楞捂,說明還有下一個任務(wù)薄坏,下一個任務(wù)的處理對象就是返回的節(jié)點(diǎn)。通過一個全局變量記住當(dāng)前任務(wù)節(jié)點(diǎn)寨闹,當(dāng)瀏覽器再次空閑的時候胶坠,通過這個全局變量,找到它的下一個任務(wù)需要處理的節(jié)點(diǎn)恢復(fù)執(zhí)行繁堡。就這樣一直循環(huán)下去沈善,直到?jīng)]有需要處理的節(jié)點(diǎn)返回,代表所有任務(wù)執(zhí)行完成椭蹄。最后大家手拉手闻牡,就形成了一顆 Fiber 樹。
終止
其實(shí)并不是每次更新都會走到提交階段绳矩。當(dāng)在調(diào)和過程中觸發(fā)了新的更新罩润,在執(zhí)行下一個任務(wù)的時候,判斷是否有優(yōu)先級更高的執(zhí)行任務(wù)翼馆,如果有就終止原來將要執(zhí)行的任務(wù)割以,開始新的 workInProgressFiber 樹構(gòu)建過程,開始新的更新流程写妥。這樣可以避免重復(fù)更新操作拳球。這也是在 React 16 以后生命周期函數(shù) componentWillMount 有可能會執(zhí)行多次的原因审姓。
3. 任務(wù)具備優(yōu)先級
React Fiber 除了通過掛起珍特,恢復(fù)和終止來控制更新外,還給每個任務(wù)分配了優(yōu)先級魔吐。具體點(diǎn)就是在創(chuàng)建或者更新 FiberNode 的時候扎筒,通過算法給每個任務(wù)分配一個到期時間(expirationTime)莱找。在每個任務(wù)執(zhí)行的時候除了判斷剩余時間,如果當(dāng)前處理節(jié)點(diǎn)已經(jīng)過期嗜桌,那么無論現(xiàn)在是否有空閑時間都必須執(zhí)行改任務(wù)奥溺。
同時過期時間的大小還代表著任務(wù)的優(yōu)先級。
任務(wù)在執(zhí)行過程中順便收集了每個 FiberNode 的副作用骨宠,將有副作用的節(jié)點(diǎn)通過 firstEffect浮定、lastEffect、nextEffect 形成一條副作用單鏈表 AI(TEXT)-B1(TEXT)-C1(TEXT)-C1-C2(TEXT)-C2-B1-B2(TEXT)-B2-A层亿。
其實(shí)最終都是為了收集到這條副作用鏈表桦卒,有了它,在接下來的渲染階段就通過遍歷副作用鏈完成 DOM 更新匿又。這里需要注意方灾,更新真實(shí) DOM 的這個動作是一氣呵成的,不能中斷碌更,不然會造成視覺上的不連貫裕偿。