文章目錄
- 一文講清楚React Fiber
- 1. 基礎概念
- 1.1瀏覽器刷新率(幀)
- 1.2 JS執行棧
- 1.3 時間分片
- 1.4 鏈表
- 2. React Fiber是如何實現更新過程控制
- 2.1 任務拆分
- 2.2掛起、恢復、終止
- 2.2.1 掛起
- 2.2.2 恢復
- 2.2.3 終止
- 2.3 任務具備優先級
一文講清楚React Fiber
1. 基礎概念
1.1瀏覽器刷新率(幀)
- 頁面都是一幀一幀繪制出來的,瀏覽器大多是60Hz(60幀/s),每一幀耗時16ms左右,每一幀分為以下7個過程
-
- 接手輸入事件
-
- 執行回調事件
-
- 開始一幀
-
- 執行RequestAnimationFrame,即RAF
-
- 頁面布局,計算樣式
-
- 渲染
-
- 執行RequestIdleCallback,即RIC
- 其中,RIC事件并不是每一幀結束都會執行,只有在一幀的16ms內做完了前6件事切還有剩余時間,RIC才會執行。如果執行了RIC事件,那么下一幀就要在事件執行結束后才能繼續渲染,所以RIC的執行時間不宜太長,不然瀏覽器得不到控制權,無法完成下一幀的渲染,會出現頁面卡頓
1.2 JS執行棧
- React16之前,是通過原生執行棧遞歸遍歷DOM,會形成一個執行棧,每次更新瀏覽器會從棧頂開始執行,直到執行棧被清空才會把執行權交給瀏覽器。而在React中,頁面視圖都被視為一個個函數執行的結果,這就意味著有多個函數的調用。如果頁面很復雜,執行棧就會很深,就要占據很長的一段時間,瀏覽器渲染就會停滯,就會出現卡頓等問題
1.3 時間分片
- 就是將粒度很小的任務放入一個時間段(一幀)去執行的一種方案,React Fiber就是將多個任務放入一個時間分片去執行
1.4 鏈表
- 鏈表的概念不用多少說
- React16之后,使用多向鏈表代替了原來的樹結構,同時還會生成副作用單鏈表和狀態更新單鏈表
2. React Fiber是如何實現更新過程控制
- 過程可控體現在三方面
-
- 任務拆分
-
- 任務掛起、恢復、終止
-
- 任務具備優先級
2.1 任務拆分
- React Fiber 將遍歷VDOM拆分成若干個小任務,每個人物只負責一個節點的處理
2.2掛起、恢復、終止
-
在React Fiber架構中,更新過程的核心在于兩棵Fiber樹的協同工作:當前工作樹(workInProgress)和當前渲染樹(current)。這兩棵樹構成了React實現可中斷渲染的基礎架構。
-
工作樹(workInProgress)是React在執行更新時正在構建的新版本Fiber樹。每當應用狀態發生變化(如通過setState觸發更新),React就會開始構建這棵新樹。在構建過程中,每個Fiber節點都- 會記錄自身的變更標記(effectTag),最終整棵樹會形成完整的變更鏈表。
-
當前樹(current)則代表著上次渲染周期最終呈現的UI對應的Fiber結構。每次更新完成后,新構建的workInProgress樹就會成為新的current樹。在下一次更新開始時,React會基于這個current樹- 創建新的workInProgress樹,并通過alternate指針在兩樹的對應節點間建立關聯。
-
在構建新workInProgress樹的過程中,React會執行關鍵的協調算法:
-
通過對比新舊節點(diff算法)來確定需要應用的變更
-
盡可能復用current樹中的節點實例,避免不必要的對象創建
-
為每個節點標記具體的更新類型(如新增、修改或刪除)
-
整個更新過程本質上就是workInProgress樹的漸進式構建過程:
-
React會將構建任務分解為多個工作單元
-
每個工作單元完成后可以暫停讓出主線程
-
通過循環調度機制繼續處理下一個工作單元
-
這種分片執行方式使得高優先級更新可以中斷低優先級任務
-
這種雙樹機制賦予了React三大核心能力:
-
可中斷的漸進式渲染
-
更新優先級的智能調度
-
高效的節點復用策略
-
值得注意的是,所有與任務調度相關的操作(暫停、恢復或取消)都發生在workInProgress樹的構建階段。React通過這種巧妙的架構設計,在保持聲明式編程模型的同時,實現了接近原生渲染的性能表現。
2.2.1 掛起
- 當第一個小任務完成后,先判斷這一幀是否還有空閑時間,沒有就掛起下一個任務的執行,記住當前掛起的節點,讓出控制權給瀏覽器執行更高優先級的任務。
2.2.2 恢復
- 在瀏覽器渲染完一幀后,判斷當前幀是否有剩余時間,如果有就恢復執行之前掛起的任務。如果沒有任務需要處理,代表調和階段完成,可以開始進入渲染階段。這樣完美的解決了調和過程一直占用主線程的問題。
那么問題來了他是如何判斷一幀是否有空閑時間的呢?答案就是我們前面提到的 RIC (RequestIdleCallback) 瀏覽器原生 API,React 源碼中為了兼容低版本的瀏覽器,對該方法進行了 Polyfill。
當恢復執行的時候又是如何知道下一個任務是什么呢?答案在前面提到的鏈表。在 React Fiber 中每個任務其實就是在處理一個 FiberNode 對象,然后又生成下一個任務需要處理的 FiberNode
class FiberNode {constructor(tag, pendingProps, key, mode) {// 實例屬性this.tag = tag; // 標記不同組件類型,如函數組件、類組件、文本、原生組件...this.key = key; // react 元素上的 key 就是 jsx 上寫的那個 key ,也就是最終 ReactElement 上的this.elementType = null; // createElement的第一個參數,ReactElement 上的 typethis.type = null; // 表示fiber的真實類型 ,elementType 基本一樣,在使用了懶加載之類的功能時可能會不一樣this.stateNode = null; // 實例對象,比如 class 組件 new 完后就掛載在這個屬性上面,如果是RootFiber,那么它上面掛的是 FiberRoot,如果是原生節點就是 dom 對象// fiberthis.return = null; // 父節點,指向上一個 fiberthis.child = null; // 子節點,指向自身下面的第一個 fiberthis.sibling = null; // 兄弟組件, 指向一個兄弟節點this.index = 0; // 一般如果沒有兄弟節點的話是0 當某個父節點下的子節點是數組類型的時候會給每個子節點一個 index,index 和 key 要一起做 diffthis.ref = null; // reactElement 上的 ref 屬性this.pendingProps = pendingProps; // 新的 propsthis.memoizedProps = null; // 舊的 propsthis.updateQueue = null; // fiber 上的更新隊列執行一次 setState 就會往這個屬性上掛一個新的更新, 每條更新最終會形成一個鏈表結構,最后做批量更新this.memoizedState = null; // 對應 memoizedProps,上次渲染的 state,相當于當前的 state,理解成 prev 和 next 的關系this.mode = mode; // 表示當前組件下的子組件的渲染方式// effectsthis.effectTag = NoEffect; // 表示當前 fiber 要進行何種更新this.nextEffect = null; // 指向下個需要更新的fiberthis.firstEffect = null; // 指向所有子節點里,需要更新的 fiber 里的第一個this.lastEffect = null; // 指向所有子節點中需要更新的 fiber 的最后一個this.expirationTime = NoWork; // 過期時間,代表任務在未來的哪個時間點應該被完成this.childExpirationTime = NoWork; // child 過期時間this.alternate = null; // current 樹和 workInprogress 樹之間的相互引用}
}
2.2.3 終止
- 其實并不是每次更新都會走到提交階段。當在調和過程中觸發了新的更新,在執行下一個任務的時候,判斷是否有優先級更高的執行任務,如果有就終止原來將要執行的任務,開始新的 workInProgressFiber 樹構建過程,開始新的更新流程。這樣可以避免重復更新操作。這也是在 React 16 以后生命周期函數 componentWillMount 有可能會執行多次的原因
2.3 任務具備優先級
- React Fiber 除了通過掛起,恢復和終止來控制更新外,還給每個任務分配了優先級。具體點就是在創建或者更新 FiberNode 的時候,通過算法給每個任務分配一個到期時間(expirationTime)。在每個任務執行的時候除了判斷剩余時間,如果當前處理節點已經過期,那么無論現在是否有空閑時間都必須執行改任務
- 同時過期時間的大小還代表著任務的優先級。
任務在執行過程中順便收集了每個 FiberNode 的副作用,將有副作用的節點通過 firstEffect、lastEffect、nextEffect 形成一條副作用單鏈表