作為壹個構建用戶界面的庫,React的核心始終圍繞著更新這壹個重要的目標,將更新和極致的用戶體驗結合起來是React團隊壹直在努力的事情。為什麽React可以將用戶體驗做到這麽好?我想這是基於以下兩點原因:
本文是對React原理解讀系列的第1篇文章,後續的文章會定期更新,歡迎持續關註。在正式開始之前,我們先基於以上的這兩點展開介紹,以便對壹些概念可以先有個基礎認知。
配合的源碼調試環境在這裏 ,會跟隨React主要版本進行更新,歡迎隨意下載調試。
Fiber是什麽?它是React的最小工作單元,在React的世界中,壹切都可以是組件。在普通的HTML頁面上,人為地將多個DOM元素整合在壹起可以組成壹個組件,HTML標簽可以是組件(HostComponent),普通的文本節點也可以是組件(HostText)。每壹個組件就對應著壹個fiber節點,許多個fiber節點互相嵌套、關聯,就組成了fiber樹,正如下面表示的Fiber樹和DOM的關系壹樣:
Fiber樹 DOM樹
div#root div#root
| |
<App/> div
| / \
div p a
/ ↖
/ ↖
p ----> <Child/>
|
a
壹個DOM節點壹定對應著壹個Fiber節點,但壹個Fiber節點卻不壹定有對應的DOM節點。
fiber 作為工作單元它的結構如下:
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Fiber元素的靜態屬性相關
this.tag = tag;
this.key = key; // fiber的key
this.elementType = null;
this.type = null; // fiber對應的DOM元素的標簽類型,div、p...
this.stateNode = null; // fiber的實例,類組件場景下,是組件的類,HostComponent場景,是dom元素
// Fiber 鏈表相關
this.return = null; // 指向父級fiber
this.child = null; // 指向子fiber
this.sibling = null; // 同級兄弟fiber
this.index = 0;
this.ref = null; // ref相關
// Fiber更新相關
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null; // 存儲update的鏈表
this.memoizedState = null; // 類組件存儲fiber的狀態,函數組件存儲hooks鏈表
this.dependencies = null;
this.mode = mode;
// Effects
// flags原為effectTag,表示當前這個fiber節點變化的類型:增、刪、改
this.flags = NoFlags;
this.nextEffect = null;
// effect鏈相關,也就是那些需要更新的fiber節點
this.firstEffect = null;
this.lastEffect = null;
this.lanes = NoLanes; // 該fiber中的優先級,它可以判斷當前節點是否需要更新
this.childLanes = NoLanes;// 子樹中的優先級,它可以判斷當前節點的子樹是否需要更新
/*
* 可以看成是workInProgress(或current)樹中的和它壹樣的節點,
* 可以通過這個字段是否為null判斷當前這個fiber處在更新還是創建過程
* */
this.alternate = null;
}
首先要明白,React要完成壹次更新分為兩個階段: render階段和commit階段,兩個階段的工作可分別概括為新fiber樹的構建和更新最終效果的應用。
render階段實際上是在內存中構建壹棵新的fiber樹(稱為workInProgress樹),構建過程是依照現有fiber樹(current樹)從root開始深度優先遍歷再回溯到root的過程,這個過程中每個fiber節點都會經歷兩個階段:beginWork和completeWork。組件的狀態計算、diff的操作以及render函數的執行,發生在beginWork階段,effect鏈表的收集、被跳過的優先級的收集,發生在completeWork階段。構建workInProgress樹的過程中會有壹個workInProgress的指針記錄下當前構建到哪個fiber節點,這是React更新任務可恢復的重要原因之壹。
如下面的動圖,就是render階段的簡要過程:
在render階段結束後,會進入commit階段,該階段不可中斷,主要是去依據workInProgress樹中有變化的那些節點(render階段的completeWork過程收集到的effect鏈表),去完成DOM操作,將更新應用到頁面上,除此之外,還會異步調度useEffect以及同步執行useLayoutEffect。
這兩個階段都是獨立的React任務,最後會進入Scheduler被調度。render階段采取的調度優先級是依據本次更新的優先級來決定的,以便高優先級任務的介入可以打斷低優先級任務的工作;commit階段的調度優先級采用的是最高優先級,以保證commit階段同步執行不可被打斷。
Scheduler用來調度執行上面提到的React任務。
何為調度?依據任務優先級來決定哪個任務先被執行。調度的目標是保證高優先級任務最先被執行。
何為執行?Scheduler執行任務具備壹個特點:即根據時間片去終止任務,並判斷任務是否完成,若未完成則繼續調用任務函數。它只是去做任務的中斷和恢復,而任務是否已經完成則要依賴React告訴它。Scheduler和React相互配合的模式可以讓React的任務執行具備異步可中斷的特點。
為了區分任務的輕重緩急,React內部有壹個從事件到調度的優先級機制。事件本身自帶優先級屬性,它導致的更新會基於事件的優先級計算出更新自己的優先級,更新會產生更新任務,更新任務的優先級由更新優先級計算而來,更新任務被調度,所以需要調度優先級去協調調度過程,調度優先級由更新任務優先級計算得出,就這樣壹步壹步,React將優先級的概念貫穿整個更新的生命周期。
React優先級相關的更多介紹請移步 React中的優先級。
雙緩沖機制是React管理更新工作的壹種手段,也是提升用戶體驗的重要機制。
當React開始更新工作之後,會有兩個fiber樹,壹個current樹,是當前顯示在頁面上內容對應的fiber樹。另壹個是workInProgress樹,它是依據current樹深度優先遍歷構建出來的新的fiber樹,所有的更新最終都會體現在workInProgress樹上。當更新未完成的時候,頁面上始終展示current樹對應的內容,當更新結束時(commit階段的最後),頁面內容對應的fiber樹會由current樹切換到workInProgress樹,此時workInProgress樹即成為新的current樹。
function commitRootImpl(root, renderPriorityLevel) {
...
// finishedWork即為workInProgress樹的根節點,
// root.current指向它來完成樹的切換
root.current = finishedWork;
...
}
兩棵樹在進入commit階段時候的關系如下圖,最終commit階段完成時,兩棵樹會進行切換。
在未更新完成時依舊展示舊內容,保持交互,當更新完成立即切換到新內容,這樣可以做到新內容和舊內容無縫切換。
本文基本概括了React大致的工作流程以及角色,本系列文章會以更新過程為主線,從render階段開始,壹直到commit階段,講解React工作的原理。除此之外,會對其他的重點內容進行大篇幅分析,如事件機制、Scheduler原理、重點Hooks以及context原理。
本系列文章耗時較長,落筆撰寫時,17版本還未發布,所以參照的源碼版本為16.13.1、17.0.0-alpha.0以及17共三個版本,我曾經對文章中涉及到的三個版本的代碼進行過核對,邏輯基本無差別,可放心閱讀。