原文: React Fiber Architecturehtml
React Fiber是React核心算法的持續再實現,這是React團隊兩年多研究的成果。react
React Fiber的目標是加強其在動畫、佈局和手勢等領域的適用性。它的主要特性是增量渲染:可以將渲染工做分割成塊,並將其分散到多個幀中。git
其餘關鍵功能包括暫停、停止或重啓更新工做的能力;爲不一樣類型的更新分配優先級的能力;以及新的併發單元。程序員
Fiber引入了一些新的概念,這些概念很難僅僅經過查看代碼來理解。本文檔一開始是我在關注React項目中Fiber的實現過程當中所作的筆記。隨着它的發展,我意識到它可能對其餘人也頗有幫助。github
我將盡可能使用最簡單的語言,並經過明肯定義關鍵術語來避免術語。若是可能的話,我還將大量連接到外部資源。算法
請注意,我不是React團隊的一員,算不上權威人士。這不是官方文檔。 我已經請求React團隊的成員檢查它的準確性。c#
這也是一項正在進行中的工做。Fiber是一個正在進行的項目,在它完成以前可能會經歷重大的重構。 我也會繼續嘗試在這裏記錄它的設計,歡迎提出改進和建議。api
個人目標是在閱讀了本文以後,你將可以很好地理解Fiber,以便在其實現過程當中進行跟蹤,並最終可以對其做出貢獻。瀏覽器
在繼續學習以前,我強烈建議你熟悉如下資源:網絡
請檢查預備知識部分的內容是否已經準備好。
在開始學習新內容以前,讓咱們先複習一些概念。
協調算法
React使用該算法來比較兩棵樹之間的不一樣,而後決定出須要改變的部分。
更新
用於渲染React應用程序的數據變化一般是setState
的結果,最終致使從新渲染。
React API的核心思想是將更新看做是致使整個應用程序從新渲染的緣由。這容許開發人員以聲明的方式推理,而沒必要擔憂如何有效地將應用程序從一些特定狀態轉換到另外一個狀態(A到B, B到C, C到A,等等)。
實際上,在每次更改時從新渲染整個應用程序只適用於最普通的應用;在真正的應用程序中,它所消耗的性能代價很是高。React對此進行了優化,在保持良好性能的同時從新渲染整個應用的界面。這些優化的大部份內容是一個被稱爲 協調 算法處理完成的。
協調是一種被廣泛理解爲「虛擬DOM」的算法。對其高層次的描述是這樣的:當你渲染一個React應用,用於描述app的節點樹被生成而且保存在內存中。而後這棵樹會被放入到渲染環境——好比,在基於瀏覽器的應用中,它被轉換成一組DOM操做。當app更新後(通常經過setState
),一個新的樹就會隨之產生。這顆新樹與先前那棵樹的不一樣是它要計算出哪些操做須要更新到渲染app中。
儘管Fiber是對「協調算法」底層方面全新的重寫,可是這個高級算法在React文檔中描述大體相同。關鍵點在於:
diff
它們,而是將舊的樹徹底替換。list
)之間的diff
使用它們keys
,由於keys
是「穩定的、可預測的和唯一的」。DOM只是React能夠渲染的「渲染環境」之一,其餘主要的目標是經過React native實現的iOS和Android視圖。(這就是爲何「虛擬DOM」有點用詞不當。)
它之因此可以支持如此多的目標,是由於React被設計成協調和渲染是兩個獨立的階段。協調器要作的工做是計算出一棵樹的哪些部分發生了變化;而後渲染器使用這些信息來更新渲染app。
這種分離意味着React DOM和React Native可使用它們本身的渲染器,同時共享React core提供的同一個協調器。
Fiber從新實現了協調器。它變得渲染沒有過高的相關性,所以渲染器也須要改變以支持(並利用)新的架構。
調度
肯定什麼時候完成work
的過程。
work
一些必須執行的運算。Work
一般是更新的結果(例如setState
)。
React的 設計原則 文檔在這方面作得很好,我在這裏引用一下:
在其當前實現中,React遞歸遍歷樹,並在單個標記期間調用整個更新樹的呈現函數。可是在未來,它可能會開始延遲一些更新以免丟失幀 (能夠理解爲交互卡頓)。
這是React設計中的一個常見主題。一些流行的框架實現了「
push
」方法,即在新數據可用時執行計算。然而,React堅持使用「pull
」方法,在這種方法中,計算能夠延遲到必要的時候。
React不是通常的數據處理框架。它是一個用於構建用戶界面的框架。咱們認爲它在應用程序中處於獨特的位置,能夠知道哪些計算是相關的,哪些不是。
若是瀏覽器屏幕外發生了什麼(好比
click
事件等),咱們能夠延遲與之相關的一些邏輯。若是數據到達的速度快於幀速率,咱們能夠將它們合併而後批量更新。咱們能夠優先處理來自用戶交互的工做(如單擊按鈕所引發的動畫),而不是不過重要的後臺工做(如渲染剛從網絡加載的新內容),以免丟失幀 (能夠理解爲交互卡頓)。
重點是:
push
的方法須要應用程序(你,程序員)來決定如何安排工做。基於pull
的方法容許框架(React)爲您作出明智的決策。React在此以前並無很好地利用調度,所以一個更新會當即從新渲染整個子樹。今天,利用調度優點的React核心算法是Fiber背後的驅動思想。
如今,咱們準備深刻討論Fiber的實現。下一節比咱們到目前爲止討論的內容更具備技術性。在繼續以前,請確保你對前面的內容達到能夠接受的狀態。
咱們將討論React Fiber的核心架構。相對於應用開發人員一般的認知,Fiber是一個至關底層的抽象。若是你發現本身在試圖理解它的過程當中受挫,不要氣餒。不斷嘗試,最終會搞定它的。(當你最終理解它後,請建議咱們如何更好的提升本章節的描述)。
咱們開始吧!
咱們已經爲Fiber確立了一個主要的目標就是讓React更好的利用調度。具體來講,咱們須要可以:
work
定義優先級。爲了作到這一點,咱們首先須要一種把work
分解成單位的方法。從某種意義上說,這就是Fiber。一根 光纖 表明一個工做單位。
爲了更進一步,讓咱們回到 React組件做爲數據函數 的概念,一般表示爲:
v = f(d)
複製代碼
渲染React app比如調用一個函數,該函數的函數體包含了對其餘函數的調用等等。這個類比在討論Fiber時頗有用。
計算機跟蹤程序執行的典型方式就是使用「堆棧」。當一個函數被執行,一個新的堆棧幀被添加到堆棧中。這個堆棧幀表明了函數所執行的那項工做。
在處理ui時,遇到的問題是若是一次性執行了太多的工做,就會致使動畫丟失幀,看起來很不穩定。更重要的是,一些工做多是沒必要要的,若是它被一個更近期的更新取代。這就是UI組件和函數之間的比較失敗的地方,由於組件比通常的函數有更具體的關注點。
更新的瀏覽器(和React Native)實現的api能夠幫助解決這個問題:requestIdleCallback
在空閒期間調度被調用的低優先級函數,requestAnimationFrame
在下一個動畫幀調度被調用的高優先級函數。如今的問題是,爲了使用這些api,你須要一種方法來將渲染工做分解爲增量單元。若是你只依賴於調用堆棧,它將一直工做,直到堆棧爲空。
若是咱們能夠定製調用堆棧的行爲來優化渲染ui,那豈不是更好?若是咱們能夠隨意中斷調用堆棧並手動操做堆棧幀,那豈不是更好?
這就是React Fiber的目標。Fiber是棧的從新實現,專門用於React組件。你能夠將單個Fiber視爲一個虛擬堆棧幀。
從新實現堆棧的好處是,你能夠 將堆棧幀保存在內存中 ,並在須要的時候以任何方式執行它們。這對於完成咱們的調度目標是相當重要的。
除了調度以外,手動處理堆棧幀還能夠解決併發性和錯誤邊界等潛在特性問題。咱們將在之後的章節中討論這些問題。
在下一節中,咱們將更多地研究一個fiber的結構。
注意:隨着咱們對實現細節的瞭解愈來愈具體,發生變化的可能性也愈來愈大。若是您注意到任何錯誤或過期的信息,請提交一份PR。
具體而言,fiber是一個JavaScript對象,它包含關於組件的輸入和輸出信息。
一個fiber對應了一個堆棧幀,也對應了一個組件的實例。
如下是fiber的一些屬性。(這份清單並不詳盡。)
type
和 key
fiber的type
和key
在React元素上面有着相同的做用。事實上,當從一個元素建立一個fiber時,這兩個字段被直接複製。
一個fiber的類型描述了它所對應的組件。對於複合組件,類型是函數或類組件自己。對於宿主組件(div
、span
等),類型是字符串。
從概念上講,類型是函數(如v = f(d)
)的組件,它的執行會由堆棧幀跟蹤。
除了類型以外,在協調期間還使用key
來肯定是否能夠重用fiber。
child
和 sibling
這兩個字段指向了其餘fiber節點,描述了fiber的遞歸樹結構。
孩子(child)fiber節點對應着組件render
函數的返回值。因此在下面的例子中
function Parent() {
return <Child /> } 複製代碼
組件Parent
的孩子fiber節點是Child
組件。
sibling
字段解釋了渲染返回多個子元素的狀況(這是Fiber的一個新特性!)。
function Parent() {
return [<Child1 />, <Child2 />] } 複製代碼
孩子fiber節點造成了一個鏈表,鏈表的頭部是第一個孩子fiber節點。在這個例子中,父結點的子結點是Child1
,而Child1
的兄弟結點是Child2
。
回到咱們的函數類比,你能夠把孩子fiber想象成 尾部調用函數。
return
返回fiber節點是程序在處理當前fiber節點後應該返回的fiber節點。它在概念上與堆棧幀的返回地址相同。它也能夠被認爲是父fiber節點。
若是一個fiber節點有多個孩子fiber節點,每個孩子的返回fiber就是父fiber節點。在上一節的例子中,Child1
和Child2
的返回fiber節點是父結點。
pendingProps
和 memoizedProps
從概念上講,屬性是函數的參數。fiber的pendingProps
設置在執行的開始,memoizedProps
設置在執行的最後。
當輸入的pendingProps
與memoizedProps
相等時,就表示fiber 以前的輸出能夠重複使用,避免了沒必要要的工做。
在fiber中,用一個數字來表示work
的優先級。ReactPriorityLevel 模塊列出了不一樣的優先級和它們所表明的內容。
除了NoWork
是0以外,較大的數字表示較低的優先級。例如,您可使用如下函數來檢查fiber節點的優先級是否至少與給定的級別相同:
function matchesPriority(fiber, priority) {
return fiber.pendingWorkPriority !== 0 &&
fiber.pendingWorkPriority <= priority
}
複製代碼
此函數僅供參考,它並非React Fiber代碼的一部分。
調度器使用priority
字段搜索要執行的下一個工做單元。這個算法將在之後的章節中討論。
flush(刷新)
刷新 fiber就是把它的輸出渲染到屏幕上。
work-in-progress
一個fiber還沒有完成工做,從概念上講,就是還沒有返回堆棧幀。
在任什麼時候候,組件實例最多有兩個與之對應的fiber樹:當前的、已刷新的fiber和正在工做的fiber。
當前的替代fiber就是work-in-progress,work-in-progress的替代就是當前fiber。
fiber的替代是使用名爲cloneFiber
的函數惰性建立的。與老是建立新對象不一樣,cloneFiber
將嘗試重用存在的替代對象,從而最小化分配。
你應該將alternate
字段視爲實現細節,可是它在代碼庫中常常出現,所以有必要在這裏討論它。
host component
React應用程序的葉子節點。它們特定於呈現環境(例如,在瀏覽器應用程序中,它們是「div」
、「span」
等)。在JSX中,它們用小寫標記名錶示。
從概念上講,fiber的輸出是函數的返回值。
每一個fiber最終都有輸出,但輸出僅由 host components (宿主組件)在葉節點上建立。而後輸出被傳送到樹上。
輸出是最終給渲染器的,這樣它就能夠刷新更改到渲染環境。渲染器的職責是定義如何建立和更新輸出。
這就是目前的所有內容,可是這個文檔還遠遠沒有完成。後面的部分將描述在更新的整個生命週期中使用的算法。主題包括:
work
是如何被刷新以及被標記以及完成的。