隨著 React 16 的發(fā)布,Hooks 的正式上線讶凉,很多小伙伴都很興奮辙谜,都想要嘗試這一新的特性,升級(jí) React 的意愿越來越強(qiáng)烈了肝匆。
我們都知道 React 是一個(gè)優(yōu)秀的前端框架粒蜈,很多的大型應(yīng)用都在使用,而作為使用 React 為工具的開發(fā)者也應(yīng)該了解下 React Fiber旗国,F(xiàn)iber 到底是什么枯怖?它能給我們帶來什么?以及 React 團(tuán)隊(duì)為什么要去重寫 Fiber 架構(gòu)能曾?
什么是 React Fiber
度硝?
React Fiber 是 React 16 中新的協(xié)調(diào)引擎,是對(duì)核心算法的一次重新實(shí)現(xiàn)寿冕。
既然是新的蕊程,那 React 團(tuán)隊(duì)為什么要重寫新的 Fiber 架構(gòu)呢?
在 React 16 以前蚂斤,當(dāng)元素較多存捺,需要頻繁刷新的時(shí)候頁面會(huì)出現(xiàn)卡頓,究其原因是因?yàn)楦逻^程是同步的曙蒸,大量的同步計(jì)算任務(wù)阻塞了瀏覽器的渲染。
當(dāng)頁面加載或者更新時(shí)岗钩,React 會(huì)去計(jì)算和比對(duì) Virtual DOM纽窟,最后繪制頁面,整個(gè)過程是同步進(jìn)行的兼吓。當(dāng) JavaScript 在瀏覽器的主線程上長期運(yùn)行臂港,就會(huì)阻塞了樣式計(jì)算、布局和繪制视搏,導(dǎo)致頁面無法得到及時(shí)的更新和響應(yīng)审孽。此時(shí),無論用戶如何點(diǎn)擊鼠標(biāo)或者敲擊鍵盤都不會(huì)得到響應(yīng)浑娜,當(dāng) React 更新完成后剛剛點(diǎn)擊或敲擊的事件才會(huì)得到響應(yīng)佑力。
由于 JavaScript 是單線程的特點(diǎn),所以一個(gè)線程執(zhí)行完成后才會(huì)執(zhí)行下一個(gè)線程筋遭,當(dāng)上一個(gè)線程任務(wù)耗時(shí)太長打颤,程序就會(huì)對(duì)其他輸入不作出響應(yīng)。
React 的更新過程會(huì)先計(jì)算漓滔,一旦任務(wù)開始進(jìn)行编饺,就無法中斷, js 將一直占用主線程响驴, 直到整棵 Virtual DOM 樹計(jì)算完成之后透且,才能把執(zhí)行權(quán)交給渲染引擎,而 React Fiber 就是要改變現(xiàn)狀豁鲤。
它能給我們帶來什么秽誊?
增量渲染
為不同任務(wù)分配優(yōu)先極
更新時(shí)能暫停罕邀、終止、復(fù)用渲染任務(wù)
并發(fā)方面新的能力
接下來养距,讓我們具體來了解下 Fiber诉探。
Fiber
Fiber 把耗時(shí)長的任務(wù)拆分成很多的小片,每個(gè)小片的運(yùn)行時(shí)間很短棍厌,每次只執(zhí)行一個(gè)小片肾胯,執(zhí)行完后看是否還有剩余時(shí)間,如果有就繼續(xù)執(zhí)行下個(gè)小片耘纱,如果沒有就掛起當(dāng)前任務(wù)敬肚,將控制權(quán)交給 React 負(fù)責(zé)任務(wù)協(xié)調(diào)的模塊,看有沒有其他緊急任務(wù)要做束析,如果沒有就繼續(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ù)丧失,通過優(yōu)先級(jí)來自由調(diào)度子任務(wù),分段更新飒赃,從而將之前的同步渲染改為異步渲染利花。
Fiber 結(jié)構(gòu)
維護(hù)每一個(gè)分片的數(shù)據(jù)結(jié)構(gòu),就是 Fiber载佳,它可以用 JS 對(duì)象來表示炒事,其中包含有關(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)樹中的 `parent`蔫慧,用來在處理完這個(gè)節(jié)點(diǎn)之后向上返回
return: Fiber | null, // 父節(jié)點(diǎn)
// 單鏈表樹結(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 一起,主要用來在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)帶來的新的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ù)在未來的哪個(gè)時(shí)間點(diǎn)應(yīng)該被完成妙啃,不包括他的子樹產(chǎn)生的任務(wù)
// 快速確定子樹中是否有不在等待的變化
childExpirationTime: ExpirationTime,
// 用來描述當(dāng)前Fiber和他子樹的`Bitfield`
// 共存的模式表示這個(gè)子樹是否默認(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樹更新的過程中,每個(gè)Fiber都會(huì)有一個(gè)跟其對(duì)應(yīng)的Fiber
// 我們稱他為`current <==> workInProgress`
// 在渲染完成之后他們會(huì)交換位置
alternate: Fiber | null,
// Effect 相關(guān)的
effectTag: SideEffectTag, // 用來記錄Side Effect
nextEffect: Fiber | null, // 單鏈表用來快速查找下一個(gè)side effect
firstEffect: Fiber | null, // 子樹中第一個(gè)side effect
lastEffect: Fiber | null, // 子樹中最后一個(gè)side effect
}
任務(wù)的優(yōu)先級(jí)
上面講到了任務(wù)的執(zhí)行是根據(jù)優(yōu)先級(jí)來調(diào)度的揖赴,那我們現(xiàn)在具體了解一下優(yōu)先級(jí)馆匿。
synchronous 同步執(zhí)行,首屏使用
task 在 next tick 之前執(zhí)行
animation 下一幀之前執(zhí)行储笑,通過requestAnimationFrame來調(diào)度甜熔,這樣在下一幀就能立即開始動(dòng)畫過程
high 在不久的將來立即執(zhí)行
low 稍微延遲(100-200ms)執(zhí)行也沒關(guān)系
offscreen 當(dāng)前隱藏的、屏幕外的(看不見的)元素突倍,在下一次 render 時(shí)或 scroll 時(shí)才執(zhí)行
Fiber reconciler
Fiber Reconciler 決定了當(dāng)任務(wù)調(diào)度完成之后,如何去執(zhí)行每個(gè)任務(wù)盆昙,如何去更新每一個(gè)節(jié)點(diǎn)的過程羽历。
React Fiber 更新過程分為兩個(gè)階段
第一階段 Reconciliation Phase,生成 Fiber 樹淡喜,得出需要更新的節(jié)點(diǎn)信息秕磷。是一個(gè)漸進(jìn)的過程,可以被打斷炼团。可能會(huì)調(diào)用 componentWillMount瘟芝,componentWillReceiveProps易桃,shouldComponentUpdate,componentWillUpdate 生命周期函數(shù)锌俱。
第二階段 Commit Phase晤郑,將需要更新的節(jié)點(diǎn)批量更新,這個(gè)過程不能被打斷≡烨蓿可能會(huì)調(diào)用 componentDidMount磕洪,componentDidUpdate,componentWillUnmount 生命周期函數(shù)诫龙。
Fiber tree 與 WorkInProgress Tree
// 單鏈表樹結(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 樹,也就是 Current tree(當(dāng)前樹)签赃。當(dāng)處理更新的時(shí)候谷异,React 會(huì)構(gòu)建 WorkInProgress Tree(工作過程樹),當(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í)間開銷尖昏。
總結(jié)
本篇文章篇幅有限,大致講了 Fiber 的相關(guān)知識(shí)和 React 的工作流程构资,對(duì)于細(xì)節(jié)抽诉,比如:如何調(diào)度任務(wù),如何 diff 等吐绵,感興趣的同學(xué)可以自行結(jié)合源碼研究分析迹淌。