【React】1040- 六個(gè)問題讓你更懂 React Fiber
大家好,我是零一,很多人都摸不透React,看不懂源碼,甚至不想看源碼(確實(shí)很難看懂?。。?霸王硬上弓" 肯定是不行呀,不如從React的整體架構(gòu)或者說從最核心的Fiber開始了解,說不定能幫你更懂React呢!
今天就給大家?guī)硪黄彝?獨(dú)釣寒江雪 帶來的精品文章,他是思否的優(yōu)秀作者,輸出多篇硬核文章,感興趣的朋友可以點(diǎn)擊文章末尾的「閱讀原文」進(jìn)行查看~
作者:獨(dú)釣寒江雪
原文:https://segmentfault.com/a/1190000039682751
正文
React Fiber 是Facebook花費(fèi)兩年余時(shí)間對(duì) React 做出的一個(gè)重大改變與優(yōu)化,是對(duì) React 核心算法的一次重新實(shí)現(xiàn)。從Facebook在 React Conf 2017會(huì)議上確認(rèn),React Fiber 會(huì)在React 16 版本發(fā)布至今,也已過去三年有余,如今,React 17 業(yè)已發(fā)布,社區(qū)關(guān)于Fiber的優(yōu)秀文章不在少數(shù)。
本文源于一次團(tuán)隊(duì)內(nèi)部的技術(shù)分享,借鑒社區(qū)優(yōu)秀文章,結(jié)合個(gè)人理解,進(jìn)行整合,從六個(gè)問題出發(fā),對(duì) React Fiber 進(jìn)行理解與認(rèn)識(shí),同時(shí)對(duì)時(shí)下熱門的前端框架Svelte進(jìn)行簡(jiǎn)要介紹與剖析,希望對(duì)正在探究 React 及各前端框架的小伙伴們能有所助益。
全文大量參考和引用以下幾篇博文,讀者可自行查閱:
React技術(shù)揭秘[1] 前端工程師的自我修養(yǎng):React Fiber 是如何實(shí)現(xiàn)更新過程可控的[2] 新興前端框架 Svelte 從入門到原理 以 React 為例,說說框架和性能(下)[3]
一、React 的設(shè)計(jì)理念是什么?
React官網(wǎng)在React哲學(xué)[4]一節(jié)開篇提到:
我們認(rèn)為,React 是用 JavaScript 構(gòu)建快速響應(yīng)的大型 Web 應(yīng)用程序的首選方式。它在 Facebook 和 Instagram 上表現(xiàn)優(yōu)秀。React 最棒的部分之一是引導(dǎo)我們思考如何構(gòu)建一個(gè)應(yīng)用。
由此可見,React 追求的是 “快速響應(yīng)”,那么,“快速響應(yīng)“的制約因素都有什么呢?
CPU的瓶頸:當(dāng)項(xiàng)目變得龐大、組件數(shù)量繁多、遇到大計(jì)算量的操作或者設(shè)備性能不足使得頁面掉幀,導(dǎo)致卡頓。 IO的瓶頸:發(fā)送網(wǎng)絡(luò)請(qǐng)求后,由于需要等待數(shù)據(jù)返回才能進(jìn)一步操作導(dǎo)致不能快速響應(yīng)。
本文要聊的fiber 架構(gòu)主要就是用來解決 CPU 和網(wǎng)絡(luò)的問題,這兩個(gè)問題一直也是最影響前端開發(fā)體驗(yàn)的地方,一個(gè)會(huì)造成卡頓,一個(gè)會(huì)造成白屏。為此 react 為前端引入了兩個(gè)新概念:Time Slicing 時(shí)間分片和Suspense。
二、React的“先天不足” —— 聽說 Vue 3.0 采用了動(dòng)靜結(jié)合的 Dom diff,React 為何不跟進(jìn)?
Vue 3.0 動(dòng)靜結(jié)合的 Dom diff
Vue3.0 提出動(dòng)靜結(jié)合的 DOM diff 思想,動(dòng)靜結(jié)合的 DOM diff其實(shí)是在預(yù)編譯階段進(jìn)行了優(yōu)化。之所以能夠做到預(yù)編譯優(yōu)化,是因?yàn)?Vue core 可以靜態(tài)分析 template,在解析模版時(shí),整個(gè) parse 的過程是利用正則表達(dá)式順序解析模板,當(dāng)解析到開始標(biāo)簽、閉合標(biāo)簽和文本的時(shí)候都會(huì)分別執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù),來達(dá)到構(gòu)造 AST 樹的目的。
借助預(yù)編譯過程,Vue 可以做到的預(yù)編譯優(yōu)化就很強(qiáng)大了。比如在預(yù)編譯時(shí)標(biāo)記出模版中可能變化的組件節(jié)點(diǎn),再次進(jìn)行渲染前 diff 時(shí)就可以跳過“永遠(yuǎn)不會(huì)變化的節(jié)點(diǎn)”,而只需要對(duì)比“可能會(huì)變化的動(dòng)態(tài)節(jié)點(diǎn)”。這也就是動(dòng)靜結(jié)合的 DOM diff 將 diff 成本與模版大小正相關(guān)優(yōu)化到與動(dòng)態(tài)節(jié)點(diǎn)正相關(guān)的理論依據(jù)。
React 能否像 Vue 那樣進(jìn)行預(yù)編譯優(yōu)化?
Vue 需要做數(shù)據(jù)雙向綁定,需要進(jìn)行數(shù)據(jù)攔截或代理,那它就需要在預(yù)編譯階段靜態(tài)分析模版,分析出視圖依賴了哪些數(shù)據(jù),進(jìn)行響應(yīng)式處理。而 React 就是局部重新渲染,React 拿到的或者說掌管的,所負(fù)責(zé)的就是一堆遞歸 React.createElement 的執(zhí)行調(diào)用(參考下方經(jīng)過Babel轉(zhuǎn)換的代碼),它無法從模版層面進(jìn)行靜態(tài)分析。JSX 和手寫的 render function[5] 是完全動(dòng)態(tài)的,過度的靈活性導(dǎo)致運(yùn)行時(shí)可以用于優(yōu)化的信息不足。
JSX 寫法:
<div>
<h1>六個(gè)問題助你理解 React Fiber</h1>
<ul>
<li>React</li>
<li>Vue</li>
</ul>
</div>
遞歸 React.createElement:
// Babel轉(zhuǎn)換后
React.createElement(
"div",
null,
React.createElement(
"h1",
null,
"\u516D\u4E2A\u95EE\u9898\u52A9\u4F60\u7406\u89E3 React Fiber"
),
React.createElement(
"ul",
null,
React.createElement("li", null, "React"),
React.createElement("li", null, "Vue")
)
);
JSX vs Template

JSX 具有 JavaScript 的完整表現(xiàn)力,可以構(gòu)建非常復(fù)雜的組件。但是靈活的語法,也意味著引擎難以理解,無法預(yù)判開發(fā)者的用戶意圖,從而難以優(yōu)化性能。 Template 模板是一種非常有約束的語言,你只能以某種方式去編寫模板。
既然存在以上編譯時(shí)先天不足,在運(yùn)行時(shí)優(yōu)化方面,React一直在努力。比如,React15實(shí)現(xiàn)了batchedUpdates(批量更新)。即同一事件回調(diào)函數(shù)上下文中的多次setState只會(huì)觸發(fā)一次更新。
但是,如果單次更新就很耗時(shí),頁面還是會(huì)卡頓(這在一個(gè)維護(hù)時(shí)間很長(zhǎng)的大應(yīng)用中是很常見的)。這是因?yàn)镽eact15的更新流程是同步執(zhí)行的,一旦開始更新直到頁面渲染前都不能中斷。
資料參考:以 React 為例,說說框架和性能(下)[6] | 新興前端框架 Svelte 從入門到原理
三、從架構(gòu)演變看不斷進(jìn)擊的 React 都做過哪些優(yōu)化?
React渲染頁面的兩個(gè)階段
調(diào)度階段(reconciliation):在這個(gè)階段 React 會(huì)更新數(shù)據(jù)生成新的 Virtual DOM,然后通過Diff算法,快速找出需要更新的元素,放到更新隊(duì)列中去,得到新的更新隊(duì)列。 渲染階段(commit):這個(gè)階段 React 會(huì)遍歷更新隊(duì)列,將其所有的變更一次性更新到DOM上。
React 15 架構(gòu)
React15架構(gòu)可以分為兩層:
Reconciler(協(xié)調(diào)器)—— 負(fù)責(zé)找出變化的組件; Renderer(渲染器)—— 負(fù)責(zé)將變化的組件渲染到頁面上;
在React15及以前,Reconciler采用遞歸的方式創(chuàng)建虛擬DOM,遞歸過程是不能中斷的。如果組件樹的層級(jí)很深,遞歸會(huì)占用線程很多時(shí)間,遞歸更新時(shí)間超過了16ms,用戶交互就會(huì)卡頓。
為了解決這個(gè)問題,React16將遞歸的無法中斷的更新重構(gòu)為異步的可中斷更新,由于曾經(jīng)用于遞歸的虛擬DOM數(shù)據(jù)結(jié)構(gòu)已經(jīng)無法滿足需要。于是,全新的Fiber架構(gòu)應(yīng)運(yùn)而生。
React 16 架構(gòu)
為了解決同步更新長(zhǎng)時(shí)間占用線程導(dǎo)致頁面卡頓的問題,也為了探索運(yùn)行時(shí)優(yōu)化的更多可能,React開始重構(gòu)并一直持續(xù)至今。重構(gòu)的目標(biāo)是實(shí)現(xiàn)Concurrent Mode(并發(fā)模式)。
從v15到v16,React團(tuán)隊(duì)花了兩年時(shí)間將源碼架構(gòu)中的Stack Reconciler重構(gòu)為Fiber Reconciler。
React16架構(gòu)可以分為三層:
Scheduler(調(diào)度器)—— 調(diào)度任務(wù)的優(yōu)先級(jí),高優(yōu)任務(wù)優(yōu)先進(jìn)入Reconciler; Reconciler(協(xié)調(diào)器)—— 負(fù)責(zé)找出變化的組件:更新工作從遞歸變成了可以中斷的循環(huán)過程。Reconciler內(nèi)部采用了Fiber的架構(gòu); Renderer(渲染器)—— 負(fù)責(zé)將變化的組件渲染到頁面上。
React 17 優(yōu)化
React16的expirationTimes模型只能區(qū)分是否>=expirationTimes決定節(jié)點(diǎn)是否更新。React17的lanes模型可以選定一個(gè)更新區(qū)間,并且動(dòng)態(tài)的向區(qū)間中增減優(yōu)先級(jí),可以處理更細(xì)粒度的更新。
Lane用二進(jìn)制位表示任務(wù)的優(yōu)先級(jí),方便優(yōu)先級(jí)的計(jì)算(位運(yùn)算),不同優(yōu)先級(jí)占用不同位置的“賽道”,而且存在批的概念,優(yōu)先級(jí)越低,“賽道”越多。高優(yōu)先級(jí)打斷低優(yōu)先級(jí),新建的任務(wù)需要賦予什么優(yōu)先級(jí)等問題都是Lane所要解決的問題。
Concurrent Mode的目的是實(shí)現(xiàn)一套可中斷/恢復(fù)的更新機(jī)制。其由兩部分組成:
一套協(xié)程架構(gòu):Fiber Reconciler 基于協(xié)程架構(gòu)的啟發(fā)式更新算法:控制協(xié)程架構(gòu)工作方式的算法
資料參考:React17新特性:?jiǎn)l(fā)式更新算法[7]
四、瀏覽器一幀都會(huì)干些什么以及requestIdleCallback的啟示
瀏覽器一幀都會(huì)干些什么?
我們都知道,頁面的內(nèi)容都是一幀一幀繪制出來的,瀏覽器刷新率代表瀏覽器一秒繪制多少幀。原則上說 1s 內(nèi)繪制的幀數(shù)也多,畫面表現(xiàn)就也細(xì)膩。目前瀏覽器大多是 60Hz(60幀/s),每一幀耗時(shí)也就是在 16.6ms 左右。那么在這一幀的(16.6ms) 過程中瀏覽器又干了些什么呢?

通過上面這張圖可以清楚的知道,瀏覽器一幀會(huì)經(jīng)過下面這幾個(gè)過程:
接受輸入事件 執(zhí)行事件回調(diào) 開始一幀 執(zhí)行 RAF (RequestAnimationFrame) 頁面布局,樣式計(jì)算 繪制渲染 執(zhí)行 RIC (RequestIdelCallback)
第七步的 RIC 事件不是每一幀結(jié)束都會(huì)執(zhí)行,只有在一幀的 16.6ms 中做完了前面 6 件事兒且還有剩余時(shí)間,才會(huì)執(zhí)行。如果一幀執(zhí)行結(jié)束后還有時(shí)間執(zhí)行 RIC 事件,那么下一幀需要在事件執(zhí)行結(jié)束才能繼續(xù)渲染,所以 RIC 執(zhí)行不要超過 30ms,如果長(zhǎng)時(shí)間不將控制權(quán)交還給瀏覽器,會(huì)影響下一幀的渲染,導(dǎo)致頁面出現(xiàn)卡頓和事件響應(yīng)不及時(shí)。
requestIdleCallback 的啟示
我們以瀏覽器是否有剩余時(shí)間作為任務(wù)中斷的標(biāo)準(zhǔn),那么我們需要一種機(jī)制,當(dāng)瀏覽器有剩余時(shí)間時(shí)通知我們。
requestIdleCallback((deadline) => {
// deadline 有兩個(gè)參數(shù)
// timeRemaining(): 當(dāng)前幀還剩下多少時(shí)間
// didTimeout: 是否超時(shí)
// 另外 requestIdleCallback 后如果跟上第二個(gè)參數(shù) {timeout: ...} 則會(huì)強(qiáng)制瀏覽器在當(dāng)前幀執(zhí)行完后執(zhí)行。
if (deadline.timeRemaining() > 0) {
// TODO
} else {
requestIdleCallback(otherTasks);
}
});
// 用法示例
var tasksNum = 10000
requestIdleCallback(unImportWork)
function unImportWork(deadline) {
while (deadline.timeRemaining() && tasksNum > 0) {
console.log(`執(zhí)行了 ${10000 - tasksNum + 1}個(gè)任務(wù)`)
tasksNum--
}
if (tasksNum > 0) { // 在未來的幀中繼續(xù)執(zhí)行
requestIdleCallback(unImportWork)
}
}
其實(shí)部分瀏覽器已經(jīng)實(shí)現(xiàn)了這個(gè)API,這就是requestIdleCallback。但是由于以下因素,F(xiàn)acebook 拋棄了 requestIdleCallback 的原生 API:
瀏覽器兼容性; 觸發(fā)頻率不穩(wěn)定,受很多因素影響。比如當(dāng)我們的瀏覽器切換tab后,之前tab注冊(cè)的requestIdleCallback觸發(fā)的頻率會(huì)變得很低。
參考:requestIdleCallback 的 FPS 只有 20[8]
基于以上原因,在React中實(shí)現(xiàn)了功能更完備的requestIdleCallbackpolyfill,這就是Scheduler。除了在空閑時(shí)觸發(fā)回調(diào)的功能外,Scheduler還提供了多種調(diào)度優(yōu)先級(jí)供任務(wù)設(shè)置。
資料參考:requestIdleCallback-后臺(tái)任務(wù)調(diào)度[9]
五、 Fiber 為什么是 React 性能的一個(gè)飛躍?
什么是 Fiber
Fiber 的英文含義是“纖維”,它是比線程(Thread)更細(xì)的線,比線程(Thread)控制得更精密的執(zhí)行模型。在廣義計(jì)算機(jī)科學(xué)概念中,F(xiàn)iber 又是一種協(xié)作的(Cooperative)編程模型(協(xié)程),幫助開發(fā)者用一種【既模塊化又協(xié)作化】的方式來編排代碼。
在 React 中,Fiber 就是 React 16 實(shí)現(xiàn)的一套新的更新機(jī)制,讓 React 的更新過程變得可控,避免了之前采用遞歸需要一氣呵成影響性能的做法。
React Fiber 中的時(shí)間分片
把一個(gè)耗時(shí)長(zhǎng)的任務(wù)分成很多小片,每一個(gè)小片的運(yùn)行時(shí)間很短,雖然總時(shí)間依然很長(zhǎng),但是在每個(gè)小片執(zhí)行完之后,都給其他任務(wù)一個(gè)執(zhí)行的機(jī)會(huì),這樣唯一的線程就不會(huì)被獨(dú)占,其他任務(wù)依然有運(yùn)行的機(jī)會(huì)。
React Fiber 把更新過程碎片化,每執(zhí)行完一段更新過程,就把控制權(quán)交還給 React 負(fù)責(zé)任務(wù)協(xié)調(diào)的模塊,看看有沒有其他緊急任務(wù)要做,如果沒有就繼續(xù)去更新,如果有緊急任務(wù),那就去做緊急任務(wù)。
Stack Reconciler
基于棧的 Reconciler,瀏覽器引擎會(huì)從執(zhí)行棧的頂端開始執(zhí)行,執(zhí)行完畢就彈出當(dāng)前執(zhí)行上下文,開始執(zhí)行下一個(gè)函數(shù),直到執(zhí)行棧被清空才會(huì)停止。然后將執(zhí)行權(quán)交還給瀏覽器。由于 React 將頁面視圖視作一個(gè)個(gè)函數(shù)執(zhí)行的結(jié)果。每一個(gè)頁面往往由多個(gè)視圖組成,這就意味著多個(gè)函數(shù)的調(diào)用。
如果一個(gè)頁面足夠復(fù)雜,形成的函數(shù)調(diào)用棧就會(huì)很深。每一次更新,執(zhí)行棧需要一次性執(zhí)行完成,中途不能干其他的事兒,只能"一心一意"。結(jié)合前面提到的瀏覽器刷新率,JS 一直執(zhí)行,瀏覽器得不到控制權(quán),就不能及時(shí)開始下一幀的繪制。如果這個(gè)時(shí)間超過 16ms,當(dāng)頁面有動(dòng)畫效果需求時(shí),動(dòng)畫因?yàn)闉g覽器不能及時(shí)繪制下一幀,這時(shí)動(dòng)畫就會(huì)出現(xiàn)卡頓。不僅如此,因?yàn)槭录憫?yīng)代碼是在每一幀開始的時(shí)候執(zhí)行,如果不能及時(shí)繪制下一幀,事件響應(yīng)也會(huì)延遲。
Fiber Reconciler
鏈表結(jié)構(gòu)
在 React Fiber 中用鏈表遍歷的方式替代了 React 16 之前的棧遞歸方案。在 React 16 中使用了大量的鏈表。
使用多向鏈表的形式替代了原來的樹結(jié)構(gòu);
<div id="A">
A1
<div id="B1">
B1
<div id="C1"></div>
</div>
<div id="B2">
B2
</div>
</div>

副作用單鏈表;

狀態(tài)更新單鏈表;

...
鏈表是一種簡(jiǎn)單高效的數(shù)據(jù)結(jié)構(gòu),它在當(dāng)前節(jié)點(diǎn)中保存著指向下一個(gè)節(jié)點(diǎn)的指針;遍歷的時(shí)候,通過操作指針找到下一個(gè)元素。

鏈表相比順序結(jié)構(gòu)數(shù)據(jù)格式的好處就是:
操作更高效,比如順序調(diào)整、刪除,只需要改變節(jié)點(diǎn)的指針指向就好了。 不僅可以根據(jù)當(dāng)前節(jié)點(diǎn)找到下一個(gè)節(jié)點(diǎn),在多向鏈表中,還可以找到他的父節(jié)點(diǎn)或者兄弟節(jié)點(diǎn)。
但鏈表也不是完美的,缺點(diǎn)就是:
比順序結(jié)構(gòu)數(shù)據(jù)更占用空間,因?yàn)槊總€(gè)節(jié)點(diǎn)對(duì)象還保存有指向下一個(gè)對(duì)象的指針。 不能自由讀取,必須找到他的上一個(gè)節(jié)點(diǎn)。
React 用空間換時(shí)間,更高效的操作可以方便根據(jù)優(yōu)先級(jí)進(jìn)行操作。同時(shí)可以根據(jù)當(dāng)前節(jié)點(diǎn)找到其他節(jié)點(diǎn),在下面提到的掛起和恢復(fù)過程中起到了關(guān)鍵作用。
斐波那契數(shù)列的 Fiber
遞歸形式的斐波那契數(shù)列寫法:
function fib(n) {
if (n <= 2) {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
采用 Fiber 的思路將其改寫為循環(huán)(這個(gè)例子并不能和 React Fiber 的對(duì)等):
function fib(n) {
let fiber = { arg: n, returnAddr: null, a: 0 }, consoled = false;
// 標(biāo)記循環(huán)
rec: while (true) {
// 當(dāng)展開完全后,開始計(jì)算
if (fiber.arg <= 2) {
let sum = 1;
// 尋找父級(jí)
while (fiber.returnAddr) {
if(!consoled) {
// 在這里打印查看形成的鏈表形式的 fiber 對(duì)象
consoled=true
console.log(fiber)
}
fiber = fiber.returnAddr;
if (fiber.a === 0) {
fiber.a = sum;
fiber = { arg: fiber.arg - 2, returnAddr: fiber, a: 0 };
continue rec;
}
sum += fiber.a;
}
return sum;
} else {
// 先展開
fiber = { arg: fiber.arg - 1, returnAddr: fiber, a: 0 };
}
}
}
六、React Fiber 是如何實(shí)現(xiàn)更新過程可控?
更新過程的可控主要體現(xiàn)在下面幾個(gè)方面:
任務(wù)拆分 任務(wù)掛起、恢復(fù)、終止 任務(wù)具備優(yōu)先級(jí)
任務(wù)拆分
在 React Fiber 機(jī)制中,它采用"化整為零"的思想,將調(diào)和階段(Reconciler)遞歸遍歷 VDOM 這個(gè)大任務(wù)分成若干小任務(wù),每個(gè)任務(wù)只負(fù)責(zé)一個(gè)節(jié)點(diǎn)的處理。
任務(wù)掛起、恢復(fù)、終止
workInProgress tree
workInProgress 代表當(dāng)前正在執(zhí)行更新的 Fiber 樹。在 render 或者 setState 后,會(huì)構(gòu)建一顆 Fiber 樹,也就是 workInProgress tree,這棵樹在構(gòu)建每一個(gè)節(jié)點(diǎn)的時(shí)候會(huì)收集當(dāng)前節(jié)點(diǎn)的副作用,整棵樹構(gòu)建完成后,會(huì)形成一條完整的副作用鏈。
currentFiber tree
currentFiber 表示上次渲染構(gòu)建的 Filber 樹。在每一次更新完成后 workInProgress 會(huì)賦值給 currentFiber。在新一輪更新時(shí) workInProgress tree 再重新構(gòu)建,新 workInProgress 的節(jié)點(diǎn)通過 alternate 屬性和 currentFiber 的節(jié)點(diǎn)建立聯(lián)系。
在新 workInProgress tree 的創(chuàng)建過程中,會(huì)同 currentFiber 的對(duì)應(yīng)節(jié)點(diǎn)進(jìn)行 Diff 比較,收集副作用。同時(shí)也會(huì)復(fù)用和 currentFiber 對(duì)應(yīng)的節(jié)點(diǎn)對(duì)象,減少新創(chuàng)建對(duì)象帶來的開銷。也就是說無論是創(chuàng)建還是更新、掛起、恢復(fù)以及終止操作都是發(fā)生在 workInProgress tree 創(chuàng)建過程中的。workInProgress tree 構(gòu)建過程其實(shí)就是循環(huán)的執(zhí)行任務(wù)和創(chuàng)建下一個(gè)任務(wù)。
掛起
當(dāng)?shù)谝粋€(gè)小任務(wù)完成后,先判斷這一幀是否還有空閑時(shí)間,沒有就掛起下一個(gè)任務(wù)的執(zhí)行,記住當(dāng)前掛起的節(jié)點(diǎn),讓出控制權(quán)給瀏覽器執(zhí)行更高優(yōu)先級(jí)的任務(wù)。
恢復(fù)
在瀏覽器渲染完一幀后,判斷當(dāng)前幀是否有剩余時(shí)間,如果有就恢復(fù)執(zhí)行之前掛起的任務(wù)。如果沒有任務(wù)需要處理,代表調(diào)和階段完成,可以開始進(jìn)入渲染階段。
如何判斷一幀是否有空閑時(shí)間的呢?
使用前面提到的 RIC (RequestIdleCallback) 瀏覽器原生 API,React 源碼中為了兼容低版本的瀏覽器,對(duì)該方法進(jìn)行了 Polyfill。
恢復(fù)執(zhí)行的時(shí)候又是如何知道下一個(gè)任務(wù)是什么呢?
答案是在前面提到的鏈表。在 React Fiber 中每個(gè)任務(wù)其實(shí)就是在處理一個(gè) FiberNode 對(duì)象,然后又生成下一個(gè)任務(wù)需要處理的 FiberNode。
終止
其實(shí)并不是每次更新都會(huì)走到提交階段。當(dāng)在調(diào)和過程中觸發(fā)了新的更新,在執(zhí)行下一個(gè)任務(wù)的時(shí)候,判斷是否有優(yōu)先級(jí)更高的執(zhí)行任務(wù),如果有就終止原來將要執(zhí)行的任務(wù),開始新的 workInProgressFiber 樹構(gòu)建過程,開始新的更新流程。這樣可以避免重復(fù)更新操作。這也是在 React 16 以后生命周期函數(shù) componentWillMount 有可能會(huì)執(zhí)行多次的原因。

任務(wù)具備優(yōu)先級(jí)
React Fiber 除了通過掛起,恢復(fù)和終止來控制更新外,還給每個(gè)任務(wù)分配了優(yōu)先級(jí)。具體點(diǎn)就是在創(chuàng)建或者更新 FiberNode 的時(shí)候,通過算法給每個(gè)任務(wù)分配一個(gè)到期時(shí)間(expirationTime)。在每個(gè)任務(wù)執(zhí)行的時(shí)候除了判斷剩余時(shí)間,如果當(dāng)前處理節(jié)點(diǎn)已經(jīng)過期,那么無論現(xiàn)在是否有空閑時(shí)間都必須執(zhí)行該任務(wù)。過期時(shí)間的大小還代表著任務(wù)的優(yōu)先級(jí)。
任務(wù)在執(zhí)行過程中順便收集了每個(gè) FiberNode 的副作用,將有副作用的節(jié)點(diǎn)通過 firstEffect、lastEffect、nextEffect 形成一條副作用單鏈表 A1(TEXT)-B1(TEXT)-C1(TEXT)-C1-C2(TEXT)-C2-B1-B2(TEXT)-B2-A。
其實(shí)最終都是為了收集到這條副作用鏈表,有了它,在接下來的渲染階段就通過遍歷副作用鏈完成 DOM 更新。這里需要注意,更新真實(shí) DOM 的這個(gè)動(dòng)作是一氣呵成的,不能中斷,不然會(huì)造成視覺上的不連貫(commit)。
<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>

直觀展示
正是基于以上這些過程,使用Fiber,我們就有了在社區(qū)經(jīng)??吹降?span style="font-weight: bold;color: #009688;">兩張對(duì)比圖[10]。
清晰展示及交互、源碼可通過下面兩個(gè)鏈接進(jìn)入,查看網(wǎng)頁源代碼。
Stack Example[11] Fiber Example[12]


Fiber 結(jié)構(gòu)長(zhǎng)什么樣?
基于時(shí)間分片的增量更新需要更多的上下文信息,之前的vDOM tree顯然難以滿足,所以擴(kuò)展出了fiber tree(即Fiber上下文的vDOM tree),更新過程就是根據(jù)輸入數(shù)據(jù)以及現(xiàn)有的fiber tree構(gòu)造出新的fiber tree(workInProgress tree)。
FiberNode 上的屬性有很多,根據(jù)筆者的理解,以下這么幾個(gè)屬性是值得關(guān)注的:return、child、sibling(主要負(fù)責(zé)fiber鏈表的鏈接);stateNode;effectTag;expirationTime;alternate;nextEffect。各屬性介紹參看下面的class FiberNode:
class FiberNode {
constructor(tag, pendingProps, key, mode) {
// 實(shí)例屬性
this.tag = tag; // 標(biāo)記不同組件類型,如函數(shù)組件、類組件、文本、原生組件...
this.key = key; // react 元素上的 key 就是 jsx 上寫的那個(gè) key ,也就是最終 ReactElement 上的
this.elementType = null; // createElement的第一個(gè)參數(shù),ReactElement 上的 type
this.type = null; // 表示fiber的真實(shí)類型 ,elementType 基本一樣,在使用了懶加載之類的功能時(shí)可能會(huì)不一樣
this.stateNode = null; // 實(shí)例對(duì)象,比如 class 組件 new 完后就掛載在這個(gè)屬性上面,如果是RootFiber,那么它上面掛的是 FiberRoot,如果是原生節(jié)點(diǎn)就是 dom 對(duì)象
// fiber
this.return = null; // 父節(jié)點(diǎn),指向上一個(gè) fiber
this.child = null; // 子節(jié)點(diǎn),指向自身下面的第一個(gè) fiber
this.sibling = null; // 兄弟組件, 指向一個(gè)兄弟節(jié)點(diǎn)
this.index = 0; // 一般如果沒有兄弟節(jié)點(diǎn)的話是0 當(dāng)某個(gè)父節(jié)點(diǎn)下的子節(jié)點(diǎn)是數(shù)組類型的時(shí)候會(huì)給每個(gè)子節(jié)點(diǎn)一個(gè) index,index 和 key 要一起做 diff
this.ref = null; // reactElement 上的 ref 屬性
this.pendingProps = pendingProps; // 新的 props
this.memoizedProps = null; // 舊的 props
this.updateQueue = null; // fiber 上的更新隊(duì)列執(zhí)行一次 setState 就會(huì)往這個(gè)屬性上掛一個(gè)新的更新, 每條更新最終會(huì)形成一個(gè)鏈表結(jié)構(gòu),最后做批量更新
this.memoizedState = null; // 對(duì)應(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; // 指向下個(gè)需要更新的fiber
this.firstEffect = null; // 指向所有子節(jié)點(diǎn)里,需要更新的 fiber 里的第一個(gè)
this.lastEffect = null; // 指向所有子節(jié)點(diǎn)中需要更新的 fiber 的最后一個(gè)
this.expirationTime = NoWork; // 過期時(shí)間,代表任務(wù)在未來的哪個(gè)時(shí)間點(diǎn)應(yīng)該被完成
this.childExpirationTime = NoWork; // child 過期時(shí)間
this.alternate = null; // current 樹和 workInprogress 樹之間的相互引用
}
}

圖片來源:完全理解React Fiber[13]
function performUnitWork(currentFiber){
//beginWork(currentFiber) //找到兒子,并通過鏈表的方式掛到currentFiber上,每一偶兒子就找后面那個(gè)兄弟
//有兒子就返回兒子
if(currentFiber.child){
return currentFiber.child;
}
//如果沒有兒子,則找弟弟
while(currentFiber){//一直往上找
//completeUnitWork(currentFiber);//將自己的副作用掛到父節(jié)點(diǎn)去
if(currentFiber.sibling){
return currentFiber.sibling
}
currentFiber = currentFiber.return;
}
}
Concurrent Mode (并發(fā)模式)
Concurrent Mode 指的就是 React 利用上面 Fiber 帶來的新特性的開啟的新模式 (mode)。react17開始支持concurrent mode,這種模式的根本目的是為了讓應(yīng)用保持cpu和io的快速響應(yīng),它是一組新功能,包括Fiber、Scheduler、Lane,可以根據(jù)用戶硬件性能和網(wǎng)絡(luò)狀況調(diào)整應(yīng)用的響應(yīng)速度,核心就是為了實(shí)現(xiàn)異步可中斷的更新。concurrent mode也是未來react主要迭代的方向。
目前 React 實(shí)驗(yàn)版本允許用戶選擇三種 mode:
Legacy Mode: 就相當(dāng)于目前穩(wěn)定版的模式 Blocking Mode: 應(yīng)該是以后會(huì)代替 Legacy Mode 而長(zhǎng)期存在的模式 Concurrent Mode: 以后會(huì)變成 default 的模式
Concurrent Mode 其實(shí)開啟了一堆新特性,其中有兩個(gè)最重要的特性可以用來解決我們開頭提到的兩個(gè)問題:
Suspense[14]:Suspense 是 React 提供的一種異步處理的機(jī)制, 它不是一個(gè)具體的數(shù)據(jù)請(qǐng)求庫(kù)。它是React 提供的原生的組件異步調(diào)用原語。 useTrasition[15]:讓頁面實(shí)現(xiàn) Pending -> Skeleton -> Complete的更新路徑, 用戶在切換頁面時(shí)可以停留在當(dāng)前頁面,讓頁面保持響應(yīng)。相比展示一個(gè)無用的空白頁面或者加載狀態(tài),這種用戶體驗(yàn)更加友好。
其中 Suspense 可以用來解決請(qǐng)求阻塞的問題,UI 卡頓的問題其實(shí)開啟 concurrent mode 就已經(jīng)解決的,但如何利用 concurrent mode 來實(shí)現(xiàn)更友好的交互還是需要對(duì)代碼做一番改動(dòng)的。
資料參考:Concurrent 模式介紹 (實(shí)驗(yàn)性)[16] | 理解 React Fiber & Concurrent Mode[17] | 11.concurrent mode(并發(fā)模式是什么樣的)[18] | 人人都能讀懂的react源碼解析[19]
未來可期
Concurrent Mode只是并發(fā),既然任務(wù)可拆分(只要最終得到完整effect list就行),那就允許并行執(zhí)行,(多個(gè)Fiber reconciler + 多個(gè)worker),首屏也更容易分塊加載/渲染(vDOM森林。
并行渲染的話,據(jù)說Firefox測(cè)試結(jié)果顯示,130ms的頁面,只需要30ms就能搞定,所以在這方面是值得期待的,而React已經(jīng)做好準(zhǔn)備了,這也就是在React Fiber上下文經(jīng)常聽到的待unlock的更多特性之一。
isInputPending —— Fiber架構(gòu)思想對(duì)前端生態(tài)的影響
Facebook 在 Chromium 中提出并實(shí)現(xiàn)了 isInputPending() API,它可以提高網(wǎng)頁的響應(yīng)能力,但是不會(huì)對(duì)性能造成太大影響。Facebook 提出的 isInputPending API 是第一個(gè)將中斷的概念用于瀏覽器用戶交互的的功能,并且允許 JavaScript 能夠檢查事件隊(duì)列而不會(huì)將控制權(quán)交于瀏覽器。
目前 isInputPending API 僅在 Chromium 的 87 版本開始提供,其他瀏覽器并未實(shí)現(xiàn)。

資料參考:Facebook 將對(duì) React 的優(yōu)化實(shí)現(xiàn)到了瀏覽器![20]
Svelte 對(duì)固有模式的沖擊
當(dāng)下前端領(lǐng)域,三大框架React、Vue、Angular版本逐漸穩(wěn)定,如果說前端行業(yè)會(huì)出現(xiàn)哪些框架有可能會(huì)挑戰(zhàn)React或者Vue呢?很多人認(rèn)為Svelte 應(yīng)該是其中的選項(xiàng)之一。
Svelte叫法是[Svelte], 本意是苗條纖瘦的,是一個(gè)新興熱門的前端框架。在開發(fā)者滿意度、興趣度、市場(chǎng)占有率上均名列前茅,同時(shí),它有更小的打包體積,更少的開發(fā)代碼書寫,在性能測(cè)評(píng)中,與React、Vue相比,也不遑多讓。
Svelte 的核心思想在于『通過靜態(tài)編譯減少框架運(yùn)行時(shí)的代碼量』。
Svelte 優(yōu)勢(shì)有哪些
No Runtime —— 無運(yùn)行時(shí)代碼 Less-Code —— 寫更少的代碼 Hight-Performance —— 高性能
Svelte 劣勢(shì)
社區(qū) 社區(qū) 社區(qū)
原理概覽
Svelte 在編譯時(shí),就已經(jīng)分析好了數(shù)據(jù) 和 DOM 節(jié)點(diǎn)之間的對(duì)應(yīng)關(guān)系,在數(shù)據(jù)發(fā)生變化時(shí),可以非常高效的來更新DOM節(jié)點(diǎn)。
Rich Harris 在進(jìn)行Svelte的設(shè)計(jì)的時(shí)候沒有采用 Virtual DOM,主要是因?yàn)樗X得Virtual DOM Diff 的過程是非常低效的。具體可參考Virtual Dom 真的高效嗎[21]一文;Svelte 采用了Templates語法,在編譯的過程中就進(jìn)行優(yōu)化操作; Svelte 記錄臟數(shù)據(jù)的方式:位掩碼(bitMask); 數(shù)據(jù)和DOM節(jié)點(diǎn)之間的對(duì)應(yīng)關(guān)系:React 和 Vue 是通過 Virtual Dom 進(jìn)行 diff 來算出來更新哪些 DOM 節(jié)點(diǎn)效率最高。Svelte 是在編譯時(shí)候,就記錄了數(shù)據(jù) 和 DOM 節(jié)點(diǎn)之間的對(duì)應(yīng)關(guān)系,并且保存在 p 函數(shù)中。

資料參考:[新興前端框架 Svelte 從入門到原理](
參考資料
React技術(shù)揭秘: https://link.segmentfault.com/?url=https%3A%2F%2Freact.iamkasong.com%2F
[2]前端工程師的自我修養(yǎng):React Fiber 是如何實(shí)現(xiàn)更新過程可控的: https://link.segmentfault.com/?url=https%3A%2F%2Fwww.zoo.team%2Farticle%2Fabout-react-fiber
[3]以 React 為例,說說框架和性能(下): https://link.segmentfault.com/?url=https%3A%2F%2Fgitbook.cn%2Fm%2Fmazi%2Fcolumns%2F5c91c813968b1d64b1e08fde%2Ftopics%2F5cbbf49bbbbba80861a35c64
[4]React哲學(xué): https://link.segmentfault.com/?url=https%3A%2F%2Fzh-hans.reactjs.org%2Fdocs%2Fthinking-in-react.html
[5]JSX 和手寫的 render function: https://link.segmentfault.com/?url=https%3A%2F%2Fcn.vuejs.org%2Fv2%2Fguide%2Frender-function.html
[6]以 React 為例,說說框架和性能(下): https://link.segmentfault.com/?url=https%3A%2F%2Fgitbook.cn%2Fm%2Fmazi%2Fcolumns%2F5c91c813968b1d64b1e08fde%2Ftopics%2F5cbbf49bbbbba80861a35c64
[7]React17新特性:?jiǎn)l(fā)式更新算法: https://link.segmentfault.com/?url=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F182411298
[8]requestIdleCallback 的 FPS 只有 20: https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Ffacebook%2Freact%2Fissues%2F13206
[9]requestIdleCallback-后臺(tái)任務(wù)調(diào)度: https://link.segmentfault.com/?url=http%3A%2F%2Fwww.zhangyunling.com%2F702.html
[10]兩張對(duì)比圖: https://link.segmentfault.com/?url=https%3A%2F%2Fclaudiopro.github.io%2Freact-fiber-vs-stack-demo%2F
[11]Stack Example: https://link.segmentfault.com/?url=https%3A%2F%2Fclaudiopro.github.io%2Freact-fiber-vs-stack-demo%2Fstack.html
[12]Fiber Example: https://link.segmentfault.com/?url=https%3A%2F%2Fclaudiopro.github.io%2Freact-fiber-vs-stack-demo%2Ffiber.html
[13]完全理解React Fiber: https://link.segmentfault.com/?url=http%3A%2F%2Fwww.ayqy.net%2Fblog%2Fdive-into-react-fiber%2F
[14]Suspense: https://link.segmentfault.com/?url=https%3A%2F%2Fjuejin.cn%2Fpost%2F6844903981999718407
[15]useTrasition: https://link.segmentfault.com/?url=https%3A%2F%2Fjuejin.cn%2Fpost%2F6844903986420514823
[16]Concurrent 模式介紹 (實(shí)驗(yàn)性): https://link.segmentfault.com/?url=https%3A%2F%2Fzh-hans.reactjs.org%2Fdocs%2Fconcurrent-mode-intro.html
[17]理解 React Fiber & Concurrent Mode: https://link.segmentfault.com/?url=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F109971435
[18]11.concurrent mode(并發(fā)模式是什么樣的): https://link.segmentfault.com/?url=https%3A%2F%2Fxiaochen1024.com%2Farticle_item%2F600acd69245877002ed5df05
[19]人人都能讀懂的react源碼解析: https://link.segmentfault.com/?url=https%3A%2F%2Fxiaochen1024.com%2F
[20]Facebook 將對(duì) React 的優(yōu)化實(shí)現(xiàn)到了瀏覽器!: https://link.segmentfault.com/?url=https%3A%2F%2Fmp.weixin.qq.com%2Fs%2FLbcu1aa2LQZlddAwIIExqA
[21]Virtual Dom 真的高效嗎: https://link.segmentfault.com/?url=https%3A%2F%2Fwww.sveltejs.cn%2Fblog%2Fvirtual-dom-is-pure-overhead

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 120+ 篇原創(chuàng)文章
