React Fiber 是Facebook花費兩年餘時間對 React 作出的一個重大改變與優化,是對 React 核心算法的一次從新實現。從Facebook在 React Conf 2017會議上確認,React Fiber 會在React 16 版本發佈至今,也已過去三年有餘,現在,React 17 業已發佈,社區關於Fiber的優秀文章不在少數。html
本文源於一次團隊內部的技術分享,借鑑社區優秀文章,結合我的理解,進行整合,從六個問題出發,對 React Fiber 進行理解與認識,同時對時下熱門的前端框架Svelte進行簡要介紹與剖析,但願對正在探究 React 及各前端框架的小夥伴們能有所助益。前端
全文大量參考和引用如下幾篇博文,讀者可自行查閱:vue
React官網在React哲學一節開篇提到:react
咱們認爲,React 是用 JavaScript 構建 快速響應的大型 Web 應用程序的首選方式。它在 Facebook 和 Instagram 上表現優秀。React 最棒的部分之一是引導咱們思考如何構建一個應用。
因而可知,React 追求的是 「快速響應」,那麼,「快速響應「的制約因素都有什麼呢?git
本文要聊的fiber 架構主要就是用來解決 CPU 和網絡的問題,這兩個問題一直也是最影響前端開發體驗的地方,一個會形成卡頓,一個會形成白屏。爲此 react 爲前端引入了兩個新概念:Time Slicing 時間分片和Suspense。github
Vue3.0 提出動靜結合的 DOM diff 思想,動靜結合的 DOM diff實際上是在預編譯階段進行了優化。之因此可以作到預編譯優化,是由於 Vue core 能夠靜態分析 template,在解析模版時,整個 parse 的過程是利用正則表達式順序解析模板,當解析到開始標籤、閉合標籤和文本的時候都會分別執行對應的回調函數,來達到構造 AST 樹的目的。正則表達式
藉助預編譯過程,Vue 能夠作到的預編譯優化就很強大了。好比在預編譯時標記出模版中可能變化的組件節點,再次進行渲染前 diff 時就能夠跳過「永遠不會變化的節點」,而只須要對比「可能會變化的動態節點」。這也就是動靜結合的 DOM diff 將 diff 成本與模版大小正相關優化到與動態節點正相關的理論依據。算法
Vue 須要作數據雙向綁定,須要進行數據攔截或代理,那它就須要在預編譯階段靜態分析模版,分析出視圖依賴了哪些數據,進行響應式處理。而 React 就是局部從新渲染,React 拿到的或者說掌管的,所負責的就是一堆遞歸 React.createElement 的執行調用(參考下方通過Babel轉換的代碼),它沒法從模版層面進行靜態分析。JSX 和手寫的 render function 是徹底動態的,過分的靈活性致使運行時能夠用於優化的信息不足。編程
JSX 寫法:api
<div> <h1>六個問題助你理解 React Fiber</h1> <ul> <li>React</li> <li>Vue</li> </ul> </div>
遞歸 React.createElement:
// Babel轉換後 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") ) );
既然存在以上編譯時先天不足,在運行時優化方面,React一直在努力。好比,React15實現了batchedUpdates(批量更新)。即同一事件回調函數上下文中的屢次setState只會觸發一次更新。
可是,若是單次更新就很耗時,頁面仍是會卡頓(這在一個維護時間很長的大應用中是很常見的)。這是由於React15的更新流程是同步執行的,一旦開始更新直到頁面渲染前都不能中斷。
資料參考: 以 React 爲例,說說框架和性能(下) | 新興前端框架 Svelte 從入門到原理
React15架構能夠分爲兩層:
在React15及之前,Reconciler採用遞歸的方式建立虛擬DOM,遞歸過程是不能中斷的。若是組件樹的層級很深,遞歸會佔用線程不少時間,遞歸更新時間超過了16ms,用戶交互就會卡頓。
爲了解決這個問題,React16將遞歸的沒法中斷的更新重構爲異步的可中斷更新,因爲曾經用於遞歸的虛擬DOM數據結構已經沒法知足須要。因而,全新的Fiber架構應運而生。
爲了解決同步更新長時間佔用線程致使頁面卡頓的問題,也爲了探索運行時優化的更多可能,React開始重構並一直持續至今。重構的目標是實現Concurrent Mode(併發模式)。
從v15到v16,React團隊花了兩年時間將源碼架構中的Stack Reconciler重構爲Fiber Reconciler。
React16架構能夠分爲三層:
React16的expirationTimes模型只能區分是否>=expirationTimes
決定節點是否更新。React17的lanes模型能夠選定一個更新區間,而且動態的向區間中增減優先級,能夠處理更細粒度的更新。
Lane用 二進制位表示任務的優先級,方便優先級的計算(位運算),不一樣優先級佔用不一樣位置的「 賽道」,並且存在批的概念,優先級越低,「賽道」越多。高優先級打斷低優先級,新建的任務須要賦予什麼優先級等問題都是Lane所要解決的問題。
Concurrent Mode的目的是實現一套可中斷/恢復的更新機制。其由兩部分組成:
資料參考: React17新特性:啓發式更新算法
咱們都知道,頁面的內容都是一幀一幀繪製出來的,瀏覽器刷新率表明瀏覽器一秒繪製多少幀。原則上說 1s 內繪製的幀數也多,畫面表現就也細膩。目前瀏覽器大可能是 60Hz(60幀/s),每一幀耗時也就是在 16.6ms 左右。那麼在這一幀的(16.6ms) 過程當中瀏覽器又幹了些什麼呢?
經過上面這張圖能夠清楚的知道,瀏覽器一幀會通過下面這幾個過程:
第七步的 RIC 事件不是每一幀結束都會執行,只有在一幀的 16.6ms 中作完了前面 6 件事兒且還有剩餘時間,纔會執行。若是一幀執行結束後還有時間執行 RIC 事件,那麼下一幀須要在事件執行結束才能繼續渲染,因此 RIC 執行不要超過 30ms,若是長時間不將控制權交還給瀏覽器,會影響下一幀的渲染,致使頁面出現卡頓和事件響應不及時。
咱們以瀏覽器是否有剩餘時間做爲任務中斷的標準,那麼咱們須要一種機制,當瀏覽器有剩餘時間時通知咱們。
requestIdleCallback((deadline) => { // deadline 有兩個參數 // timeRemaining(): 當前幀還剩下多少時間 // didTimeout: 是否超時 // 另外 requestIdleCallback 後若是跟上第二個參數 {timeout: ...} 則會強制瀏覽器在當前幀執行完後執行。 if (deadline.timeRemaining() > 0) { // TODO } else { requestIdleCallback(otherTasks); } });
// 用法示例 var tasksNum = 10000 requestIdleCallback(unImportWork) function unImportWork(deadline) { while (deadline.timeRemaining() && tasksNum > 0) { console.log(`執行了${10000 - tasksNum + 1}個任務`) tasksNum-- } if (tasksNum > 0) { // 在將來的幀中繼續執行 requestIdleCallback(unImportWork) } }
其實部分瀏覽器已經實現了這個API,這就是requestIdleCallback。可是因爲如下因素,Facebook 拋棄了 requestIdleCallback 的原生 API:
參考: requestIdleCallback 的 FPS 只有 20
基於以上緣由,在React中實現了功能更完備的requestIdleCallbackpolyfill,這就是Scheduler。除了在空閒時觸發回調的功能外,Scheduler還提供了多種調度優先級供任務設置。
資料參考: requestIdleCallback-後臺任務調度
Fiber 的英文含義是「纖維」,它是比線程(Thread)更細的線,比線程(Thread)控制得更精密的執行模型。在廣義計算機科學概念中,Fiber 又是一種協做的(Cooperative)編程模型(協程),幫助開發者用一種【既模塊化又協做化】的方式來編排代碼。
在 React 中,Fiber 就是 React 16 實現的一套新的更新機制,讓 React 的更新過程變得可控,避免了以前採用遞歸須要一鼓作氣影響性能的作法。
把一個耗時長的任務分紅不少小片,每個小片的運行時間很短,雖然總時間依然很長,可是在每一個小片執行完以後,都給其餘任務一個執行的機會,這樣惟一的線程就不會被獨佔,其餘任務依然有運行的機會。
React Fiber 把更新過程碎片化,每執行完一段更新過程,就把控制權交還給 React 負責任務協調的模塊,看看有沒有其餘緊急任務要作,若是沒有就繼續去更新,若是有緊急任務,那就去作緊急任務。
基於棧的 Reconciler,瀏覽器引擎會從執行棧的頂端開始執行,執行完畢就彈出當前執行上下文,開始執行下一個函數,直到執行棧被清空纔會中止。而後將執行權交還給瀏覽器。因爲 React 將頁面視圖視做一個個函數執行的結果。每個頁面每每由多個視圖組成,這就意味着多個函數的調用。
若是一個頁面足夠複雜,造成的函數調用棧就會很深。每一次更新,執行棧須要一次性執行完成,中途不能幹其餘的事兒,只能"一心一意"。結合前面提到的瀏覽器刷新率,JS 一直執行,瀏覽器得不到控制權,就不能及時開始下一幀的繪製。若是這個時間超過 16ms,當頁面有動畫效果需求時,動畫由於瀏覽器不能及時繪製下一幀,這時動畫就會出現卡頓。不只如此,由於事件響應代碼是在每一幀開始的時候執行,若是不能及時繪製下一幀,事件響應也會延遲。
在 React Fiber 中用鏈表遍歷的方式替代了 React 16 以前的棧遞歸方案。在 React 16 中使用了大量的鏈表。
<div id="A"> A1 <div id="B1"> B1 <div id="C1"></div> </div> <div id="B2"> B2 </div> </div>
鏈表是一種簡單高效的數據結構,它在當前節點中保存着指向下一個節點的指針;遍歷的時候,經過操做指針找到下一個元素。
鏈表相比順序結構數據格式的好處就是:
但鏈表也不是完美的,缺點就是:
React 用空間換時間,更高效的操做能夠方便根據優先級進行操做。同時能夠根據當前節點找到其餘節點,在下面提到的掛起和恢復過程當中起到了關鍵做用。
遞歸形式的斐波那契數列寫法:
function fib(n) { if (n <= 2) { return 1; } else { return fib(n - 1) + fib(n - 2); } }
採用 Fiber 的思路將其改寫爲循環(這個例子並不能和 React Fiber 的對等):
function fib(n) { let fiber = { arg: n, returnAddr: null, a: 0 }, consoled = false; // 標記循環 rec: while (true) { // 當展開徹底後,開始計算 if (fiber.arg <= 2) { let sum = 1; // 尋找父級 while (fiber.returnAddr) { if(!consoled) { // 在這裏打印查看造成的鏈表形式的 fiber 對象 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 機制中,它採用"化整爲零"的思想,將調和階段(Reconciler)遞歸遍歷 VDOM 這個大任務分紅若干小任務,每一個任務只負責一個節點的處理。
workInProgress 表明當前正在執行更新的 Fiber 樹。在 render 或者 setState 後,會構建一顆 Fiber 樹,也就是 workInProgress tree,這棵樹在構建每個節點的時候會收集當前節點的反作用,整棵樹構建完成後,會造成一條完整的反作用鏈。
currentFiber 表示上次渲染構建的 Filber 樹。在每一次更新完成後 workInProgress 會賦值給 currentFiber。在新一輪更新時 workInProgress tree 再從新構建,新 workInProgress 的節點經過 alternate 屬性和 currentFiber 的節點創建聯繫。
在新 workInProgress tree 的建立過程當中,會同 currentFiber 的對應節點進行 Diff 比較,收集反作用。同時也會複用和 currentFiber 對應的節點對象,減小新建立對象帶來的開銷。也就是說不管是建立仍是更新、掛起、恢復以及終止操做都是發生在 workInProgress tree 建立過程當中的。workInProgress tree 構建過程其實就是循環的執行任務和建立下一個任務。
當第一個小任務完成後,先判斷這一幀是否還有空閒時間,沒有就掛起下一個任務的執行,記住當前掛起的節點,讓出控制權給瀏覽器執行更高優先級的任務。
在瀏覽器渲染完一幀後,判斷當前幀是否有剩餘時間,若是有就恢復執行以前掛起的任務。若是沒有任務須要處理,表明調和階段完成,能夠開始進入渲染階段。
使用前面提到的 RIC (RequestIdleCallback) 瀏覽器原生 API,React 源碼中爲了兼容低版本的瀏覽器,對該方法進行了 Polyfill。
答案是在前面提到的鏈表。在 React Fiber 中每一個任務其實就是在處理一個 FiberNode 對象,而後又生成下一個任務須要處理的 FiberNode。
其實並非每次更新都會走到提交階段。當在調和過程當中觸發了新的更新,在執行下一個任務的時候,判斷是否有優先級更高的執行任務,若是有就終止原來將要執行的任務,開始新的 workInProgressFiber 樹構建過程,開始新的更新流程。這樣能夠避免重複更新操做。這也是在 React 16 之後生命週期函數 componentWillMount 有可能會執行屢次的緣由。
React Fiber 除了經過掛起,恢復和終止來控制更新外,還給每一個任務分配了優先級。具體點就是在建立或者更新 FiberNode 的時候,經過算法給每一個任務分配一個到期時間(expirationTime)。在每一個任務執行的時候除了判斷剩餘時間,若是當前處理節點已通過期,那麼不管如今是否有空閒時間都必須執行該任務。過時時間的大小還表明着任務的優先級。
任務在執行過程當中順便收集了每一個 FiberNode 的反作用,將有反作用的節點經過 firstEffect、lastEffect、nextEffect 造成一條反作用單鏈表 A1(TEXT)-B1(TEXT)-C1(TEXT)-C1-C2(TEXT)-C2-B1-B2(TEXT)-B2-A。
其實最終都是爲了收集到這條反作用鏈表,有了它,在接下來的渲染階段就經過遍歷反作用鏈完成 DOM 更新。這裏須要注意,更新真實 DOM 的這個動做是一鼓作氣的,不能中斷,否則會形成視覺上的不連貫(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,咱們就有了在社區常常看到的兩張對比圖。
清晰展現及交互、源碼可經過下面兩個連接進入,查看網頁源代碼。
基於時間分片的增量更新須要更多的上下文信息,以前的vDOM tree顯然難以知足,因此擴展出了fiber tree(即Fiber上下文的vDOM tree),更新過程就是根據輸入數據以及現有的fiber tree構造出新的fiber tree(workInProgress tree)。
FiberNode 上的屬性有不少,根據筆者的理解,如下這麼幾個屬性是值得關注的:return、child、sibling(主要負責fiber鏈表的連接);stateNode;effectTag;expirationTime;alternate;nextEffect。各屬性介紹參看下面的class FiberNode
:
class FiberNode { constructor(tag, pendingProps, key, mode) { // 實例屬性 this.tag = tag; // 標記不一樣組件類型,如函數組件、類組件、文本、原生組件... this.key = key; // react 元素上的 key 就是 jsx 上寫的那個 key ,也就是最終 ReactElement 上的 this.elementType = null; // createElement的第一個參數,ReactElement 上的 type this.type = null; // 表示fiber的真實類型 ,elementType 基本同樣,在使用了懶加載之類的功能時可能會不同 this.stateNode = null; // 實例對象,好比 class 組件 new 完後就掛載在這個屬性上面,若是是RootFiber,那麼它上面掛的是 FiberRoot,若是是原生節點就是 dom 對象 // fiber this.return = null; // 父節點,指向上一個 fiber this.child = null; // 子節點,指向自身下面的第一個 fiber this.sibling = null; // 兄弟組件, 指向一個兄弟節點 this.index = 0; // 通常若是沒有兄弟節點的話是0 當某個父節點下的子節點是數組類型的時候會給每一個子節點一個 index,index 和 key 要一塊兒作 diff this.ref = null; // reactElement 上的 ref 屬性 this.pendingProps = pendingProps; // 新的 props this.memoizedProps = null; // 舊的 props this.updateQueue = null; // fiber 上的更新隊列執行一次 setState 就會往這個屬性上掛一個新的更新, 每條更新最終會造成一個鏈表結構,最後作批量更新 this.memoizedState = null; // 對應 memoizedProps,上次渲染的 state,至關於當前的 state,理解成 prev 和 next 的關係 this.mode = mode; // 表示當前組件下的子組件的渲染方式 // effects this.effectTag = NoEffect; // 表示當前 fiber 要進行何種更新(更新、刪除等) this.nextEffect = null; // 指向下個須要更新的fiber this.firstEffect = null; // 指向全部子節點裏,須要更新的 fiber 裏的第一個 this.lastEffect = null; // 指向全部子節點中須要更新的 fiber 的最後一個 this.expirationTime = NoWork; // 過時時間,表明任務在將來的哪一個時間點應該被完成 this.childExpirationTime = NoWork; // child 過時時間 this.alternate = null; // current 樹和 workInprogress 樹之間的相互引用 } }
圖片來源: 徹底理解React Fiber
function performUnitWork(currentFiber){ //beginWork(currentFiber) //找到兒子,並經過鏈表的方式掛到currentFiber上,每一偶兒子就找後面那個兄弟 //有兒子就返回兒子 if(currentFiber.child){ return currentFiber.child; } //若是沒有兒子,則找弟弟 while(currentFiber){//一直往上找 //completeUnitWork(currentFiber);//將本身的反作用掛到父節點去 if(currentFiber.sibling){ return currentFiber.sibling } currentFiber = currentFiber.return; } }
Concurrent Mode 指的就是 React 利用上面 Fiber 帶來的新特性的開啓的新模式 (mode)。react17開始支持concurrent mode,這種模式的根本目的是爲了讓應用保持cpu和io的快速響應,它是一組新功能,包括Fiber、Scheduler、Lane,能夠根據用戶硬件性能和網絡情況調整應用的響應速度,核心就是爲了實現異步可中斷的更新。concurrent mode也是將來react主要迭代的方向。
目前 React 實驗版本容許用戶選擇三種 mode:
Concurrent Mode 其實開啓了一堆新特性,其中有兩個最重要的特性能夠用來解決咱們開頭提到的兩個問題:
Pending -> Skeleton -> Complete
的更新路徑, 用戶在切換頁面時能夠停留在當前頁面,讓頁面保持響應。 相比展現一個無用的空白頁面或者加載狀態,這種用戶體驗更加友好。其中 Suspense 能夠用來解決請求阻塞的問題,UI 卡頓的問題其實開啓 concurrent mode 就已經解決的,但如何利用 concurrent mode 來實現更友好的交互仍是須要對代碼作一番改動的。
資料參考: Concurrent 模式介紹 (實驗性) | 理解 React Fiber & Concurrent Mode | 11.concurrent mode(併發模式是什麼樣的) | 人人都能讀懂的react源碼解析
Concurrent Mode只是併發,既然任務可拆分(只要最終獲得完整effect list就行),那就容許並行執行,(多個Fiber reconciler + 多個worker),首屏也更容易分塊加載/渲染(vDOM森林。
並行渲染的話,聽說Firefox測試結果顯示,130ms的頁面,只須要30ms就能搞定,因此在這方面是值得期待的,而React已經作好準備了,這也就是在React Fiber上下文常常聽到的待unlock的更多特性之一。
Facebook 在 Chromium 中提出並實現了 isInputPending() API
,它能夠提升網頁的響應能力,可是不會對性能形成太大影響。Facebook 提出的 isInputPending API
是第一個將中斷的概念用於瀏覽器用戶交互的的功能,而且容許 JavaScript 可以檢查事件隊列而不會將控制權交於瀏覽器。
目前 isInputPending API 僅在 Chromium 的 87 版本開始提供,其餘瀏覽器並未實現。
資料參考: Facebook 將對 React 的優化實現到了瀏覽器! | Faster input events with Facebook’s first browser API contribution
當下前端領域,三大框架React、Vue、Angular版本逐漸穩定,若是說前端行業會出現哪些框架有可能會挑戰React或者Vue呢?不少人認爲Svelte 應該是其中的選項之一。
Svelte叫法是[Svelte]
, 本意是苗條纖瘦的,是一個新興熱門的前端框架。在開發者滿意度、興趣度、市場佔有率上均名列前茅,同時,它有更小的打包體積,更少的開發代碼書寫,在性能測評中,與React、Vue相比,也不遑多讓。
Svelte 的核心思想在於『經過靜態編譯減小框架運行時的代碼量』。
Svelte 在編譯時,就已經分析好了數據 和 DOM 節點之間的對應關係,在數據發生變化時,能夠很是高效的來更新DOM節點。
資料參考: 新興前端框架 Svelte 從入門到原理