隨著 React 16 的發(fā)布,Hooks 的正式上線,很多小伙伴都很興奮,都想要嘗試這一新的特性,升級(jí) React 的意愿越來(lái)越強(qiáng)烈了。
我們都知道 React 是一個(gè)優(yōu)秀的前端框架,很多的大型應(yīng)用都在使用,而作為使用 React 為工具的開(kāi)發(fā)者也應(yīng)該了解下 React Fiber,F(xiàn)iber 到底是什么?它能給我們帶來(lái)什么?以及 React 團(tuán)隊(duì)為什么要去重寫(xiě) Fiber 架構(gòu)?
什么是 React Fiber ?
React Fiber 是 React 16 中新的協(xié)調(diào)引擎,是對(duì)核心算法的一次重新實(shí)現(xiàn)。
既然是新的,那 React 團(tuán)隊(duì)為什么要重寫(xiě)新的 Fiber 架構(gòu)呢?
在 React 16 以前,當(dāng)元素較多,需要頻繁刷新的時(shí)候頁(yè)面會(huì)出現(xiàn)卡頓,究其原因是因?yàn)楦逻^(guò)程是同步的,大量的同步計(jì)算任務(wù)阻塞了瀏覽器的渲染。
當(dāng)頁(yè)面加載或者更新時(shí),React 會(huì)去計(jì)算和比對(duì) Virtual DOM,最后繪制頁(yè)面,整個(gè)過(guò)程是同步進(jìn)行的。當(dāng) JavaScript 在瀏覽器的主線程上長(zhǎng)期運(yùn)行,就會(huì)阻塞了樣式計(jì)算、布局和繪制,導(dǎo)致頁(yè)面無(wú)法得到及時(shí)的更新和響應(yīng)。此時(shí),無(wú)論用戶如何點(diǎn)擊鼠標(biāo)或者敲擊鍵盤(pán)都不會(huì)得到響應(yīng),當(dāng) React 更新完成后剛剛點(diǎn)擊或敲擊的事件才會(huì)得到響應(yīng)。
由于 JavaScript 是單線程的特點(diǎn),所以一個(gè)線程執(zhí)行完成后才會(huì)執(zhí)行下一個(gè)線程,當(dāng)上一個(gè)線程任務(wù)耗時(shí)太長(zhǎng),程序就會(huì)對(duì)其他輸入不作出響應(yīng)。
React 的更新過(guò)程會(huì)先計(jì)算,一旦任務(wù)開(kāi)始進(jìn)行,就無(wú)法中斷, js 將一直占用主線程, 直到整棵 Virtual DOM 樹(shù)計(jì)算完成之后,才能把執(zhí)行權(quán)交給渲染引擎,而 React Fiber 就是要改變現(xiàn)狀。

它能給我們帶來(lái)什么?
增量渲染
為不同任務(wù)分配優(yōu)先極
更新時(shí)能暫停、終止、復(fù)用渲染任務(wù)
并發(fā)方面新的能力
接下來(lái),讓我們具體來(lái)了解下 Fiber。
Fiber
Fiber 把耗時(shí)長(zhǎng)的任務(wù)拆分成很多的小片,每個(gè)小片的運(yùn)行時(shí)間很短,每次只執(zhí)行一個(gè)小片,執(zhí)行完后看是否還有剩余時(shí)間,如果有就繼續(xù)執(zhí)行下個(gè)小片,如果沒(méi)有就掛起當(dāng)前任務(wù),將控制權(quán)交給 React 負(fù)責(zé)任務(wù)協(xié)調(diào)的模塊,看有沒(méi)有其他緊急任務(wù)要做,如果沒(méi)有就繼續(xù)更新當(dāng)前任務(wù),如果有緊急任務(wù)就去做緊急任務(wù),等主線程不忙的時(shí)候在繼續(xù)執(zhí)行當(dāng)前任務(wù)。

分片之后,每執(zhí)行一段時(shí)間,都會(huì)將控制權(quán)交給主線程。
這樣唯一的線程不會(huì)被獨(dú)占,其他任務(wù)依然有運(yùn)行的機(jī)會(huì)。
這種策略叫做 Cooperative Scheduling(合作式調(diào)度),操作系統(tǒng)常用任務(wù)調(diào)度策略之一。
總而言之,我們了解到,F(xiàn)iber 是一個(gè)最小工作單元,也是堆棧的重新實(shí)現(xiàn),可以理解為是一個(gè)虛擬的堆棧幀。它將可中斷的任務(wù)拆分成多個(gè)任務(wù),通過(guò)優(yōu)先級(jí)來(lái)自由調(diào)度子任務(wù),分段更新,從而將之前的同步渲染改為異步渲染。
Fiber 結(jié)構(gòu)
維護(hù)每一個(gè)分片的數(shù)據(jù)結(jié)構(gòu),就是 Fiber,它可以用 JS 對(duì)象來(lái)表示,其中包含有關(guān)組件,以及輸入和輸出的信息:
const fiber = {
// 跟當(dāng)前 Fiber 相關(guān)本地狀態(tài)(比如瀏覽器環(huán)境就是 DOM 節(jié)點(diǎn))
stateNode: any, // 節(jié)點(diǎn)實(shí)例
// 指向他在 Fiber 節(jié)點(diǎn)樹(shù)中的 `parent`,用來(lái)在處理完這個(gè)節(jié)點(diǎn)之后向上返回
return: Fiber | null, // 父節(jié)點(diǎn)
// 單鏈表樹(shù)結(jié)構(gòu)
child: Fiber | null, // 指向自己的第一個(gè)子節(jié)點(diǎn)
sibling: Fiber | null, // 指向自己的兄弟結(jié)構(gòu),兄弟節(jié)點(diǎn)的return指向同一個(gè)父節(jié)點(diǎn)
index: number,
// 組件相關(guān)
tag: WorkTag, // 標(biāo)記不同的組件類型
key: null | string, // ReactElement 里面的 key,與 type 一起,主要用來(lái)在reconciliation 期間確定 Fiber 是否可重用。
elementType: any, // ReactElement.type,也就是我們調(diào)用 `createElement` 的第一個(gè)參數(shù)
type: any, // 對(duì)于復(fù)合組件,type 是函數(shù)或者是類組件(`function`或者`class`),對(duì)于標(biāo)準(zhǔn)組件(div或者span),type 是 string
// ref屬性
ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,
// 更新相關(guān)
// 當(dāng)傳入的 pendingProps 和 memoizedProps 相同的時(shí)候,表示 fiber 可以重新使用之前的 fiber,以避免重復(fù)的工作。
pendingProps: any, // 新的變動(dòng)帶來(lái)的新的props
memoizedProps: any, // 上一次渲染完成之后的props
updateQueue: UpdateQueue<any> | null, // 該Fiber對(duì)應(yīng)的組件產(chǎn)生的Update會(huì)存放在這個(gè)隊(duì)列里面
memoizedState: any, // 上一次渲染的時(shí)候的state
firstContextDependency: ContextDependency<mixed> | null, // 一個(gè)列表,存放這個(gè)Fiber依賴的context
pendingWorkPriority: number, // 待處理的工作優(yōu)先級(jí),除 NoWork 為0外,數(shù)字越大表示優(yōu)先級(jí)越低。
// Scheduler 相關(guān)
expirationTime: ExpirationTime, // 代表任務(wù)在未來(lái)的哪個(gè)時(shí)間點(diǎn)應(yīng)該被完成,不包括他的子樹(shù)產(chǎn)生的任務(wù)
// 快速確定子樹(shù)中是否有不在等待的變化
childExpirationTime: ExpirationTime,
// 用來(lái)描述當(dāng)前Fiber和他子樹(shù)的`Bitfield`
// 共存的模式表示這個(gè)子樹(shù)是否默認(rèn)是異步渲染的
// Fiber被創(chuàng)建的時(shí)候他會(huì)繼承父Fiber
// 其他的標(biāo)識(shí)也可以在創(chuàng)建的時(shí)候被設(shè)置
// 但是在創(chuàng)建之后不應(yīng)該再被修改,特別是他的子Fiber創(chuàng)建之前
mode: TypeOfMode,
// 在Fiber樹(shù)更新的過(guò)程中,每個(gè)Fiber都會(huì)有一個(gè)跟其對(duì)應(yīng)的Fiber
// 我們稱他為`current <==> workInProgress`
// 在渲染完成之后他們會(huì)交換位置
alternate: Fiber | null,
// Effect 相關(guān)的
effectTag: SideEffectTag, // 用來(lái)記錄Side Effect
nextEffect: Fiber | null, // 單鏈表用來(lái)快速查找下一個(gè)side effect
firstEffect: Fiber | null, // 子樹(shù)中第一個(gè)side effect
lastEffect: Fiber | null, // 子樹(shù)中最后一個(gè)side effect
}
任務(wù)的優(yōu)先級(jí)
上面講到了任務(wù)的執(zhí)行是根據(jù)優(yōu)先級(jí)來(lái)調(diào)度的,那我們現(xiàn)在具體了解一下優(yōu)先級(jí)。
synchronous 同步執(zhí)行,首屏使用
task 在 next tick 之前執(zhí)行
animation 下一幀之前執(zhí)行,通過(guò)requestAnimationFrame來(lái)調(diào)度,這樣在下一幀就能立即開(kāi)始動(dòng)畫(huà)過(guò)程
high 在不久的將來(lái)立即執(zhí)行
low 稍微延遲(100-200ms)執(zhí)行也沒(méi)關(guān)系
offscreen 當(dāng)前隱藏的、屏幕外的(看不見(jiàn)的)元素,在下一次 render 時(shí)或 scroll 時(shí)才執(zhí)行
Fiber reconciler
Fiber Reconciler 決定了當(dāng)任務(wù)調(diào)度完成之后,如何去執(zhí)行每個(gè)任務(wù),如何去更新每一個(gè)節(jié)點(diǎn)的過(guò)程。
React Fiber 更新過(guò)程分為兩個(gè)階段
第一階段 Reconciliation Phase,生成 Fiber 樹(shù),得出需要更新的節(jié)點(diǎn)信息。是一個(gè)漸進(jìn)的過(guò)程,可以被打斷。可能會(huì)調(diào)用 componentWillMount,componentWillReceiveProps,shouldComponentUpdate,componentWillUpdate 生命周期函數(shù)。
第二階段 Commit Phase,將需要更新的節(jié)點(diǎn)批量更新,這個(gè)過(guò)程不能被打斷??赡軙?huì)調(diào)用 componentDidMount,componentDidUpdate,componentWillUnmount 生命周期函數(shù)。

Fiber tree 與 WorkInProgress Tree
// 單鏈表樹(shù)結(jié)構(gòu)
{
return: Fiber | null, // 指向父節(jié)點(diǎn)
child: Fiber | null, // 指向自己的第一個(gè)子節(jié)點(diǎn)
sibling: Fiber | null, // 指向自己的兄弟結(jié)構(gòu),兄弟節(jié)點(diǎn)的 return 指向同一個(gè)父節(jié)點(diǎn)
}
首次渲染之后 React 會(huì)得到一個(gè) Fiber 樹(shù),也就是 Current tree(當(dāng)前樹(shù))。當(dāng)處理更新的時(shí)候,React 會(huì)構(gòu)建 WorkInProgress Tree(工作過(guò)程樹(shù)),當(dāng)構(gòu)造完成后會(huì)將 current 指針指向 WorkInProgress Tree,WorkInProgress Tree 成了新的 Fiber tree。
這被稱做雙緩沖。以 Fiber tree 為主,WorkInProgress Tree 為輔。
雙緩沖技術(shù)可以復(fù)用內(nèi)部對(duì)象(fiber),節(jié)省內(nèi)存分配、GC的時(shí)間開(kāi)銷。

總結(jié)
本篇文章篇幅有限,大致講了 Fiber 的相關(guān)知識(shí)和 React 的工作流程,對(duì)于細(xì)節(jié),比如:如何調(diào)度任務(wù),如何 diff 等,感興趣的同學(xué)可以自行結(jié)合源碼研究分析。