「從0實現React18系列」Fiber架構的實現原理
Reconciler 是什么
Reconciler 是 React 核心邏輯所在的模塊,中文名叫協調器。
Reconciler 架構介紹
在React中,Reconciler?(協調器)是負責管理虛擬DOM樹更新的關鍵部分。當組件狀態或屬性發生更改時,Reconciler的任務是確定如何有效地更新DOM來反映這些更改。這個過程通常被稱為 "協調"(Reconciliation)。
Reconciler?的核心思想是通過將新的虛擬DOM樹與舊的虛擬DOM樹進行比較,找出需要實際更新的部分,然后最小化實際DOM操作的數量。這個過程被稱為"diffing"算法。
傳統庫與現代框架的工作原理
在傳統的庫(jQuery)工作原理(過程驅動)。
在傳統的前端開發中使用的jQuery?庫的工作原理主要是通過一個簡化和統一的API,使得開發者能夠更容易地操作DOM、處理事件、創建動畫以及發起AJAX請求等。而不是描述UI的狀態。所以jQuery的工作原理是過程驅動的。
現代的前端框架結構與工作原理(狀態驅動)。
現代的前端框架結構與工作原理
- 在現代的前端框架中,開發者使用描述UI的方法(template或JSX)來定義組件和它們之間的關系。
- 運行時核心模塊根據描述的UI,管理組件的創建、更新和銷毀,處理數據狀態變更,并通過虛擬DOM或響應式系統來優化UI更新性能。
- 當需要與宿主環境交互時(如操作DOM、處理事件或發起網絡請求),運行時核心模塊會調用宿主環境API。前端框架通常會封裝這些API,提供統一的跨平臺接口。
AOT預編譯 與 JIT即時編譯
現代框架都需要“編譯”這一步驟,用于:
- 將“框架中描述的UI”轉換為宿主環境可識別的代碼。
- 代碼轉化,比如將ts編譯成js,實現polyfill等。
- 執行一些編譯時優化。
“編譯”可以選擇兩個時機執行:
- 代碼在構建時,被稱為 AOT(Ahead Of Time,提前編譯或預編譯),宿主環境獲得是編譯后的代碼。
- 代碼在宿主環境執行時,被稱為 JIT(Just In Time,即時編譯),代碼在宿主環境中編譯并執行。
大部分采用模板語法描述UI的前端框架都會進行AOT優化,例如:Vue3、Angular、Svelte。
其本質原因在于模板語法時固定的,固定意味著“可分析”,“可分析”意味著在編譯時可以標記模板語法中的靜態部分(不變的部分)與動態部分(包含自變量、可變的部分)。
但采用JSX語法描述UI的前端框架很難從 AOT中受益,因為JSX是ES的語法糖,ES語句的靈活性使其很難進行靜態分析。
拓展 那么Template語法是如何從中受益的呢?
- 解析:將模板字符串解析成抽象語法樹(AST)。AST是一種樹形結構,用于表示模板中的元素、屬性、文本節點等。
- 優化:遍歷AST,對其中的靜態內容(如純文本節點、靜態屬性等)進行標記。這些標記在后續的渲染過程中有助于避免不必要的計算和更新,從而提高性能。
- 代碼生成:將優化后的AST轉換成可執行的JavaScript代碼。這通常包括生成渲染函數(render function)和虛擬DOM節點。渲染函數用于創建和更新實際的DOM結構。
模板語法由于在構建時已經被編譯成可執行的JavaScript代碼,運行時無需再進行解析和編譯,從而減少了性能開銷。
ReactElement 數據結構的不足
回歸主題,根據前面的學習,我們知道了 JSX 方法執行后會返回一個新的 React 元素(ReactElement)。React 元素是一個輕量級的對象,描述了要渲染的 UI 組件的類型(type)、屬性(props),和子元素(children)等信息。
這里可以給自己個問題,如果ReactElement?作為reconciler核心模塊操作的數據結構,會存在哪些問題:
- 無法表達ReactElement節點與另一個ReactElement節點之間的關系(因為它只記錄了自身的數據,比如組件的類型、屬性和子元素等),一般把ReactElement稱為React的數據存儲單元。
- 字段有限,不好拓展(比如:無法表達狀態)。
從下圖中可以看到,ReactELement這種數據結構很有限,在節點屬性關聯方面也只有 children,并沒有保存兄弟節點以及父節點之間的關系:
當然在React 16版本之前,React 使用的是名為Stack Reconciler的舊調和算法。Stack Reconciler 的核心是遞歸遍歷組件樹,把數據保存在遞歸調用棧中。它使用的深層遞歸遍歷方法。
但是使用遞歸遍歷組件樹時,會導致一些問題:
- 阻塞主線程:在 JavaScript 中,遞歸調用可能會阻塞主線程,因為 JavaScript 是單線程的。如果組件樹很大或者更新很頻繁,遞歸調用可能會導致 UI 變得不流暢,影響用戶體驗。
- 沒有優先級調度:Stack Reconciler 無法對不同的更新任務進行優先級調度,所有的更新任務都會被視為相同的優先級。這意味著對于高優先級的任務(如動畫或用戶交互),React 無法優先處理,從而可能導致性能下降。
為了解決這些問題,React 引入了 Fiber Reconciler?。Fiber Reconciler? 使用了一種名為 "Fiber" 的新數據結構來表示組件樹。
它的特點:
- 介于ReactElement與真實UI節點之間。
- 能夠表達節點之間的關系。
- 方便拓展,不僅作為數據存儲單元,也能作為工作單元。
FiberNode 是虛擬DOM在React中的實現。
FiberNode Tree的數據結構如圖所示:
FiberNode 上有很多屬性,包括和自身相關的屬性 ref,節點之間的關系 return、silbing還有工作單元上的屬性,比如 pendingProps等等,后面會詳細介紹。
Fiber出現的意義
Fiber最主要的兩層含義:
- 作為靜態的數據結構來說,每個Fiber節點對應一個React element,保存了該組件的類型(函數組件/類組件/原生組件...)、對應的DOM節點等信息。
- 作為動態的工作單元來說,每個Fiber節點保存了本次更新中該組件改變的狀態、要執行的工作(需要被刪除/被插入頁面中/被更新...)。
Fiber的出現也為React帶來了很多意義:
從優化層面來說,Fiber是一種新的調和算法(reconciliation algorithm)。
- 增量渲染:在早期的 React 版本(Stack Reconciler)中,當有組件更新時,React 會一次性完成整個組件樹的調和過程。這會導致長時間的 JavaScript 執行阻塞,從而影響用戶界面的響應性。Fiber 引入了增量渲染的概念,允許將調和過程分成多個小任務,這些任務可以在瀏覽器的空閑時間內執行。這樣,即使在復雜的應用程序中,React 也能實現更平滑的用戶界面更新。
- 任務調度:Fiber 引入了任務優先級的概念,使得 React 可以根據任務的優先級來調度它們的執行。這意味著較高優先級的任務(如用戶交互事件)可以打斷較低優先級的任務(如數據加載),從而實現更靈活的任務調度。這有助于提高應用程序的響應性和性能。
這兩個概念會在后面的章節詳細講解。
Fiber是什么?
Fiber是React的最小的工作單元。在React的世界中,一切都可以是組件。在普通的HTML頁面上,開發者們可以將多個DOM元素整合在一起組成一個組件。
普通的DOM元素(HostComponent)可以是組件,普通的文本節點(HostText)也可以是組件。還有通過ReactDom.render方法創建的根元素(RootElement)也可以是組件,還有經常在React中使用的函數組件(FunctionComponent)。
在React源碼中,每個FiberNode都有一個WorkTag屬性,用于標識當前節點的類型。
ReactWorkTags.ts文件中定義了所有可能的節點類型,每個類型都應一個number類型的值。
這樣做的好處是可以通過比較兩個節點的 WorkTag 屬性來判斷它們是否是同一類型的節點,而不需要通過字符串比較等方式,這樣可以提高比較的效率,也可以減少出錯的可能性。
每一個組件都對應著一個FiberNode?,許多個FiberNode?互相嵌套、關聯就組成了FiberNode Tree?。正如下面表示的FiberNode Tree和DOM樹的關系一樣:
一個DOM節點一定對應著一個FiberNode,但每一個Fiber節點缺不一定有對應的DOM節點。
因為React支持不同類型的組件,因此每個FiberNode并不一定具有對應的DOM節點。
- 函數組件:函數組件是一個簡單的函數,它接收屬性(props)并返回JSX。這個JSX可能包含DOM節點,但這個DOM節點并不是真實的DOM節點,而是當React渲染組件時,它會將函數組件返回的JSX轉換成真實的DOM節點。所以函數組件本身并不會直接映射到一個DOM節點。
- Fragment:React Fragment是一種特殊的組件,用于在不添加額外DOM節點的情況下返回多個子元素。當遍歷組件樹時,React會將Fragment的子元素視為直接子元素,而不會為Fragment本身創建DOM節點。
- 還有很多,不一一舉例了。
Fiber工作單元的結構
Fiber作為工作單元,它有很多屬性:
- Fiber實例屬性: tag、key、type、stateNode等。
- 與其它節點關系的鏈表屬性:return、child、sibling、index。
- Ref相關的屬性:ref。
- Fiber更新相關的屬性:pendingProps、memoizedProps、memoizedState、updateQueue、alternate。
- Fiber Effect:flags、subtreeFlags、deletions。
這里Fiber節點的屬性沒有寫完全,可以去react源碼里看,地址在代碼塊首行。
雖然屬性很多,但可以按三層含義將它們分類來看:
作為架構來說
每個Fiber節點有個對應的React element,多個Fiber節點是如何連接形成樹呢?靠如下三個屬性:
舉個例子,比如下面的組件結構:
對應的FiberNode Tree結構:
作為靜態的數據結構
作為靜態的數據結構,需要保存組件的相關的信息:
作為動態的工作單元
作為動態的工作單元,Fiber中如下參數保存了本次更新相關的信息,會在后續的更新流程章節中使用到具體屬性時再詳細介紹。
如下兩個字段保存調度優先級相關的信息,會在講解Scheduler時介紹。
總結
在本節我們對Reconciler的架構有了大概的認知,了解了傳統的庫與現代框架的工作原理,也掌握了預編譯和即時編譯的區別,以及它們在現代框架中的應用。
在上一節中,我們實現了JSX的轉換,知道了React Element這種數據,但是它也有一定的缺陷,為了解決這個缺陷,React 引入了Fiber架構,介紹了Fiber出現的意義,以及它的結構是什么樣的,通過FiberNode組成的FiberNode Tree的結構。
下一節主要介紹Fiber作為Reconciler核心模塊的工作單元,是如何創建及更新DOM的。