React Fiber 初探

發表於2018-06-20

一、為什麼需要React Fiber

React Fiber 初探

在React Fiber之前的版本,當React決定要載入或者更新元件樹時,會做很多事,但主要是兩個階段:

排程階段(Reconciler):這個階段React用新資料生成新的Virtual DOM,遍歷Virtual DOM,然後通過Diff演算法,快速找出需要更新的元素,放到更新佇列中去。

渲染階段(Renderer):這個階段React 根據所在的渲染環境,遍歷更新佇列,將對應元素更新。在瀏覽器中,就是跟新對應的DOM元素。除瀏覽器外,渲染環境還可以是 Native、硬體、VR 、WebGL等等。

React Fiber 初探

表面上看,這種設計也是挺合理的,因為更新過程不會有任何I/O操作,完全是CPU計算,所以無需非同步操作,執行到結束即可。

主要問題出現在,React之前的排程策略Stack Reconciler。這個策略像函式呼叫棧一樣,會深度優先遍歷所有的Virtual DOM節點,進行Diff。它一定要等整棵Virtual DOM計算完成之後,才將任務出棧釋放主執行緒。而瀏覽器中的渲染引擎是單執行緒的,除了網路操作,幾乎所有的操作都在這個單執行緒中執行:解析渲染DOM tree和CSS tree、解析執行JavaScript,這個執行緒就是瀏覽器的主執行緒。

假設更新一個元件需要1ms,如果有200個元件要更新,那就需要200ms,在這200ms的更新過程中,瀏覽器唯一的主執行緒都在專心執行更新操作,無暇去做任何其他的事情。

想象一下,在這200ms內,使用者往一個input元素中輸入點什麼,敲擊鍵盤也不會獲得響應,因為渲染輸入按鍵結果也是瀏覽器主執行緒的工作,但是瀏覽器主執行緒被React佔用,抽不出空,最後的結果就是使用者敲了按鍵看不到反應,等React更新過程結束之後,那些按鍵會一下出現在input元素裡,這就是所謂的介面卡頓。

React這樣的排程策略對動畫的支援也不好。如果React更新一次狀態,佔用瀏覽器主執行緒的時間超過16.6ms,就會被人眼發覺前後兩幀不連續,呈現出動畫卡頓。

過去的優化都是停留在JavaScript層面(Virtual DOM的 create/diff):如減少元件的複雜度(Stateless)、減少向下diff的規模(SCU)、減少diff的成本(immutable.js)…這些都並不能解決執行緒的問題。React希望通過Fiber重構來改變這種現狀,進一步提升互動體驗。

二、什麼是React Fiber

React Fiber之前的Stack Reconciler,在首次渲染過程中構建出Virtual DOM tree,後續需要更新時,diff Virtual DOM tree得到DOM change,並把DOM change應用(patch)到DOM樹。

其執行時存在以下三種例項:

  • DOM: 真實的DOM節點。
  • Elements:主要是描述UI長什麼樣子(type, props)。
  • Instances: 根據Elements建立的,對元件及DOM節點的抽象表示,Virtual DOM tree維護了元件狀態以及元件與DOM樹的關係。

React Fiber 初探

React Fiber解決過去Reconciler存在的問題的思路是把渲染/更新過程(遞迴diff)拆分成一系列小任務,每次檢查樹上的一小部分,完成後確認否還有時間繼續下一個任務,存在時繼續,不存在下一個任務時自己掛起,主執行緒不忙的時候再繼續。

React Fiber將元件的遞迴更新,改成連結串列的依次執行,擴充套件出了fiber tree,即Fiber上下文的Virtual DOM tree,更新過程根據輸入資料以及現有的fiber tree構造出新的fiber tree(workInProgress tree)。

因此,執行時的例項變更為這種結構:

  • DOM: 真實的DOM節點。
  • effect: 每個workInProgress tree節點上都有一個effect list用來存放diff結果,當前節點更新完畢會queue收集diff結果,向上merge effect list。
  • workInProgress:workInProgress tree是reconcile過程中從fiber tree建立的當前進度快照,用於斷點恢復。
  • fiber:fiber tree與Virtual DOM tree類似,用來描述增量更新所需的上下文資訊。
  • Elements:主要是描述UI長什麼樣子(type, props)。

React Fiber 初探

fiber tree實際上是個單連結串列(Singly Linked List)樹結構。Fiber的拆分單位是fiber tree上的一個節點fiber,按Virtual DOM節點拆,因為fiber tree是根據Virtual DOM tree構造出來的,樹結構一模一樣,只是節點攜帶的資訊有差異。所以,實際上Virtual DOM node粒度的拆分以fiber為工作單元,每個元件例項和每個DOM節點抽象表示的例項都是一個工作單元。工作迴圈中,每次處理一個fiber,處理完可以中斷/掛起整個工作迴圈。

2、reconcile

React Fiber 初探

React Fiber把渲染/更新過程分為兩個階段:

1)可中斷的render/reconciliation 通過構造workInProgress tree得出change。

2)不可中斷的commit 應用這些DOM change。

第一階段render/reconciliation具體實現為以fiber tree為藍本,把每個fiber作為一個工作單元,自頂向下逐節點構造workInProgress tree(構建中的新fiber tree)。

以元件節點為例,具體過程如下:

1)如果當前節點不需要更新,直接把子節點clone過來,跳到5;要更新的話打個tag。

2)更新當前props, state, context等節點狀態。

3)呼叫should Component Update(),false的話,跳到5。

4)呼叫render()獲得新的子節點,併為子節點建立fiber。建立過程會盡量複用現有fiber,子節點增刪也發生在這裡。

5)如果沒有產生child fiber,該工作單元結束,把effect list歸併到return,並把當前節點的sibling作為下一個工作單元;否則把child作為下一個工作單元。

6)如果沒有剩餘可用時間了,等到下一次主執行緒空閒時才開始下一個工作單元;否則,立即開始做。

7)如果沒有下一個工作單元了,回到了workInProgress tree的根節點,第1階段結束,進入pending Commit狀態。

實際上是1->6的工作迴圈,7是出口,工作迴圈每次只做一件事,做完看要不要休息。工作迴圈結束時,因為每做完一個都向上歸併,workInProgress tree根節點身上的effect list就是收集到的所有side effect。

所以,構建workInProgress tree的過程就是diff的過程,通過request Idle Callback來排程執行一組任務,每完成一個任務後回來看看有沒有插隊的(更緊急的),每完成一組任務,把時間控制權交還給主執行緒,直到下一次request Idle Callback回撥再繼續構建workInProgress tree。

這一階段是沒有副作用的,因此這個過程可以被打斷,然後恢復執行。

第二階段commit:第一階段產生的effectlist只有在commit之後才會生效,也就是真正應用到DOM中。這一階段往往不會執行太長時間,因此是同步(所謂的一次性)的,這樣也避免了元件內檢視層結構和DOM不一致。

3、workInProgress tree

React Fiber 初探

在React Fiber中使用了雙緩衝技術(double buffering),像redux裡的nextListeners,以fiber tree為主,workInProgress tree為輔。

雙緩衝具體指的是workInProgress tree構造完畢,得到的就是新的fiber tree,每個fiber上都有個alternate屬性,也指向一個fiber,建立workInProgress節點時優先取alternate,沒有的話就建立一個。

fiber與workInProgress互相持有引用,把current指標指向workInProgress tree,丟掉舊的fiber tree。舊fiber就作為新fiber更新的預留空間,達到複用fiber例項的目的。

4、優先順序策略

React Fiber為了更好的進行任務排程,會給不同的任務設定不同優先順序。

React Fiber切分任務並呼叫requestIdleCallback和requestAnimationFrame API,保證渲染任務和其他任務,在不影響應用互動,不掉幀的前提下,穩定執行。而實現排程的方式正是給每一個fiber例項設定到期執行時間,不同時間即代表不同優先順序,到期時間越短,則代表優先順序越高,需要儘早執行。

React Fiber 初探

synchronous首屏(首次渲染)用,要求儘量快,不管會不會阻塞UI執行緒。animation通過requestAnimationFrame來排程,這樣在下一幀就能立即開始動畫過程;後3個都是由requestIdleCallback回撥執行的;offscreen指的是當前隱藏的、螢幕外看不見的元素。高優先順序的比如希望立即得到反饋的鍵盤輸入,低優先順序的比如網路請求,讓評論顯示出來等等。另外,緊急的事件允許插隊。

5、生命週期

因為render/reconciliation階段可能執行多次,會導致willXXX鉤子執行多次。所以getDerivedStateFromProps取代了原本的componentWillMount與componentWillReceiveProps方法,而componentWillUpdate本來就是可有可無所以也被廢棄了。

進入commit階段時,元件多了一個新鉤子叫getSnapshotBeforeUpdate,它與commit階段的鉤子一樣只執行一次。

出錯時,在componentDidMount/Update後,可以使用componentDidCatch方法。

三、總結

React Fiber最終提供的新功能主要是:

  • 可切分,可中斷任務。
  • 可重用各分階段任務,且可以設定優先順序。
  • 可以在父子元件任務間前進/後退切換任務。
  • render方法可以返回多元素(即可以返回陣列)。
  • 支援異常邊界處理異常。

通過本文主要了解了React Fiber的基本實現和其產生的原因,其本身的還有更多的優化和實現細節,可以檢視原始碼或參考其他文章進行更深入的瞭解。

  • 參考文章

1)react-fiber-architecture

來源: https://github.com/acdlite/react-fiber-architecture

2)Lin Clark

來源: https://conf.reactjs.org/speakers/lin

相關文章