如何理解 React Fiber 架構?

在 Hacker News 上看到一篇叫做 React Fiber architecture 的文章,作者是 React 項目的開發者,文章說 React Fiber 是對 React 近兩年開發工作的總結,將會重新實現 React 的核心演算法,並且會提供增量渲染等新特性。

文章:GitHub - acdlite/react-fiber-architecture: A description of React"s new core algorithm, React Fiber

Hacker News 討論:React Fiber Architecture


在 http://conf.reactjs.org/ 上,Lin Clark 通過漫畫為我們介紹 Fiber,結合她的介紹,我談談我的理解:

Fiber 可以提升複雜React 應用的可響應性和性能。Fiber 即是React新的調度演算法(reconciliation algorithm)

react 即 reconsiler(調度者),react-dom則是 renderer。調度者一直都是又 React 本身決定,而 renderer 則可以由社區控制和貢獻。

那新的調度演算法是如何優化可響應性和性能的呢 ?

每次有 state 的變化 React 重新計算,如果計算量過大,瀏覽器主線程來不及做其他的事情,比如 rerender 或者 layout,那例如動畫就會出現卡頓現象。

React 制定了一種名為 Fiber 的數據結構,加上新的演算法,使得大量的計算可以被拆解,非同步化,瀏覽器主線程得以釋放,保證了渲染的幀率。從而提高響應性。

React 將更新分為了兩個時期:

render/reconciliation

可打斷,React 在 workingProgressTree 上復用 current 上的 Fiber 數據結構來一步地(通過requestIdleCallback)來構建新的 tree,標記處需要更新的節點,放入隊列中。

commit

不可打斷。在第二階段,React 將其所有的變更一次性更新到DOM上。

除此之外,還有更多的優化細節,可以參看 Lin Clark 的演講視頻。

廣告時間

歡迎關注 前端外刊評論 - 知乎專欄,外刊君將會代碼 React Conf 2017 的全部解讀。也可以微信、微博搜索 FrontendMagazine 關注,期待後續。


正好這幾天在翻react的源碼,寫了篇這部分的文章:xieyu/blog


分析原文,作者的思路是這樣的:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

(1)發現痛點

To go further, let"s go back to the conception of React components as functions of data, commonly expressed as v = f(d).

It follows that rendering a React app is akin to calling a function whose body contains calls to other functions, and so on. This analogy is useful when thinking about fibers.

When dealing with UIs, the problem is that if too much work is executed all at once, it can cause animations to drop frames and look choppy.

React的UI解決方案是,View = F(Data),

頁面中的所有相關的React Components共同組成了F,Components之間是互相調用的關係。

頁面複雜的話,這個調用棧會很深,導致UI變卡。

(一旦執行,我們就不能干擾它。。

(2)腦洞

Wouldn"t it be great if we could customize the behavior of the call stack to optimize for rendering UIs? Wouldn"t it be great if we could interrupt the call stack at will and manipulate stack frames manually?

如果可以隨意操縱這些調用棧就好了,容我先返回搞一下UI,接著再計算可否?

That"s the purpose of React Fiber. Fiber is reimplementation of the stack, specialized for React components. You can think of a single fiber as a virtual stack frame.

我還是起一個高大上的名字吧,我稱這個為「虛擬調用棧」。

(與「虛擬DOM」起到了遙相呼應的效果。。

(3)設計(測試驅動開發

We"ve established that a primary goal of Fiber is to enable React to take advantage of scheduling. Specifically, we need to be able to

a) pause work and come back to it later.

b) assign priority to different types of work.

c) reuse previously completed work.

d) abort work if it"s no longer needed.

說白了,我們就是想實現一個任務調度器。

它可以暫停一個任務,一會再回來執行,還能給任務分配優先順序,

或者重新啟用一個已停止的任務,刪掉某個不再使用的任務。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

這篇文章看下來,覺得此項工作在其他語言環境中提及的話並沒有什麼特別的。

只不過JavaScript是用戶代碼單線程的語言,容易讓人誤以為執行的任務是無法調度的。

其實JavaScript有自己的並發模型,詳見: Concurrency model and Event Loop

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

如果作者說的這些,你總覺得在哪裡見過,請移步:Ruby Fiber


寫了一篇專欄文章 React Fiber是什麼 - 知乎專欄 ,沒有詳細介紹React Fiber架構細節,因為這玩意現在還沒有正式發布,很多東西也不是很清楚,但是作為快餐迅速了解React Fiber是啥我們要注意些啥還是有些用吧。


看了看文檔有了個大概的理解,有誤的話趕緊通知我不要誤導了別人=。=

React 的核心思想是每次對於界面 state 的改動,都會重新渲染整個 virtual dom,然後新老的兩個 virtual dom 樹進行 diff,對比出變化的地方,然後通過 renderer 渲染到實際的UI界面(這裡可能是瀏覽器的DOM,也可能是native組件)。這樣實質上就是把界面變成一個純粹的狀態機,React 的作用就是把這個狀態機之間的狀態轉換高效率地運行出來。

但是存在以下問題:

1、不是每一次狀態的變化都要立刻執行。

2、不同的狀態變化之間是有輕重緩急之分的,比如『動畫』這種狀態變化的優先順序,出於對用戶體驗的考量,為了避免動畫卡頓或者掉幀,一般比『改變頁面數據』的優先順序更高。

3、我們現在的做法只是調用 setState 觸發重新渲染,然後 React 會收集一個 tick 內的 state 變化,然後執行(這部分感謝 @aji 的指正),所以有可能大量的計算會在同一時刻阻塞進程。但我們沒法控制 React 運算的時序問題,也不太可能通過手工聲明讓動畫的優先順序比數據變更更高。而 React 作為一個用戶交互的框架,它本應該能讓程序員能控制這些東西。

所以這個破事要怎麼解決咧?( ⊙ o ⊙ )

我們知道,任何的函數調用都會有自己的調用棧,比如對於 v = f(d) 這個函數來說,函數 f 可能又調用了一系列其它的函數,這些函數就包括在 f 的調用棧中。關鍵的問題在於,這種原生的調用棧是基本不可延遲的,它會立即執行,如果計算量很大的話就會阻塞住進程,讓界面失去響應,這種事情經常發生在 React 的渲染過程中。

所以這個時候就需要 fiber 這個東西,如果說 virtual dom 是對於實際 UI 的一種抽象的數據結構,那麼 fiber 就是一種 virtual stack,它是對於調用棧的一種重新實現的數據抽象。每一個 fiber 就是一個計算單元,可以任意修改它的優先順序,可以在任何時候讓它執行。這樣我們就擁有了調度計算的能力,比如在動畫進行的時候停止其它大計算量的工作,直到動畫結束再這些工作繼續。

至於 fiber 的結構以及它是怎麼實現的。。。介紹得太籠統了,我還真沒看懂=。=

而且看樣子這個介紹 fiber 結構的文章還遠沒有完結,先等著唄。。。


大晚上看到,躺在床上隨手畫了個示意圖…

或許可以幫助理解?

最大亮點大概還是 Virtual Call Stack

Fiber === Virtual Stack Frame

其他細節還是得看原文,原因啊,優點啊, Fiber 的 Implementation Details 啊什麼的。

手機答,以上。


說白了就是重新實現個協程, 比 ES6 generator 好的地方是不用手動插 yield point 而是在解釋器里暫停, 另外可以自己調度, 可以實現鬼畜控制流, 可以造更多開發工具...

具體怎麼實現? 第一當然是把本來存在局部變數/臨時變數的元素放到自己控制的數組裡, 可以寫解釋器執行 fiber, 或者更直白的, 編譯器直接把 jsx 編譯成 generator 然後在裡面插一些 yield.


通讀了兩遍文章。作者自己也說 "this document is nowhere near complete",所以我看完後,覺得也就是對 Fiber 有了個模糊印象吧。

- - - - - - - -

先是作者強烈推薦閱讀的四篇文章:

  • React Components, Elements, and Instances

  • Reconciliation

  • React Basic Theoretical Concepts

  • React Design Principles

講的都是 React 的一些基本概念和思想,不是很難。

React 是狀態驅動的,它實現了一個叫做 Reconciliation 的演算法,用於狀態轉移時對組件樹的快速比對,從而計算出需要對組件樹進行哪些 Update 操作。

Reconciliation 的時間複雜度僅為 O(n) 而不是 O(n^3) 的部分原因是因為如下優化:

  • 不同的 Component Type 生成不同的組件樹。
  • Lists 比對時,通過唯一穩定可預測的 key 來識別元素。

作者還指出 "Virtual DOM" 是一個不精確的說法,因為 DOM 不是唯一可被 render 的,還有 native iOS 和 Android 呢。但是 Reconciliation 演算法是通用的。

- - - - - - - -

下面出現了兩個概念:

  • Scheduling(時序)決定了事務應在何時執行。

  • Work(事務)執行計算,更新狀態。

狀態轉移時,是在一次 tick 中遞歸遍歷組件樹,找出需要更新的節點 rerender。但是這樣造成了一些問題:

  • 在 UI 中,不是所有的狀態轉移都需要立即執行。大量的同時計算可能會導致資源的浪費,以致出現掉幀的狀況,降低用戶體驗。
  • 不同類型的狀態轉移應有不同的優先順序,比如點擊按鈕出現動畫的優先順序應該比 Fetch API 要高。
  • React 是 pull-based 實現的,事務的時序全部由 React 決定。我們沒辦法操控執行事務的時序。

- - - - - - - -

出於以上的考慮,Fiber 重新實現了 Reconciler。目的是為了做到:

  • 暫停一個事務,並在不久後再重新開始執行。
  • 設定不同類型事務的優先順序。
  • 復用已完成的事務。
  • 當不再需要時,取消該事務。

為了實現以上目的,需要將事務分解成事務單元 (unit of work)。一個 Fiber 就代表了一個事務單元。

我們都知道 React 渲染界面是函數式的,滿足 view = f(data)。調用渲染函數時,會將該函數的棧幀壓入棧中,這個棧幀就代表了該函數執行的事務。

那麼,如果能手工操作這個函數調用棧,就能定製事務的時序,達到優化 UI 渲染的目的了。而這就是 React Fiber 的初衷。Fiber 重新實現了 Component 渲染函數的調用棧,一個 Fiber 也可以被看做一個虛擬棧幀 (virtual stack frame)

現在,我們就能隨心所欲地按照既定的時序執行事務了。

- - - - - - - -

在具體實現上,Fiber 是一個包含組件信息的對象,與棧幀和組件相關聯。

下面是一些 Fiber 的重要組成部分:

  • type 和 key:type 決定了 fiber 與哪個組件相關聯,類型為函數,組件類或字元串。如果有看上面四篇文章應該就很清楚的啦。而 key 用在 Reconciliation 中,決定 fiber 是否可被複用。
  • child 和 sibling:描述組件樹的結構。

function Parent() {
return &
}

Parent 的 child fiber 就與 Child相關聯。

下面出現了一個令人驚喜的未來新特性:

function Parent() {
return [&, &]
}

render 函數能返回不止一個節點了。多個節點被組織成單鏈表,Parent 的 child fiber 與 Child1 相關聯,而 Child1 的 sibling fiber 與 Child2 相關聯。

  • return:上例中 Child1 和 Child2 的 fiber 都與 Parent 相關聯。
  • pendingWorkPriority:一個數字,決定了事務的優先順序。直接引源碼過來啦,一看就懂。

"use strict";

export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;

module.exports = {
NoWork: 0, // No work is pending.
SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.
AnimationPriority: 2, // Needs to complete before the next frame.
HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive.
LowPriority: 4, // Data fetching, or result from updating stores.
OffscreenPriority: 5, // Won"t be visible but do the work in case it becomes visible.
};

  • alternate:出現了兩個概念,flush 和 work-in-progress,flush 是組件已渲染完成後的 fiber 狀態,而 work-in-progress 則是未完成的,意味著棧幀還未被彈出。任何時候,組件實例可與至多兩個 fiber 相關聯,就是上述兩種類型。flushed fiber 的 alternate 是 work-in-progress fiber,反之亦然。【其實這段我根本沒看懂作者想說什麼……頂鍋蓋逃QAQ
  • output:渲染函數的輸出結果,只在組件樹葉節點的原生組件(如 div span)相關聯的 fiber 處生成。

- - - - - - - -

文章大概就這些內容。文末處,作者還說會有更詳細的後續文章介紹,拭目以待咯~


感覺是:

把節點的計算抽象成了一個數組的鏈式調用,可以對數組進行插入刪除節點的操作.

為什麼要這麼做,大家都說得很清楚了.


抽空將文章翻譯成了中文,看完應該對React Fiber有大致了了解,React Fiber架構 · Yet Another Programmer"s Notes

Fiber的核心思想是將渲染分割成多個事務,更新的時候根據事務優先順序來調度執行順序。之前的更新是直接從父節點遞歸子節點壓棧,執行,彈棧,沒有優先順序,也不能控制順序;Fiber為了實現調度功能,重構了棧,即虛擬棧(virtual stack),這樣棧的執行順序就能定製了,調度、暫停、終止、復用事務都成為可能。


Fiber是一種輕量的執行線程,同線程一樣共享定址空間,線程靠系統調度,並且是搶佔式多任務處理,Fiber 則是自調用,協作式多任務處理。

看起來React Fiber很厲害的樣子,如果要用的話,還是有一些問題是需要考慮的。

比如說,task按照優先順序之後,可能低優先順序的任務永遠不會執行,稱之為starvation;

比如說,task有可能被打斷,需要重新執行,那麼某些依賴生命周期實現的業務邏輯可能會受到影響。

……

React Fiber也是帶來了很多的好處的。

比如說,增強了某些領域的支持,如動畫、布局和手勢;

比如說,在複雜頁面,對用戶的反饋會更及時,應用的用戶體驗會變好,簡單頁面看不到明顯的差異;

比如說,api基本上沒有變化,對現有項目很友好。

……

現在,react-fiber已經通過了所有的測試,在網站上面Is Fiber Ready Yet?http://isfiberreadyyet.com/,已經通過了所有的測試,還有4個warning需要fix。它會隨著React 16發布,到底效果怎麼樣,只有用過才知道了。

文/ThoughtWorks劉傑鳳 原文:從React到React Fiber


之前的演算法是一個遞歸的過程,不管三七二十一,重新執行計算,很暴力

新版的 fiber 演算法,讓這一過程可控,比如可以重新利用之前的計算

另外吐槽一下react的源碼,真的很難看懂啊……


先佔個坑,提示:簡單來說,是個輕量級線程調度方式,可以對比 Ruby 的 fiber 來看~


React認為一個組件應該具有如下特徵:

(1)可組合(Composeable):一個組件易於和其它組件一起使用,或者嵌套在另一個組件內部。如果一個組件內部創建了另一個組件,那麼說父組件擁有(own)它創建的子組件,通過這個特性,一個複雜的UI可以拆分成多個簡單的UI組件;

(2)可重用(Reusable):每個組件都是具有獨立功能的,它可以被使用在多個UI場景;

(3)可維護(Maintainable):每個小的組件僅僅包含自身的邏輯,更容易被理解和維護;

(4)可測試(Testable):因為每個組件都是獨立的,那麼對於各個組件分別測試顯然要比對於整個UI進行測試容易的多。http://tinyurl.com/hbapn5s


推薦閱讀:

有哪些設計精美的網頁?
如何解決a標籤nest問題?
為什麼 CSS 人員被稱為「網頁重構工程師」?
哪裡可以比較系統的學習前端代碼瀏覽器兼容問題?
你寫過最複雜的表單頁面是怎樣的,你是怎麼解決的?

TAG:前端開發 | React |