React Fiber架構

介紹

對於一些react語境下的術語不翻譯。原文javascript

React Fiber是對React核心算法的從新實現。這是一個正在進行中的項目。到目前爲止(指2016年),React團隊已經對此進行了爲期兩年的研究和調研。html

React Fiber的目標是增長React對動畫,佈局和手勢等領域的是適配性(suitability)。React Fiber的頭等特性是incremental rendering(增量渲染)。何爲增量渲染?那就是一種將rendering work切割成chunks,而後將其分紅多個幀。java

其實比較關鍵的特性包括有暫停,中斷和在更新流程中複用work;對各類不一樣類型的更新任務賦予不一樣的優先級(priority);爲concurrency做鋪墊。react

關於本文檔

React Fiber引入了幾種新穎的概念。這些概念每每不能僅僅靠經過查閱源碼就能理解的。 這個文檔純粹是我我的跟進React Fiber項目實現細節的過程當中記下來的筆記。隨着這種筆記記得愈來愈多,我意識到這些筆記可能對別人也會有用。git

我打算用最簡單,通俗易懂的語言來描述和表達,避免一上來就給你展現各類術語的艱澀難懂的定義。我也儘量地給出一些比較重要的外部資源的連接。github

注意,我不是React團隊的成員,個人話並不具有權威性。所以,這不是官方的權威文檔。不過,我已經諮詢過幾個React團隊的成員,以確保所表達內容的準確性。算法

這份文檔會隨時變動,由於我感受React Fiber項目在完成以前隨時都會進行一些重大的重構。固然,我也會嘗試記錄它的設計的變動。任何關於該文檔的優化和建議都是十分歡迎的。c#

個人目的是,在讀完這邊文檔以後,你在跟進它已經實現的部分過程當中,對React Fiber會有足夠的瞭解。甚至最後,你可以反過來爲React社區作出你本身的貢獻。瀏覽器

先決知識

在繼續閱讀前,我強烈建議你過目一下如下羅列出來的文章:性能優化

重溫

請檢查一下你是否已經閱讀過「先決知識」章節。若是沒有,建議你去閱讀一下。在咱們深刻接觸新東西以前,咱們不妨重溫幾個概念。

什麼是reconciliation?

reconciliation

是一種react用來diff(比對)兩顆節點(好比react element)樹,從而決定哪一部分須要更新的一種算法。

update

致使React app從新渲染的數據更改。一般來講,setState就會致使一個更新。從新渲染做爲更新的一個結果而存在。

React API的中心思想是這樣的:一次update等同於整個app的從新渲染。這種設計有助於開發者用聲明式的思惟方式去理解app是如何高效地將狀態轉換的(A to B, B to C, C to A),而不是用命令式的思惟方式去操心期間的細節。

不過,每一次數據的改變致使整個app的從新渲染只會發生在一些不過重要app上。在真實的世界裏,通常不會這麼作。由於這麼作會致使很昂貴的性能消耗。React已經在呈現最新界面的同時幫咱們作好了性能優化。大部分這些優化就是咱們所提reconciliation算法的一部分。

大衆所熟知的"virtual DOM"的背後就是reconciliation算法。一個高水平的描述是這樣的:當你渲染一個react app的時候,那麼react就會生成一顆用於描述該app的節點樹(譯者注:這個節點就是指react element),並保存在內存當中。而後,這個節點樹會被flush到相應的渲染平臺上-舉個例子,對於瀏覽器應用來講,「節點樹會被flush到相應的渲染平臺上」具體點講就是進行一系列的DOM操做。一旦app被更新過(經過調用setState),一個新的節點樹就會被生成了。新的節點樹會跟存在於內存當中的那顆舊的節點樹進行比對,從而計算出更新整個app界面所須要的具體操做。

雖然,React Fiber是對reconciler的重寫,可是依據React doc中對高層算法的描述,重寫先後,reconciler仍是有大量的相同處。比較關鍵的兩點是:

  • 假設不一樣「組件類型」的組件會生成大致不一樣的的節點樹。對於這種狀況(不一樣「組件類型」的組件的更新),react不會對它們使用diff算法,而是簡單粗暴地將老的節點樹徹底替換爲新的節點樹。
  • 使用key這個prop來diff列表。key應該是「穩定(譯者注:也就是說不能用相似於Math.random()來生成key的prop值),可預測的和惟一的」。

reconciliation與rendering

DOM只是React可以適配的的渲染平臺之一。其餘主要的渲染平臺還有IOS和安卓的視圖層(經過React Native這個renderer來完成)。

react之因此可以適配到這麼多的渲染平臺,那是由於react是被設計成兩個獨立的階段:reconciliation和rendering。reconciler經過比對節點樹來計算出節點樹中哪部分須要更改;renderer負責利用這些計算結果來作一些實際的界面更新動做。

這種分離的設計意味着React DOM和React Native能使用獨立的renderer的同時公用相同的,由React core提供的reconciler。React Fiber重寫了reconciler,但這事大體跟rendering無關。不過,不管怎麼,衆多renderer確定是須要做出一些調整來整合新的架構。

Scheduling

Scheduling

決定什麼時候(譯者注:這是個關鍵詞)應該執行work的那部分程序

work

須要被執行的全部計算。work(這個概念)通常意義下是做爲一次更新的結果而存在的。

React的Design Principles文檔在這個話題上闡述得很是好,所以,我在這裏會引用它:

在當前實現中,React會遞歸地遍歷節點樹,相應地調用這顆已更新的節點樹上的render方法。然而,在未來,React會延遲某些更新來避免界面更新的掉幀。

這是一個React設計中常見的主題。Some popular libraries implement the "push" approach where computations are performed when the new data is available. React, however, sticks to the "pull" approach where computations can be delayed until necessary.

React不是一個通用的數據處理類庫。它是一個用於構建用戶界面的類庫。可以知道app哪些計算跟當前更新是相關的,哪些是不相關的,這種能力在構建用戶界面方面有獨到的優點。

若是某些東西用戶經過屏幕看不到的話,咱們能夠將它所關聯的邏輯延遲執行。若是數據的到達速度比幀率還要快的話,咱們能夠對數據進行合併,採用批量更新的策略 。咱們能夠把來自於用戶界面的work(好比說,經過一個button的點擊觸發了某個動畫)的優先級定得比一些跑在後臺,不過重要的work(經過網絡請求把一些內容渲染到屏幕上)的優先級要高。經過這麼作,咱們能夠防止用戶所看到的界面出現掉幀的現象。

上面給出的文檔的關鍵點以下:

  • 在UI開發中,沒有必要將每個更新請求付諸實施。實際上,若是真的是這麼作的話,那麼界面就會出現掉幀的現象,這大大下降了用戶體驗。
  • 不一樣類型的更新請求應該由不一樣的優先級。好比,執行動畫的優先級應該要比一個來自於data store的更新的優先級要高。
  • push-based的方案要求你(開發者)須要手動去調度work。而pull-based的方案則可以讓框架(react)學得變聰明,而後幫你去作這些調度工做。

react當前(2016年)對scheduling的應用度尚未足夠大。一次更新意味着(簡單粗暴地)對整個節點樹進行一個完整的重渲染(譯者注:級聯式的層層更新)。重寫react的核心算法就是爲了利用scheduling帶來的優點。這應該就是React Fiber項目的初心。

如今,咱們已經準備好深刻到React Fiber的實現當中去了。下一節,咱們將會提到愈來愈多的技術性東西。在繼續閱讀以前,請確保你對上面章節所提到的內容已經消化理解得差很少了。

什麼是React Fiber?

咱們將要討論React Fiber的核心了。React Fiber這個抽象層級比你想象中的還要低。若是你發現你本身在嘗試理解這個架構的過程當中苦苦掙扎的話,不要氣餒哈。不要放棄,繼續嘗試,你終將會弄明白它的(當你終於理解了React Fiber,麻煩給點關於完善這一小節內容的建議)。

咱們在上面已經確認了React Fiber的首要目標是使得React可以整合scheduling。更具體地來講,是咱們能作到一下幾點:

  • 中斷work和稍後恢復work
  • 對不一樣類型的work賦予至關的優先級
  • 複用以前已經完成的work
  • 若是一個work已經不須要繼續了,中斷它。

爲了可以作到以上幾點,咱們須要將work拆分紅一個個的單元。這個單元其實就是React Fiber。一個React Fiber表明着一個work單元

再繼續闡述前,咱們不妨重溫一下這個概念:React components as functions of data,能夠用通俗的方式來表達:

v = f(d) // view = f(data)
複製代碼

所以,咱們能夠作這樣的等價的心智模型:渲染一個 React app 就等同於調用一個函數,而後這個函數體裏面又包含了對其實其它函數的調用......與此類推。這種心智模型對理解React Fiber十分之有用。

計算機是經過call stack 來追蹤程序的執行過程的。一個函數一旦被執行,它就會成爲stack frame,加入到stack中。這一幀stack frame表明着這個函數所要完成的一個work。

問題就是,當跟UI打交道的的時候,一次性會有不少的work被執行。這會致使動畫掉幀從而看起來不太流暢。還有就是,一些work若是會立刻被後面的work所取代的話,那麼立刻執行這個work就顯得不必了。This is where the comparison between UI components and function breaks down, because components have more specific concerns than functions in general.

新加入的渲染平臺(好比react native)實現了一些用於專門處理這個問題的API:requestIdleCallback和requestAnimationFrame。requestIdleCallback負責將一些低優先級的函數安排在空閒期間執行;requestAnimationFrame負責將一個高優先級的函數安排在下一個動畫幀期間調用。如今的問題是,爲了可以使用這種API,你必須找到一種方法將rendering work拆分紅各類增量單元(incremental units)。若是咱們僅僅依靠call stack,彷佛是不行的。由於call stack會一直執行work單元,直到call stack清空爲止。

若是可以經過自定義call stack的行爲來優化UI渲染豈不是很棒嗎?若是咱們能手動地打斷call stack和操做call stack的每個幀豈不是很棒嗎?

而這就是React Fiber的目的。React Fiber是對stack的從新實現,特別是爲了React組件而做的實現。你能夠把單獨的一個Fiber理解爲一個virtual stack frame。

對stack的從新實現的好處是,你能將stack frame保存在內存中,本身想何時,在哪裏執行均可以。這對於咱們實現引入scheduling時所設下的目標很重要。

重寫stack,除了能實現scheduling以外,手動處理stack frames還能讓實現一些新特性(好比:concurrency 和error boundaries)成爲了可能。咱們將會在將來的一些章節來討論這些話題。

在下一節,咱們更深刻地看看一個Fiber的數據結構。

Fiber的數據結構

注意:隨着愈來愈深刻到技術細節,那麼這些細節所面臨更改的可能性也會隨着增長。若是你發現了任何錯誤或者過時的信息,麻煩發個PR給我。

具體而言,一個fiber其實就是一個javascript對象。這個對象包含了一些關於組件的信息:這個組件的輸入,這個組件的輸出。

一個fiber對應於一幀stack frame,同時也對應於一個組件的實例。

如下是fiber對象的一些比較重要的字段(這個列表也不太詳盡)。

type 和 key

fiber對象的type和key字段跟react element的type和key字段的含義是一致的。(實際上,fiber對象是從react element建立而來的,在建立的時候,這兩個字段會被直接地copy過來)。

fiber對象的type字段描述的就是它所對應的組件。對於composite components來講,這個type字段值就是一個function或者class component(譯者注:本質上也是一個function)。對於host components(好比,div,span等)而言,type字段的值就是字符串。

理論上說,type字段的值就是被stack frame追蹤它的執行的那個函數(v = f(d ))。

跟type字段同樣,key字段被用於reconciliation期間決定是否要複用該fiber。

child 和 sibling

這兩個字段都是指向其它的fiber,共同組成了具備遞歸結構的fiber樹。

child fiber就是組件的render方法的返回值。舉個例子:

function Parent(){
    return <Child />
}
複製代碼

Parent的child fiber就是Child。

sibling字段存在於那些從render返回的多個children的身上(fiber的新特性):

function Parent() {
  return [<Child1 />, <Child2 />]
}

複製代碼

上面全部的child fiber組成了一個單(向)鏈表。第一個child就是這個單鏈表的頭節點。在這個示例中,Child1是Parent的child fiber,Child2是Child1的 sibling child。

不妨回頭看看咱們以前用function作過的類比,你能夠把child fiber理解爲一個tail-called function

return

return fiber是指程序在處理完當前這個fiber所返回的那個fiber。概念上,它是等同於返回的stack frame的地址(It is conceptually the same as the return address of a stack frame)。咱們也能夠把它看成parent fiber。

若是一個fiber有多個child fiber,這些child fiber的return fiber就是它們的parent fiber(此處的return有點是「上游」的意思)。因此,在咱們上面所提到的示例中, Child1和Child2的return fiber就是Parent。

pendingProps 和 memoizedProps

概念上說,props就是函數的參數。在函數剛開始執行的時候fiber的pendingProps就會被設置上,在函數執行完畢,fiber的memoizedProps也會被設置上。

當函數再次被執行,一個新的pendingProps會被計算出來,若是它與fiber的memoizedProps字段值相等的話,那麼這就至關於告訴咱們fiber以前的output能夠複用,從而阻止了沒必要要的work。

pendingWorkPriority

一個用於指示優先級的數值。誰的優先級呢?fiber所表明的work的優先級。ReactPriorityLevel 模塊列舉出了全部work的優先級,而且也羅列了這些work所表明着什麼。

除了「noWork」這個特例外(它的優先級數值是0),數值越大表明着優先級越低。舉個例子,你能夠用如下的函數去檢查當前的fiber的優先級是否比給定的優先級高:

function matchesPriority(fiber, priority) {
  return fiber.pendingWorkPriority !== 0 &&
         fiber.pendingWorkPriority <= priority
}
複製代碼

上面這個函數只是用於演示而已,它並非來源於真實的react fiber源碼。

scheduler就是經過消費fiber的priority字段來決定下一個須要執行的work單元是誰。這其中的算法將會在future section小節去討論。

alternate

flush

當咱們說flush一個fiber時,意思就是將這個fiber的output渲染到屏幕上。

work-in-progress

當一個fiber還沒被完成時(has not yet completed),咱們就說這個work是work-in-progress。概念上就是指某個stack frame還沒被返回的時候。

在任什麼時候候,一個組件實例最多對應着兩個fiber:當前的,已經flushed的fiber和處在work-in-progress的fiber。

current fiber與work-in-progress的fiber會相互轉化的(互生性)。current fiber最終會轉化爲work-in-progress的fiber,work-in-progress的fiber最終會轉化爲下一次work開始時的current fiber。

一個fiber的互生fiber是由一個叫cloneFiber的函數惰性地建立的。相比於老是建立一個新的對象,cloneFiber將會嘗試複用它的互生fiber,若是它存在的話。這麼作,可以減小內存分配。

alternate字段已然是react fiber的實現細節了。由於它在源碼中的出現頻率過高了,全部值得咱們在這裏討論一下它。

output

host component

React app的葉子節點。它們表明的是具體的渲染平臺(例如,對於瀏覽器app來講,DOM節點如「div」, 「span」等就是 host component)。在JSX中,它們都是以小寫的標籤名出現的。

概念上說,一個函數的返回值就是一個fiber的output(這句話有歧義)。

每個fiber最終都會有本身的output,可是這個output的建立只能在葉子節點由host component來建立。output建立後,會沿着節點樹往上傳遞。

output最終會傳遞給renderer,而後應用會把最新狀態渲染在屏幕上。定義output是如何建立的,又是如何被更新到屏幕的,這些事就是renderer的職責之所在了。

Future sections

這是到目前爲止的全部內容了。可是,這是一篇未完待續的文檔。將來的一些章節裏面,我會闡述一個更新過程當中自始至終所採用的算法。包含的主題以下:

  • scheduler是如何查找出下個要執行的work unit是誰?
  • fiber tree中,優先級是如何被追蹤和傳播的?
  • scheduler是如何知道何時暫停和恢復work的呢?
  • work是如何被flush掉,而且並標記爲「已完成的」?
  • side-effect(好比生命週期方法)是如何運行的呢?
  • coroutine是什麼?它是如何被用來實現某些特性(好比context和layout)的呢?

Related Videos

相關文章
相關標籤/搜索