標籤:

React Fiber初探

摘要: React 16版本已經推出多時,提出了包括Portal,異常邊界等新特性,最重要的是重寫了調和演算法,推出了新版本演算法實現-Fiber,於是博主歷時三周,在業餘時間學習Fiber架構實現和源碼,對Fiber整體有了初步了解,並總結分享出來,若對一些源碼不感興趣,大可跳過,另博主水平有限,若有不對之處,歡迎指正。

歡迎訪問本人博客

前言

React的定位是一個構建用戶界面的JavaScript類庫,它使用JavaScript語言開發UI組件,可以使用多種方式渲染這些組件,輸出用戶界面,較大程度的達到了跨技術棧跨平台的兼容重用:

We don』t make assumptions about the rest of your technology stack, so you can develop new features in React without rewriting existing code.

現在的React已然在以下幾個方面發揮的都很不錯:

  1. React Web應用用戶界面開發;
  2. React Native App用戶界面開發;
  3. Node.js服務端渲染;

在這些不同場景,渲染的主體很明顯是不一樣的,有諸如web應用的DOM渲染,React Native的原生View渲染,服務端字元串渲染等,要做到兼容適應多種不同渲染環境,很顯然,React不能局限固定渲染UI的方式。

React核心內容也確實只包括定義組件相關的內容和API,源碼可以查看,實際項目中,可以看到首先需要使用如下代碼:

import React from react;n

這句代碼做的就是引入了React核心源碼模塊。

渲染

上一節已經說到React核心內容只涉及如何定義組件,並不涉及具體的組件渲染(即輸出用戶界面),這需要額外引入渲染模塊,以渲染React定義的組件:

  1. React DOM渲染模塊:將React組件渲染為DOM,然後可以被瀏覽器處理呈現給用戶,這就是通常在web應用中引入的react-dom模塊:

import React from react;nimport { render } from react-dom;nimport App from ./apps/App.js;nnrender(n <App />,n document.getElementById(mainBox)n);n

如上代碼,App是使用React核心模塊定義的組件,然後使用react-dom渲染模塊提供的render方法將其渲染為DOM輸出至頁面。

  1. React Native 渲染:將React組件渲染為移動端原生View,在React Native應用中引入react-native模塊,它提供相應渲染方法可以渲染React組件:

import { AppRegistry } from react-native;nimport App from ./src/app.js;nnAppRegistry.registerComponent(fuc, () => App);n

如上,App是React根組件,使用react-native渲染器的AppRegistry.registerComponent方法將其渲染為原生View。

  1. React測試渲染:將React組件渲染為JSON樹,用來完成Jest的快照測試,內容在react-test-renderer模塊:

import ReactTestRenderer from react-test-renderer;nnconst renderer = ReactTestRenderer.create(n <Link page="https://www.facebook.com/">Facebook</Link>n);nnconsole.log(renderer.toJSON());n// { type: a, // props: { href: https://www.facebook.com/ }, // children: [ Facebook ] } n

  1. React矢量圖渲染:將React組件渲染為對應的適量圖(ART庫);

web React應用是最常見的,也是最易於理解的,所以本篇後文均從React-DOM渲染器角度解析Fiber。

調和(Reconciliation)

如前面兩節所述,React核心是定義組件,渲染組件方式由環境決定,定義組件,組件狀態管理,生命周期方法管理,組件更新等應該跨平台一致處理,不受渲染環境影響,這部分內容統一由調和器(Reconciler)處理,源碼傳送,不同渲染器都會使用該模塊。調和器主要作用就是在組件狀態變更時,調用組件樹各組件的render方法,渲染,卸載組件。

Stack Reconciler

我們知道瀏覽器渲染引擎是單線程的,在React 15.x版本及之前版本,計算組件樹變更時將會阻塞整個線程,整個渲染過程是連續不中斷完成的,而這時的其他任務都會被阻塞,如動畫等,這可能會使用戶感覺到明顯卡頓,比如當你在訪問某一網站時,輸入某個搜索關鍵字,更優先的應該是交互反饋或動畫效果,如果交互反饋延遲200ms,用戶則會感覺較明顯的卡頓,而數據響應晚200毫秒並沒太大問題。這個版本的調和器可以稱為棧調和器(Stack Reconciler),其調和演算法大致過程見React Diff演算法 和React Stack Reconciler實現。

Stack Reconcilier的主要缺陷就是不能暫停渲染任務,也不能切分任務,無法有效平衡組件更新渲染與動畫相關任務間的執行順序,即不能劃分任務優先順序,有可能導致重要任務卡頓,動畫掉幀等問題。

Fiber Reconciler

React 16版本提出了一個更先進的調和器,它允許渲染進程分段完成,而不必須一次性完成,中間可以返回至主進程式控制制執行其他任務。而這是通過計算部分組件樹的變更,並暫停渲染更新,詢問主進程是否有更高需求的繪製或者更新任務需要執行,這些高需求的任務完成後才開始渲染。這一切的實現是在代碼層引入了一個新的數據結構-Fiber對象,每一個組件實例對應有一個fiber實例,此fiber實例負責管理組件實例的更新,渲染任務及與其他fiber實例的聯繫。

這個新推出的調和器就叫做纖維調和器(Fiber Reconciler),它提供的新功能主要有:

  1. 可切分,可中斷任務;
  2. 可重用各分階段任務,且可以設置優先順序;
  3. 可以在父子組件任務間前進後退切換任務;
  4. render方法可以返回多元素(即可以返回數組);
  5. 支持異常邊界處理異常;

說了這麼多,終於要正式出場本篇主角:Fiber了,React最新版本已經升到16.1.1,估計16.x穩定版不會太遠,讓我們先睹為快吧。

Fiber與JavaScript

前面說到Fiber可以非同步實現不同優先順序任務的協調執行,那麼對於DOM渲染器而言,在JavaScript層是否提供這種方式呢,還是說只能使用setTimeout模擬呢?目前新版本主流瀏覽器已經提供了可用API:requestIdleCallbackrequestAnimationFrame:

  1. requestIdleCallback: 在線程空閑時期調度執行低優先順序函數;
  2. requestAnimationFrame: 在下一個動畫幀調度執行高優先順序函數;

空閑期(Idle Period)

通常,客戶端線程執行任務時會以幀的形式劃分,大部分設備控制在30-60幀是不會影響用戶體驗;在兩個執行幀之間,主線程通常會有一小段空閑時間,requestIdleCallback可以在這個空閑期(Idle Period)調用空閑期回調(Idle Callback),執行一些任務。

Fiber與requestIdleCallback

Fiber所做的就是需要分解渲染任務,然後根據優先順序使用API調度,非同步執行指定任務:

  1. 低優先順序任務由requestIdleCallback處理;
  2. 高優先順序任務,如動畫相關的由requestAnimationFrame處理;
  3. requestIdleCallback可以在多個空閑期調用空閑期回調,執行任務;
  4. requestIdleCallback方法提供deadline,即任務執行限制時間,以切分任務,避免長時間執行,阻塞UI渲染而導致掉幀;

具體執行任務實現源碼傳送:

  1. 若支持原生API,具體原生實現見上文給出的鏈接:

rIC = window.requestIdleCallback;ncIC = window.cancelIdleCallback;nexport {now, rIC, cIC};n

  1. 若不支持,則自定義實現:

let isIdleScheduled = false; // 是否在執行空閑期回調 let frameDeadlineObject = {n didTimeout: false,n timeRemaining() {n // now = Performance.now || Date.now const remaining = frameDeadline - now();n // 計算得到當前幀運行剩餘時間 return remaining > 0 ? remaining : 0;n },n};n// 幀回調 const animationTick = function(rafTime) {n ...n if (!isIdleScheduled) {n // 不在執行空閑期回調,表明可以調用空閑期回調n isIdleScheduled = true;n // 執行Idle空閑期回調n idleTick();n }n};n// 空閑期回調 const idleTick = function() {n // 重置為false,表明可以調用空閑期回調n isIdleScheduled = false;n const currentTime = now();n if (frameDeadline - currentTime <= 0) {n // 幀到期時間小於當前時間,說明已過期 if (timeoutTime !== -1 && timeoutTime <= currentTime) {n // 此幀已過期,且發生任務處理函數(執行具體任務,傳入的回調)的超時 // 需要執行任務處理,下文將調用;n frameDeadlineObject.didTimeout = true;n } else {n // 幀已過期,但沒有發生任務處理函數的超時,暫時不調用任務處理函數 if (!isAnimationFrameScheduled) {n // 當前沒有調度別的幀回調函數 // 調度下一幀n isAnimationFrameScheduled = true;n requestAnimationFrame(animationTick);n }n // Exit without invoking the callback. return;n }n } else {n // 這一幀還有剩餘時間 // 標記未超時,之後調用任務處理函數n frameDeadlineObject.didTimeout = false;n }nn // 緩存的任務處理函數n timeoutTime = -1;n const callback = scheduledRICCallback;n scheduledRICCallback = null;n if (callback !== null) {n // 執行回調n callback(frameDeadlineObject);n }n}nn// 自定義模擬requestIdleCallbacknrIC = function(n callback: (deadline: Deadline) => void, // 傳入的任務處理函數參數n options?: {timeout: number} // 其他參數n) {n // 回調函數n scheduledRICCallback = callback;n if (options != null && typeof options.timeout === number) {n // 計算過期時間n timeoutTime = now() + options.timeout;n }n if (!isAnimationFrameScheduled) {n // 當前沒有調度別的幀回調函數n isAnimationFrameScheduled = true;n // 初始開始執行幀回調 n requestAnimationFrame(animationTick);n }n return 0;n};n

    1. frameDeadline:是以啟發法,從30fps(即30幀)開始調整得到的更適於當前環境的一幀限制時間;
    2. timeRemaining:計算requestIdleCallback此次空閑(幀)執行任務剩餘時間,即距離deadline的時間;
    3. options.timeout:Fiber內部調用rICAPI執行非同步任務時,傳遞的任務到期時間參數;
    4. frameDeadlineObject:計算得到的某一幀可用時間對象,兩個屬性分別表示:
      1. didTimeout:傳入的非同步任務 處理函數是否超時;
      2. timeRemaining:當前幀可執行任務處理函數的剩餘空閑時間;
    1. frameDeadlineObject對象是基於傳入的timeout參數和此模塊內部自調整得到的frameDeadline參數計算得出;

Fiber與組件

我們已經知道了Fiber的功能及其主要特點,那麼其如何和組件聯繫,並且如何實現效果的呢,以下幾點可以概括:

  1. React應用中的基礎單元是組件,應用以組件樹形式組織,渲染組件;
  2. Fiber調和器基礎單元則是fiber(調和單元),應用以fiber樹形式組織,應用Fiber演算法;
  3. 組件樹和fiber樹結構對應,一個組件實例有一個對應的fiber實例;
  4. Fiber負責整個應用層面的調和,fiber實例負責對應組件的調和;

注意Fiber與fiber的區別,Fiber是指調和器演算法,fiber則是調和器演算法組成單元,和組件與應用關係類似,每一個組件實例會有對應的fiber實例負責該組件的調和。

Fiber數據結構

截止目前,我們對Fiber應該有了初步的了解,在具體介紹Fiber的實現與架構之前,準備先簡單介紹一下Fiber的數據結構,數據結構能一定程度反映其整體工作架構。

其實,一個fiber就是一個JavaScript對象,以鍵值對形式存儲了一個關聯組件的信息,包括組件接收的props,維護的state,最後需要渲染出的內容等。接下來我們將介Fiber對象的主要屬性。

Fiber對象

首先Fiber對象的定義如下:

// 一個Fiber對象作用於一個組件nexport type Fiber = {|n // 標記fiber類型tag.n tag: TypeOfWork,n // fiber對應的function/class/module類型組件名.n type: any,n // fiber所在組件樹的根組件FiberRoot對象n stateNode: any,n // 處理完當前fiber後返回的fiber,n // 返回當前fiber所在fiber樹的父級fiber實例n return: Fiber | null,n // fiber樹結構相關鏈接n child: Fiber | null,n sibling: Fiber | null,n index: number,nn // 當前處理過程中的組件props對象n pendingProps: any, n // 緩存的之前組件props對象n memoizedProps: any, // The props used to create the output.n // The state used to create the outputn memoizedState: any,nn // 組件狀態更新及對應回調函數的存儲隊列n updateQueue: UpdateQueue<any> | null,nnn // 描述當前fiber實例及其子fiber樹的數位,n // 如,AsyncUpdates特殊字表示默認以非同步形式處理子樹,n // 一個fiber實例創建時,此屬性繼承自父級fiber,在創建時也可以修改值,n // 但隨後將不可修改。n internalContextTag: TypeOfInternalContext,nn // 更新任務的最晚執行時間n expirationTime: ExpirationTime,nn // fiber的版本池,即記錄fiber更新過程,便於恢復n alternate: Fiber | null,nn // Conceptual aliasesn // workInProgress : Fiber -> alternate The alternate used for reuse happensn // to be the same as work in progress.n|};n

  1. type & key:同React元素的值;
  2. type:描述fiber對應的React組件;
    1. 對於組合組件:值為function或class組件本身;
    2. 對於原生組件(div等):值為該元素類型字元串;
  3. key:調和階段,標識fiber,以檢測是否可重用該fiber實例;
  4. child & sibling:組件樹,對應生成fiber樹,類比的關係;
  5. pendingProps & memoizedProps:分別表示組件當前傳入的及之前的props;
  6. return:返回當前fiber所在fiber樹的父級fiber實例,即當前組件的父組件對應的fiber;
  7. alternate:fiber的版本池,即記錄fiber更新過程,便於恢復重用;
  8. workInProgress:正在處理的fiber,概念上叫法,實際上沒有此屬性;

ALTERNATE FIBER

可以理解為一個fiber版本池,用於交替記錄組件更新(切分任務後變成多階段更新)過程中fiber的更新,因為在組件更新的各階段,更新前及更新過程中fiber狀態並不一致,在需要恢復時(如,發生衝突),即可使用另一者直接回退至上一版本fiber。

  1. 使用alternate屬性雙向連接一個當前fiber和其work-in-progress,當前fiber實例的alternate屬性指向其work-in-progress,work-in-progress的alternate屬性指向當前穩定fiber;
  2. 當前fiber的替換版本是其work-in-progress,work-in-progress的交替版本是當前fiber;
  3. 當work-in-progress更新一次後,將同步至當前fiber,然後繼續處理,同步直至任務完成;
  4. work-in-progress指向處理過程中的fiber,而當前fiber總是維護處理完成的最新版本的fiber。

創建FIBER實例

創建fiber實例即返回一個帶有上一小節描述的諸多屬性的JavaScript對象,FiberNode即根據傳入的參數構造返回一個初始化的對象:

var createFiber = function(n tag: TypeOfWork,n key: null | string,n internalContextTag: TypeOfInternalContext,n) {n return new FiberNode(tag, key, internalContextTag);n};n

創建alternate fiber以處理任務的實現如下:

// 創建一個alternate fiber處理任務nexport function createWorkInProgress(n current: Fiber,n pendingProps: any,n expirationTime: ExpirationTime,n) {n let workInProgress = current.alternate;n if (workInProgress === null) {n workInProgress = createFiber(n current.tag,n current.key,n current.internalContextTag,n );n workInProgress.type = current.type;n workInProgress.stateNode = current.stateNode;n // 形成alternate關係,互相交替模擬版本池n workInProgress.alternate = current;n current.alternate = workInProgress;n } nn workInProgress.expirationTime = expirationTime;n workInProgress.pendingProps = pendingProps;n workInProgress.child = current.child;n workInProgress.memoizedProps = current.memoizedProps;n workInProgress.memoizedState = current.memoizedState;n workInProgress.updateQueue = current.updateQueue;n ...n return workInProgress;n}n

Fiber類型

上一小節,Fiber對象中有個tag屬性,標記fiber類型,而fiber實例是和組件對應的,所以其類型基本上對應於組件類型,源碼見ReactTypeOfWork模塊:

export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;nnexport const IndeterminateComponent = 0; // 尚不知是類組件還是函數式組件nexport const FunctionalComponent = 1; // 函數式組件nexport const ClassComponent = 2; // Class類組件nexport const HostRoot = 3; // 組件樹根組件,可以嵌套nexport const HostPortal = 4; // 子樹. Could be an entry point to a different renderer.nexport const HostComponent = 5; // 標準組件,如地div, span等nexport const HostText = 6; // 文本nexport const CallComponent = 7; // 組件調用nexport const CallHandlerPhase = 8; // 調用組件方法nexport const ReturnComponent = 9; // placeholder(佔位符)nexport const Fragment = 10; // 片段n

在調度執行任務的時候會根據不同類型fiber,即fiber.tag值進行不同處理。

FiberRoot對象

FiberRoot對象,主要用來管理組件樹組件的更新進程,同時記錄組件樹掛載的DOM容器相關信息,具體定義見ReactFiberRoot模塊:

export type FiberRoot = {n // fiber節點的容器元素相關信息,通常會直接傳入容器元素n containerInfo: any,n // 當前fiber樹中激活狀態(正在處理)的fiber節點,n current: Fiber,n // 此節點剩餘的任務到期時間n remainingExpirationTime: ExpirationTime,n // 更新是否可以提交n isReadyForCommit: boolean,n // 準備好提交的已處理完成的work-in-progressn finishedWork: Fiber | null,n // 多組件樹FirberRoot對象以單鏈表存儲鏈接,指向下一個需要調度的FiberRootn nextScheduledRoot: FiberRoot | null,n};n

創建FIBERROOT實例

import {n ClassComponent,n HostRootn} from shared/ReactTypeOfWork;nn// 創建返回一個初始根組件對應的fiber實例nfunction createHostRootFiber(): Fiber {n // 創建fibern const fiber = createFiber(HostRoot, null, NoContext);n return fiber;n}nnexport function createFiberRoot(n containerInfo: any,n hydrate: boolean,n) {n // 創建初始根組件對應的fiber實例n const uninitializedFiber = createHostRootFiber();n // 組件樹根組件的FiberRoot對象n const root = {n // 根組件對應的fiber實例n current: uninitializedFiber,n containerInfo: containerInfo,n pendingChildren: null,n remainingExpirationTime: NoWork,n isReadyForCommit: false,n finishedWork: null,n context: null,n pendingContext: null,n hydrate,n nextScheduledRoot: null,n };n // 組件樹根組件fiber實例的stateNode指向FiberRoot對象n uninitializedFiber.stateNode = root;n return root;n}n

ReactChildFiber

在生成組件樹的FiberRoot對象後,會為子組件生成各自的fiber實例,這一部分由ReactChildFiber模塊實現:

// 調和(處理更新)子fibersnexport const reconcileChildFibers = ChildReconciler(true);n// 掛載(初始化)子fibersnexport const mountChildFibers = ChildReconciler(false);n

ChildReconciler方法所做的則是根據傳入參數判斷是調用初始化子組件fibers邏輯還是執行調和已有子組件fibers邏輯。

ChildReconciler方法,返回reconcileChildFibers方法:

  1. 判斷子級傳遞內容的數據類型,執行不同的處理,這也對應著我們寫React組件時傳遞props.children時,其類型可以是對象或數組,字元串,是數字等;
  2. 然後具體根據子組件類型,調用不同的具體調和處理函數;
  3. 最後返回根據子組件創建或更新得到的fiber實例;

function ChildReconciler(a) {n function reconcileChildFibers(n returnFiber: Fiber, currentFirstChild: Fiber | null,n newChild: any, expirationTime: ExpirationTime,n ) {n // Handle object typesn const isObject = typeof newChild === object && newChild !== null;nn if (isObject) {n // 子組件實例類型,以Symbol符號表示的n switch (newChild.$$typeof) {n // React Elementn case REACT_ELEMENT_TYPE:n return placeSingleChild(n reconcileSingleElement(n returnFiber, currentFirstChild,n newChild, expirationTimen )n );n // React組件調用n case REACT_CALL_TYPE:n return placeSingleChild(reconcileSingleCall(...));n // placeholdern case REACT_RETURN_TYPE:n return ...;n case REACT_PORTAL_TYPE:n return ...;n }n }n if (typeof newChild === string || typeof newChild === number) {n return placeSingleChild(reconcileSingleTextNode(...));n }n if (isArray(newChild)) {n return reconcileChildrenArray(...);n }n if (getIteratorFn(newChild)) {n return reconcileChildrenIterator(...);n }n ... n }n}n

Fiber架構

在學習Fiber的時候,我嘗試去閱讀源碼,發現通過這種方式很難快速理解,學習Fiber,而先了解調和器是幹什麼的及調和器在React中的存在形式,然後再學習Fiber的結構及演算法實現思路,明白從組件被定義到渲染至頁面它需要做什麼,這也是本篇文章的組織形式。

優先順序(ExpirationTime VS PriorityLevel)

我們已經知道Fiber可以切分任務並設置不同優先順序,那麼是如何實現劃分優先順序的呢,其表現形式什麼呢?

EXPIRATIONTIME

Fiber切分任務並調用requestIdleCallbackrequestAnimationFrameAPI,保證渲染任務和其他任務,在不影響應用交互,不掉幀的前提下,穩定執行,而實現調度的方式正是給每一個fiber實例設置到期執行時間,不同時間即代表不同優先順序,到期時間越短,則代表優先順序越高,需要儘早執行。

所謂的到期時間(ExpirationTime),是相對於調度器初始調用的起始時間而言的一個時間段;調度器初始調用後的某一段時間內,需要調度完成這項更新,這個時間段長度值就是到期時間值。

Fiber提供ReactFiberExpirationTime模塊實現到期時間的定義:

export const NoWork = 0; // 沒有任務等待處理nexport const Sync = 1; // 同步模式,立即處理任務nexport const Never = 2147483647; // Max int32: Math.pow(2, 31) - 1nconst UNIT_SIZE = 10; // 過期時間單元(ms)nconst MAGIC_NUMBER_OFFSET = 2; // 到期時間偏移量nn// 以ExpirationTime特定單位(1單位=10ms)表示的到期執行時間n// 1 unit of expiration time represents 10ms.nexport function msToExpirationTime (ms) {n // 總是增加一個偏移量,在ms<10時與Nowork模式進行區別n return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET;n}n// 以毫秒表示的到期執行時間nexport function expirationTimeToMs(expirationTime: ExpirationTime) {n return (expirationTime - MAGIC_NUMBER_OFFSET) * UNIT_SIZE;n}n// 向上取整(整數單位到期執行時間)n// precision範圍精度:彌補任務執行時間誤差nfunction ceiling(num, precision) {n return(((num / precision) | 0) + 1) * precision;n}nn// 計算處理誤差時間在內的到期時間nexport function computeExpirationBucket(currentTime, expirationInMs, bucketSizeMs,) {n return ceiling(n currentTime + expirationInMs / UNIT_SIZE,n bucketSizeMs / UNIT_SIZEn );n}n

該模塊提供的功能主要有:

  1. Sync:同步模式,在UI線程立即執行此類任務,如動畫反饋等;
  2. 非同步模式:
    1. 轉換:到期時間特定單位和時間單位(ms)的相互轉換;
    2. 計算:計算包含允許誤差在內的到期時間;

PRIORITYLEVEL

其實在15.x版本中出現了對於任務的優先層級劃分,ReactPriorityLevel模塊:

export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;nnmodule.exports = {n NoWork: 0, // No work is pending.n SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.n AnimationPriority: 2, // Needs to complete before the next frame.n HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive.n LowPriority: 4, // Data fetching, or result from updating stores.n OffscreenPriority: 5, // Wont be visible but do the work in case it becomes visible.n};n

相對於PriorityLevel的簡單層級劃分,在16.x版本中使用的則是ExpirationTime的到期時間方式表示任務的優先順序,可以更好的對任務進行切分,調度。

調度器(Scheduler)

前面介紹調和器主要作用就是在組件狀態變更時,調用組件樹各組件的render方法,渲染,卸載組件,而Fiber使得應用可以更好的協調不同任務的執行,調和器內關於高效協調的實現,我們可以稱它為調度器(Scheduler)。

顧名思義,調度器即調度資源以執行指定任務,React應用中應用組件的更新與渲染,需要佔用系統CPU資源,如果不能很好的進行資源平衡,合理調度,優化任務執行策略,那很容易造成CPU這一緊缺資源的消耗和浪費,容易造成頁面卡頓,動畫掉幀,組件更新異常等諸多問題,就像城市交通調度一樣,如果不能有效調度,交通狀況很可能將擁堵不堪。

在React 15.x版本中,組件的狀態變更將直接導致其子組件樹的重新渲染,新版本Fiber演算法將在調度器方面進行全面改進,主要的關注點是:

  1. 合併多次更新:沒有必要在組件的每一個狀態變更時都立即觸發更新任務,有些中間狀態變更其實是對更新任務所耗費資源的浪費,就比如用戶發現錯誤點擊時快速操作導致組件某狀態從A至B再至C,這中間的B狀態變更其實對於用戶而言並沒有意義,那麼我們可以直接合併狀態變更,直接從A至C只觸發一次更新;
  2. 任務優先順序:不同類型的更新有不同優先順序,例如用戶操作引起的交互動畫可能需要有更好的體驗,其優先順序應該比完成數據更新高;
  3. 推拉式調度:基於推送的調度方式更多的需要開發者編碼間接決定如何調度任務,而拉取式調度更方便React框架層直接進行全局自主調度;

傳送查看源碼

export default function () {n ...n return {n computeAsyncExpiration,n computeExpirationForFiber,n scheduleWork,n batchedUpdates,n unbatchedUpdates,n flushSync,n deferredUpdates,n };n}n

如上調度器主要輸出API為實現調度任務,拉取更新,延遲更新等功能。

調度器與優先順序

調度器如何切分任務劃分優先順序的呢?在React調和演算法中,任務由fiber實例描述,所以要劃分任務優先順序,等效於設置fiber的到期時間(expirationTime),調度器內提供了computeExpirationForFiber方法以計算某一個fiber的到期時間:

import { n NoWork, Sync, Never, msToExpirationTime,n expirationTimeToMs, computeExpirationBucketn} from ./ReactFiberExpirationTime;nn// 表示下一個要處理的任務的到期時間,默認為NoWork,即當前沒有正在等待執行的任務;n// Nowork默認更新策略:非同步模式下,非同步執行任務;同步模式下同步執行任務nlet expirationContext = NoWork;n// 下一次渲染到期時間nlet nextRenderExpirationTime = NoWork;n// 非同步更新nexport const AsyncUpdates = 1;n// 初始時間(ms).nconst startTime = now();n// ExpirationTime單位表示的當前時間(ExpirationTime單位,初始值傳入0)nlet mostRecentCurrentTime = msToExpirationTime(0);nn// 計算fiber的到期時間nfunction computeExpirationForFiber(fiber) {n let expirationTime;nn if (isWorking) {n if (isCommitting) {n // 在提交階段的更新任務n // 需要明確設置同步優先順序(Sync Priority)n expirationTime = Sync;n } else {n // 在渲染階段發生的更新任務n // 需要設置為下一次渲染時間的到期時間優先順序n expirationTime = nextRenderExpirationTime;n }n } else {n // 不在任務執行階段,需要計算新的過期時間nn // 明確傳遞useSyncScheduling為true表明期望同步調用n // 且fiber.internalContextTag != AsyncUpdatesn if (useSyncScheduling && !(fiber.internalContextTag & AsyncUpdates)) {n // 同步更新,設置為同步標記n expirationTime = Sync;n } else {n // 非同步更新,計算非同步到期時間n expirationTime = computeAsyncExpiration();n }n }n return expirationTime;n}n

  1. 若當前處於任務提交階段(更新提交至DOM渲染)時,設置當前fiber到期時間為Sync,即同步執行模式;
  2. 若處於DOM渲染階段時,則需要延遲此fiber任務,將fiber到期時間設置為下一次DOM渲染到期時間;
  3. 若不在任務執行階段,則需重新設置fiber到期時間:
    1. 若明確設置useSyncSchedulingfiber.internalContextTag值不等於AsyncUpdates,則表明是同步模式,設置為Sync
    2. 否則,調用computeAsyncExpiration方法重新計算此fiber的到期時間;

// 重新計算當前時間(ExpirationTime單位表示)nfunction recalculateCurrentTime() {n const ms = now() - startTime;n // ExpirationTime單位表示的當前時間n // 時間段值為 now() - startTime(起始時間)n mostRecentCurrentTime = msToExpirationTime(ms);n return mostRecentCurrentTime;n}nn// 計算非同步任務的到期時間nfunction computeAsyncExpiration() {n // 計算得到ExpirationTime單位的當前時間n // 聚合相似的更新在一起n // 更新應該在 ~1000ms,最多1200ms內完成n const currentTime = recalculateCurrentTime();n // 對於每個fiber的期望到期時間的增值,最大值為1000msn const expirationMs = 1000;n // 到期時間的可接受誤差時間,200msn const bucketSizeMs = 200;n // 返回包含誤差時間在內的到期時間n return computeExpirationBucket(currentTime, expirationMs, bucketSizeMs);n}n

對於每一個fiber我們期望的到期時間參數是1000ms,另外由於任務執行時間誤差,接受200ms誤差,最後計算得到的到期時間默認返回值為ExpirationTime單位。

任務調度

上一節介紹了調度器主要提供computeExpirationForFiber等方法支持計算任務優先順序(到期時間),接下來介紹調度器如何調度任務。

React應用更新時,Fiber從當前處理節點,層層遍歷至組件樹根組件,然後開始處理更新,調用前面的requestIdleCallback等API執行更新處理。

主要調度邏輯實現在scheduleWork

  1. 通過fiber.return屬性,從當前fiber實例層層遍歷至組件樹根組件;
  2. 依次對每一個fiber實例進行到期時間判斷,若大於傳入的期望任務到期時間參數,則將其更新為傳入的任務到期時間;
  3. 調用requestWork方法開始處理任務,並傳入獲取的組件樹根組件FiberRoot對象和任務到期時間;

// 調度任務n// expirationTime為期望的任務到期時間nfunction scheduleWork(fiber, expirationTime: ExpirationTime) {n return scheduleWorkImpl(fiber, expirationTime, false);n}nnfunction scheduleWorkImpl(n fiber, expirationTimen) {n let node = fiber;n while (node !== null) {n // 向上遍歷至根組件fiber實例,並依次更新expirationTime到期時間n if (n node.expirationTime === NoWork ||n node.expirationTime > expirationTimen ) {n // 若fiber實例到期時間大於期望的任務到期時間,則更新fiber到期時間n node.expirationTime = expirationTime;n }n // 同時更新alternate fiber的到期時間n if (node.alternate !== null) {n if (n node.alternate.expirationTime === NoWork ||n node.alternate.expirationTime > expirationTimen ) {n // 若alternate fiber到期時間大於期望的任務到期時間,則更新fiber到期時間n node.alternate.expirationTime = expirationTime;n }n }n // node.return為空,說明到達組件樹頂部n if (node.return === null) {n if (node.tag === HostRoot) {n // 確保是組件樹根組件並獲取FiberRoot實例n const root = node.stateNode;n // 請求處理任務n requestWork(root, expirationTime);n } else {n return;n }n }n // 獲取父級組件fiber實例n node = node.return;n }n}n

處理任務的requestWork方法實現如下:

  1. 首先比較任務剩餘到期時間和期望的任務到期時間,若大於,則更新值;
  2. 判斷任務期望到期時間(expirationTime),區分同步或非同步執行任務;

// 當根節點發生更新時,調度器將調用requestWork方法開始任務處理過程n// Its up to the renderer to call renderRoot at some point in the future.nfunction requestWork(root: FiberRoot, expirationTime) {n const remainingExpirationTime = root.remainingExpirationTime;n if (remainingExpirationTime === NoWork ||n expirationTime < remainingExpirationTime) {n // 若任務剩餘到期時間大於期望的任務到期時間,則需要更新n root.remainingExpirationTime = expirationTime;n }nn if (expirationTime === Sync) {n // 同步n performWork(Sync, null);n } else {n // 非同步n scheduleCallbackWithExpiration(expirationTime);n }n}n

更新隊列(UpdateQueue)

我們知道如果需要實現組件的非同步更新,肯定需要在更新前將更新任務進行存儲,然後非同步任務開始的時候讀取更新並實現組件更新,存儲更新任務就需要一個數據結構,最常見的就是棧和隊列,Fiber的實現方式就是隊列。

Fiber切分任務為多個任務單元(Work Unit)後,需要劃分優先順序然後存儲在更新隊列,隨後按優先順序進行調度執行。我們知道每一個組件都對應有一個fiber實例,fiber實例即負責管理調度組件的任務單元,所以需要為每一個組件fiber實例維護一個更新隊列。

Fiber更新隊列由ReactFiberUpdateQueue模塊實現,主要涉及:

  1. 創建更新隊列;
  2. 添加更新至更新隊列;
  3. 添加更新至fiber(即fiber實例對應的更新隊列);
  4. 處理更新隊列中的更新並返回新狀態對象:

// 一個更新對應的數據結構nexport type Update<State> = {n expirationTime: ExpirationTime,n partialState: PartialState<any, any>,n callback: Callback | null,n isReplace: boolean,n isForced: boolean,n next: Update<State> | null,n};nn// 更新隊列,以單鏈表形式表示並持久化n// 調度一個更新任務時,將其添加至當前(current)fiber和work-in-progress fiber的更新隊列中;n// 這兩個更新隊列相互獨立但共享同一個持久化數據結構;n// work-in-progress更新隊列通常是current fiber更新隊列的子集;n// 發生調和時,更新任務從work-in-progress fiber更新隊列移除,n// current fiber內的更新任務則保留,當work-in-progress中斷時可以從current fiber恢復;n// 提交完更新時,work-in-progress fiber就會變成current fibernexport type UpdateQueue<State> = {n // 若存在更早添加至隊列的更新未被處理,n // 則此已處理的更新並不會從隊列中移除-先進先出原則n // 所以需要維護baseState,代表第一個未處理的更新的基礎狀態,n // 通常這就是隊列中的第一個更新,因為在隊列首部的已處理更新會被移除n baseState: State,n // 同理,需要維護最近的未處理的更新的到期時間,n // 即未處理更新中到期時間值最小的n expirationTime: ExpirationTime,n first: Update<State> | null,n last: Update<State> | null,n callbackList: Array<Update<State>> | null,n hasForceUpdate: boolean,n isInitialized: booleann};nn// 添加更新至更新隊列nexport function insertUpdateIntoQueue<State>(n queue: UpdateQueue<State>,n update: Update<State>n){n // 添加更新至隊列尾部n if (queue.last === null) {n // 隊列為空n queue.first = queue.last = update;n } else {n queue.last.next = update;n queue.last = update;n }n if (n queue.expirationTime === NoWork ||n queue.expirationTime > update.expirationTimen ) {n // 更新最近到期時間n queue.expirationTime = update.expirationTime;n }n}n// 添加更新至fiber實例nexport function insertUpdateIntoFiber<State>(n fiber: Fiber,n update: Update<State>,n) {n // 可以創建兩個獨立的更新隊列n // alternate主要用來保存更新過程中各版本更新隊列,方便崩潰或衝突時回退n const alternateFiber = fiber.alternate;n let queue1 = fiber.updateQueue;n if (queue1 === null) {n // 更新隊列不存在,則創建一個空的更新隊列n queue1 = fiber.updateQueue = createUpdateQueue((null));n }nn let queue2;n if (alternateFiber !== null) {n // alternate fiber實例存在,則需要為此n queue2 = alternateFiber.updateQueue;n if (queue2 === null) {n queue2 = alternateFiber.updateQueue = createUpdateQueue((null: any));n }n } else {n queue2 = null;n }n queue2 = queue2 !== queue1 ? queue2 : null;nn // 如果只存在一個更新隊列n if (queue2 === null) {n insertUpdateIntoQueue(queue1, update);n return;n }nn // 如果任意更新隊列為空,則需要將更新添加至兩個更新隊列n if (queue1.last === null || queue2.last === null) {n insertUpdateIntoQueue(queue1, update);n insertUpdateIntoQueue(queue2, update);n return;n }nn // 如果2個更新隊列均非空,則添加更新至第一個隊列,並更新另一個隊列的尾部更新項n insertUpdateIntoQueue(queue1, update);n queue2.last = update;n}nn// 處理更新隊列任務,返回新狀態對象nexport function processUpdateQueue<State>(n current, workInProgress, queue, instance, props,n renderExpirationTime,n) {n if (current !== null && current.updateQueue === queue) {n // 克隆current fiber以創建work-in-progress fibern const currentQueue = queue;n queue = workInProgress.updateQueue = {n baseState: currentQueue.baseState,n expirationTime: currentQueue.expirationTime,n first: currentQueue.first,n last: currentQueue.last,n isInitialized: currentQueue.isInitialized,n // These fields are no longer valid because they were already committed. Reset them.n callbackList: null,n hasForceUpdate: false,n };n }nn // Reset the remaining expiration time. If we skip over any updates, welln // increase this accordingly.n queue.expirationTime = NoWork;nn let dontMutatePrevState = true;n let update = queue.first;n let didSkip = false;n while (update !== null) {n const updateExpirationTime = update.expirationTime;n if (updateExpirationTime > renderExpirationTime) {n // 此更新優先順序不夠,不處理,跳過n if (queue.expirationTime === NoWork ||n queue.expirationTime > updateExpirationTimen ) {n // 重新設置最近未處理更新的到期時間n queue.expirationTime = updateExpirationTime;n }n update = update.next;n continue;n }nn // 優先順序足夠,處理n let partialState;n if (update.isReplace) {n // 使用replaceState()直接替換狀態對象方式更新時n // 獲取新狀態對象n state = getStateFromUpdate(update, instance, state, props);n // 不需要合併至之前狀態對象,標記為truen dontMutatePrevState = true;n } else {n // 更新部分狀態方式n // 獲取更新部分狀態時的狀態對象n partialState = getStateFromUpdate(update, instance, state, props);n if (partialState) {n if (dontMutatePrevState) {n // 上一次是替換狀態,所以不能影響staten state = Object.assign({}, state, partialState);n } else {n // 更新部分狀態,直接將新狀態合併至上一次狀態n state = Object.assign(state, partialState);n }n // 重置標記為falsen dontMutatePrevState = false;n }n }n // 強制立即更新n if (update.isForced) {n queue.hasForceUpdate = true;n }n // 添加回調函數n if (update.callback !== null) {n // Append to list of callbacks.n let callbackList = queue.callbackList;n if (callbackList === null) {n callbackList = queue.callbackList = [];n }n callbackList.push(update);n }n // 遍歷下一個更新任務n update = update.next;n }n // 返回最新的狀態對象n return state;n}n

更新器(Updater)

調度器協調,調度的任務主要就是執行組件或組件樹更新,而這些任務則具體由更新器(Updater)完成,可以說調度器是在整個應用組件樹層面掌控全局,而更新器則深入到個更具體的每一個組件內部執行。

每一個組件實例化時都會被注入一個更新器,負責協調組件與React核心進程的通信,其職責主要可以概括為以下幾點:

  1. 找到組件實例對應的fiber實例;
  2. 詢問調度器當前組件fiber實例的優先順序;
  3. 將更新推入fiber的更新隊列;
  4. 根據優先順序調度更新任務;

更新器實現見ReactFiberClassComponent模塊:

export default function(n scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,n computeExpirationForFiber: (fiber: Fiber) => ExpirationTime,n memoizeProps: (workInProgress: Fiber, props: any) => void,n memoizeState: (workInProgress: Fiber, state: any) => void,n) {n // Class component state updatern const updater = {n isMounted,n // 狀態變更,更新入隊列n enqueueSetState(instance, partialState, callback) {n // 獲取fibern const fiber = ReactInstanceMap.get(instance);n const expirationTime = computeExpirationForFiber(fiber);n // 創建更新任務n const update = {n expirationTime,n partialState,n callback,n isReplace: false,n isForced: false,n nextCallback: null,n next: null,n };n // 添加更新任務至fibern insertUpdateIntoFiber(fiber, update);n // 調用調度器API以調度fiber任務n scheduleWork(fiber, expirationTime);n },n // 替換狀態時n enqueueReplaceState(instance, state, callback) {n const fiber = ReactInstanceMap.get(instance);n const expirationTime = computeExpirationForFiber(fiber);n const update = {n expirationTime,n partialState: state,n callback,n isReplace: true,n isForced: false,n nextCallback: null,n next: null,n };n // 添加更新任務至fibern insertUpdateIntoFiber(fiber, update);n scheduleWork(fiber, expirationTime);n },n // 強制更新n enqueueForceUpdate(instance, callback) {n const fiber = ReactInstanceMap.get(instance);n const expirationTime = computeExpirationForFiber(fiber);n const update = {n expirationTime,n partialState: null,n callback,n isReplace: false,n isForced: true,n nextCallback: null,n next: null,n };n insertUpdateIntoFiber(fiber, update);n scheduleWork(fiber, expirationTime);n },n };nn // 調用組件實例生命周期方法並調用更新器APIn function callComponentWillReceiveProps(n workInProgress, instance, newProps, newContextn ) {n const oldState = instance.state;n instance.componentWillReceiveProps(newProps, newContext);nn if (instance.state !== oldState) {n // 調用更新器入隊列方法n updater.enqueueReplaceState(instance, instance.state, null);n }n }nn // 設置Class組件實例的更新器和fibern function adoptClassInstance(workInProgress, instance): {n // 設置更新器n instance.updater = updater;n workInProgress.stateNode = instance;n // 設置fibern ReactInstanceMap.set(instance, workInProgress);n }nn // 實例化Class組件實例n function constructClassInstance(workInProgress, props) {n const ctor = workInProgress.type;n const unmaskedContext = getUnmaskedContext(workInProgress);n const needsContext = isContextConsumer(workInProgress);n const context = needsContextn ? getMaskedContext(workInProgress, unmaskedContext)n : emptyObject;n // 實例化組件類型n const instance = new ctor(props, context);n // 設置Class實例的更新器和fibern adoptClassInstance(workInProgress, instance);nn return instance;n }nn // 掛載組件實例n function mountClassInstance(n workInProgress, renderExpirationTime) {n if (typeof instance.componentWillMount === function) {n callComponentWillMount(workInProgress, instance);n }n }nn // 更新組件實例 n function updateClassInstance(n current, workInProgress, renderExpirationTimen ) {n // 組件實例n const instance = workInProgress.stateNode;n // 原Props或新Propsn const oldProps = workInProgress.memoizedProps;n let newProps = workInProgress.pendingProps;n if (!newProps) {n // 沒有新Props則直接使用原Propsn newProps = oldProps;n }nn if (typeof instance.componentWillReceiveProps === function &&n (oldProps !== newProps)) {n // 調用方法進行更新器相關處理n callComponentWillReceiveProps(n workInProgress, instance, newPropsn );n }nn // 根據原狀態對象和更新隊列計算得到新狀態對象n const oldState = workInProgress.memoizedState;n let newState;n if (workInProgress.updateQueue !== null) {n // 處理更新隊列更新,計算得到新State對象n newState = processUpdateQueue(n current,n workInProgress,n workInProgress.updateQueue,n instance,n newProps,n renderExpirationTime,n );n } else {n newState = oldState;n }nn // 檢查是否需要更新組件n const shouldUpdate = checkShouldComponentUpdate(...);nn if (shouldUpdate) {n if (typeof instance.componentWillUpdate === function) { n instance.componentWillUpdate(newProps, newState, newContext); n }n }n // 調用生命周期方法n ...n return shouldUpdate;n }nn return {n adoptClassInstance,n constructClassInstance,n mountClassInstance,n updateClassInstancen };n}n

主要實現以下幾個功能:

  1. 初始化組件實例並為其設置fibre實例和更新器;
  2. 初始化或更新組件實例,根據更新隊列計算得到新狀態等;
  3. 調用組件實例生命周期方法,並且調用更新器API更新fiber實例等,如更新組件實例調用的callComponentWillReceiveProps方法,該方法調用組件實例的componentWillReceiveProps生命周期方法,並調用更新器updater.enqueueReplaceState方法,更新fiber實例,並將更新添加至更新隊列:

// 調用組件實例生命周期方法並調用更新器API function callComponentWillReceiveProps(nworkInProgress, instance, newProps, newContextn) {n const oldState = instance.state;n instance.componentWillReceiveProps(newProps, newContext);nn if (instance.state !== oldState) {n // 調用更新器入隊列方法n updater.enqueueReplaceState(instance, instance.state, null);n }n}n

另外需要重點關注的是insertUpdateIntoFiber方法,該方法實現將更新任務添加至組件fiber實例,內部會處理將任務添加至fiber更新隊列,源碼見上文更新隊列中介紹的ReactFiberUpdateQueue模塊,最終還是調用insertUpdateIntoQueue

獲取FIBER實例

獲取fiber實例比較簡單,fiber實例通過ReactInstanceMap模塊提供的API進行維護:

export function get(key) {n return key._reactInternalFiber;n}nexport function set(key, value) {n key._reactInternalFiber = value;n}n

使用節點上的_reactInternalFiber屬性維護fiber實例,調用get方法即可獲取。

獲取優先順序

fiber實例的優先順序是由調度器控制,所以需要詢問調度器關於當前fiber實例的優先順序,調度器提供computeExpirationForFiber獲取特定fiber實例的優先順序,即獲取特點fiber實例的到期時間(expirationTime),方法具體實現見調度器與優先順序章節。

將更新任務添加至更新隊列

組件狀態變更時,將對應的組件更新任務劃分優先順序並根據優先順序從高到低依次推入fiber實例的更新隊列,諸如使用setState方法觸發的更新任務通常是添加至更新隊列尾部。

調度器完成切分任務為任務單元後,將使用performUnitOfWork方法開始處理任務單元,然後按調用組件的更新器(實現見上文介紹)相關API,按優先順序將任務單元添加至fiber實例的更新隊列:

  1. 從work-in-progress的alternate屬性獲取當前穩定fiber,然後調用beginWork開始處理更新;

// 處理任務單元 function performUnitOfWork(workInProgress: Fiber): Fiber | null {n // 當前最新版本fiber實例使用fiber的alternate屬性獲取 const current = workInProgress.alternate;n // 開始處理,返回子組件fiber實例 let next = beginWork(current, workInProgress, nextRenderExpirationTime);n if (next === null) {n // 不存在子級fiber,完成單元任務的處理,之後繼續處理下一個任務n next = completeUnitOfWork(workInProgress);n }n return next;n}n

  1. beginWork返回傳入fiber實例的子組件fiber實例,,若為空,則代表此組件樹任務處理完成,否則會在workLoop 方法內迭代調用performUnitOfWork方法處理:
    1. deadline:是調用requestIdleCallbackAPI執行任務處理函數時返回的幀時間對象;
    2. nextUnitOfWork:下一個要處理的任務單元;
    3. shouldYield:判斷是否暫停當前任務處理過程;

function workLoop(expirationTime) {n // 渲染更新至DOM的到期時間值 小於 調度開始至開始處理此fiber的時間段值 // 說明任務已經過期 if (nextRenderExpirationTime <= mostRecentCurrentTime) {n // Flush all expired work, 處理所有已經到期的更新 while (nextUnitOfWork !== null) {n nextUnitOfWork = performUnitOfWork(nextUnitOfWork);n }n } else {n // Flush asynchronous work until the deadline runs out of time. // 依次處理非同步更新,直至deadline到達 while (nextUnitOfWork !== null && !shouldYield()) {n nextUnitOfWork = performUnitOfWork(nextUnitOfWork);n }n }n}n// 處理非同步任務時, 調和器將詢問渲染器是否暫停執行; // 在DOM中,使用requestIdleCallback API實現 function shouldYield() {n if (deadline === null) {n return false;n }n if (deadline.timeRemaining() > 1) {n // 這一幀幀還有剩餘時間,不需要暫停; // 只有非過期任務可以到達此判斷條件 return false;n }n deadlineDidExpire = true;n return true;n}n

  1. beginWork方法內根據組件類型調用不同方法,這些方法內調用更新器API將更新添加至更新隊列,具體實現見ReactFiberBeginWork模塊:

// 引入更新器模塊nimport ReactFiberClassComponent from ./ReactFiberClassComponent;nnexport default function(n config, hostContext, hydrationContext,n scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,n computeExpirationForFiber: (fiber: Fiber) => ExpirationTime,n) {n // 初始化更新器模塊,獲取API const {n adoptClassInstance, constructClassInstance,n mountClassInstance, updateClassInstancen } = ReactFiberClassComponent(n scheduleWork, computeExpirationForFiber,n memoizeProps, memoizeStaten );nn // beginWork,開始任務處理 function beginWork(n current, workInProgress, renderExpirationTimen ) {n switch (workInProgress.tag) {n // 對應不同類型fiber,執行不同處理邏輯 case IndeterminateComponent:n ...n case FunctionalComponent:n return updateFunctionalComponent(current, workInProgress);n case ClassComponent:n // 更新類組件,返回子級fiber實例 return updateClassComponent(n current, workInProgress, renderExpirationTimen );n case HostRoot:n return updateHostRoot(current, workInProgress, renderExpirationTime);n case HostComponent:n ...n case HostText:n return updateHostText(current, workInProgress);n case CallHandlerPhase:n // This is a restart. Reset the tag to the initial phase.n workInProgress.tag = CallComponent;n case CallComponent:n ...n case ReturnComponent:n // A return component is just a placeholder, we can just run through the // next one immediately. return null;n case HostPortal:n ...n case Fragment:n return updateFragment(current, workInProgress);n default:;n }n }nn return {n beginWork,n beginFailedWorkn };n}n

    1. 引入ReactFiberClassComponent更新器相關模塊並初始化獲得API;
    2. beginWork方法內根據傳入的work-in-progress的fiber類型(tag)調用不同邏輯處理;
    3. 在邏輯處理裡面會調用更新期API,將更新添加至更新隊列;
    4. ClassComponent為例,將調用updateClassComponent方法:
      1. 判斷若第一次則初始化並掛載組件實例,否則調用updateClassInstance方法更新組件實例;
      2. 最後調用finishClassComponent方法,調和處理其子組件並返回其子級fiber實例;

// 更新類組件 function updateClassComponent(ncurrent, workInProgress, renderExpirationTimen) {nlet shouldUpdate;nif (current === null) {n if (!workInProgress.stateNode) {n // fiber沒有組件實例時需要初始化組件實例n constructClassInstance(workInProgress, workInProgress.pendingProps);n // 掛載組件實例n mountClassInstance(workInProgress, renderExpirationTime);n // 默認需要更新n shouldUpdate = true;n }n} else {n // 處理實例更新並返回是否需要更新組件n shouldUpdate = updateClassInstance(n current,n workInProgress,n renderExpirationTime,n );n}n// 更新完成後,返回子組件fiber實例 return finishClassComponent(n current, workInProgress, shouldUpdate, hasContextn);n}nn// 類組件更新完成 function finishClassComponent(ncurrent, workInProgress, shouldUpdate, hasContextn) {nif (!shouldUpdate) {n // 明確設置不需要更新時,不處理更新, // 如shouldCOmponentUpdate方法return false return bailoutOnAlreadyFinishedWork(current, workInProgress);n}nnconst instance = workInProgress.stateNode;n// 重新渲染nReactCurrentOwner.current = workInProgress;n// 返回組件子組件樹等內容 let nextChildren = instance.render();n// 調和子組件樹,將迭代處理每一個組件 // 函數內將調用ReactChildFiber模塊提供的APInreconcileChildren(current, workInProgress, nextChildren);n// 返回子組件fiber實例 return workInProgress.child;n}n

調度更新任務

上一節更新器已經能按照優先順序將更新添加至更新隊列,那麼如何調度執行更新任務呢?

在更新器實現ReactFiberClassComponent模塊中,在enqueueSetStateenqueueReplaceStateenqueueForceUpdate入隊列方法中,均會調用如下方法:

insertUpdateIntoFiber(fiber, update);nscheduleWork(fiber, expirationTime);n

  1. insertUpdateIntoFiber:將更新添加至fiber實例,最終會添加至更新隊列;
  2. scheduleWork:調度任務,傳入fiber實例和任務到期時間;

渲染與調和

在調和階段,不涉及任何DOM處理,在處理完更新後,需要渲染模塊將更新渲染至DOM,這也是React應用中虛擬DOM(Virtual DOM)的概念,即所有的更新計算都基於虛擬DOM,計算完後才將優化後的更新渲染至真實DOM。Fiber使用requestIdleCallbackAPI更高效的執行渲染更新的任務,實現任務的切分。

源碼簡單分析

本小節針對React渲染模塊及調和演算法模塊代碼層關係做簡要探討,不感興趣可以跳過此劫(節)。

REACT-DOM渲染模塊

在項目中,如果要將應用渲染至頁面,通常會有如下代碼:

import ReactDOM from react-dom;nimport App form ./App; // 應用根組件nnReactDOM.render(n <App>,n document.querySelector(#App) // 應用掛載容器DOMn);n

react-dom模塊就是適用於瀏覽器端渲染React應用的渲染方案,ReactDOM模塊源碼結構如:

const ReactDOM = {n render(n element: React$Element<any>, // React元素,通常是項目根組件n container: DOMContainer, // React應用掛載的DOM容器n callback: ?Function, // 回調函數n ) {n return renderSubtreeIntoContainer(n null,n element,n container,n false,n callback,n );n }n};n

常用的渲染組件至DOM的render方法如上,調用renderSubtreeIntoContainer方法,渲染組件的子組件樹:

// 渲染組件的子組件樹至父容器nfunction renderSubtreeIntoContainer(n parentComponent: ?React$Component<any, any>,n children: ReactNodeList,n container: DOMContainer,n forceHydrate: boolean,n callback: ?Function,n) {n let root = container._reactRootContainer;n if (!root) {n // 初次渲染時初始化n // 創建react根容器n const newRoot = DOMRenderer.createContainer(container, shouldHydrate);n // 緩存react根容器至DOM容器的reactRootContainer屬性n root = container._reactRootContainer = newRoot;n // 初始化容器相關n // Initial mount should not be batched.n DOMRenderer.unbatchedUpdates(() => {n DOMRenderer.updateContainer(children, newRoot, parentComponent, callback);n });n } else {n // 如果不是初次渲染則直接更新容器n DOMRenderer.updateContainer(children, root, parentComponent, callback);n }n // 返回根容器fiber樹的根fiber實例n return DOMRenderer.getPublicRootInstance(root); n}n

DOM渲染器對象

DOMRenderer是調用調和演算法返回的DOM渲染器對象,在此處會傳入渲染模塊的渲染UI操作API,如:

// 調用調和演算法方法nconst DOMRenderer = ReactFiberReconciler(n // 傳遞至調和演算法中的渲染UI(react-dom模塊即DOM)n // 實際操作APIn {n getPublicInstance(instance) {n return instance;n },n createInstance(n type: string,n props: Props,n rootContainerInstance: Container,n hostContext: HostContext,n internalInstanceHandle: Object,n ) {n // 創建DOM元素n const domElement = createElement(n type,n props,n rootContainerInstance,n parentNamespace,n );n precacheFiberNode(internalInstanceHandle, domElement);n updateFiberProps(domElement, props);n return domElement; n },n now: ReactDOMFrameScheduling.now,n mutation: {n // 提交渲染n commitMount(n domElement: Instance,n type: string,n newProps: Props,n internalInstanceHandle: Object,n ) {n ((domElement: any):n | HTMLButtonElementn | HTMLInputElementn | HTMLSelectElementn | HTMLTextAreaElement).focus();n },n // 提交更新n commitUpdate(n domElement: Instance,n updatePayload: Array<mixed>,n type: string,n oldProps: Props,n newProps: Props,n internalInstanceHandle: Object,n ) {n // 更新屬性n updateFiberProps(domElement, newProps);n // 對DOM節點進行Diff演算法分析n updateProperties(domElement, updatePayload, type, oldProps, newProps);n },n // 清空文本內容n resetTextContent(domElement: Instance): void {n domElement.textContent = ;n },n // 添加為子級n appendChild(n parentInstance: Instance,n child: Instance | TextInstance,n ): void {n parentInstance.appendChild(child);n }n ...n }n});n

ReactDOMFrameScheduling.now源碼見Github。

在任務完成時將執行createInstance方法,然後調用createElement創建DOM元素並添加至文檔。

調和演算法入口

調和演算法入口:

import ReactFiberScheduler from ./ReactFiberScheduler;nimport {insertUpdateIntoFiber} from ./ReactFiberUpdateQueue;nnexport default function Reconciler(n // all parameters as config objectn // 下文用到的config參數即從此處傳入n getPublicInstance,n createInstance,n ...n) {n // 生成調度器APIn var {n computeAsyncExpiration, computeExpirationForFiber, scheduleWork,n batchedUpdates, unbatchedUpdates, flushSync, deferredUpdates,n } = ReactFiberScheduler(config);nn return {n // 創建容器n createContainer(containerInfo, hydrate: boolean) {n // 創建根fiber實例n return createFiberRoot(containerInfo, hydrate);n },n // 更新容器內容n updateContainer(n element: ReactNodeList,n container: OpaqueRoot,n parentComponent: ?React$Component<any, any>,n callback: ?Function,n ): void {n const current = container.current;n ...n // 更新n scheduleTopLevelUpdate(current, element, callback);n },n ...n // 獲取容器fiber樹的根fiber實例n getPublicRootInstance (container) {n // 獲取fiber實例n const containerFiber = container.current;n if (!containerFiber.child) {n return null;n }n switch (containerFiber.child.tag) {n case HostComponent:n return getPublicInstance(containerFiber.child.stateNode);n default:n return containerFiber.child.stateNode;n }n },n unbatchedUpdatesn }n}n

react-dom渲染模塊調用createContainer創建容器和根fiber實例,FiberRoot對象,調用updateContainer方法更新容器內容。

開始更新

// 更新nfunction scheduleTopLevelUpdate(n current: Fiber,n element: ReactNodeList,n callback: ?Function,n ) {n callback = callback === undefined ? null : callback;n const update = {n expirationTime,n partialState: {element},n callback,n isReplace: false,n isForced: false,n nextCallback: null,n next: null,n };n // 更新fiber實例n insertUpdateIntoFiber(current, update);n // 執行任務n scheduleWork(current, expirationTime);n}n

處理更新

調用scheduleWork方法處理更新任務,實現見上文,源碼。

提交更新

處理完更新後需要確認提交更新至渲染模塊,然後渲染模塊才能將更新渲染至DOM。

import ReactFiberCommitWork from ./ReactFiberCommitWork;nnconst {n commitResetTextContent,n commitPlacement,n commitDeletion,n commitWork,n commitLifeCycles,n commitAttachRef,n commitDetachRef,n } = ReactFiberCommitWork(config, captureError);nnfunction commitRoot(finishedWork) {n ...n commitAllHostEffects();n}n// 循環執行提交更新nfunction commitAllHostEffects() {n while (nextEffect !== null) {n let primaryEffectTag =n effectTag & ~(Callback | Err | ContentReset | Ref | PerformedWork);n switch (primaryEffectTag) {n case Placement: {n commitPlacement(nextEffect);n nextEffect.effectTag &= ~Placement;n break;n }n case PlacementAndUpdate: {n // Placementn commitPlacement(nextEffect);n nextEffect.effectTag &= ~Placement;n // Updaten const current = nextEffect.alternate;n commitWork(current, nextEffect);n break;n }n case Update: {n const current = nextEffect.alternate;n commitWork(current, nextEffect);n break;n }n case Deletion: {n isUnmounting = true;n commitDeletion(nextEffect);n isUnmounting = false;n break;n }n }n nextEffect = nextEffect.nextEffect;n }n}n// Flush sync work.nlet finishedWork = root.finishedWork;nif (finishedWork !== null) {n // This root is already complete. We can commit it.n root.finishedWork = null;n root.remainingExpirationTime = commitRoot(finishedWork);n}n

提交更新是最後確認更新組件的階段,主要邏輯如下:

export default function (mutation, ...) {n const {n commitMount,n commitUpdate,n resetTextContent,n commitTextUpdate,n appendChild,n appendChildToContainer,n insertBefore,n insertInContainerBefore,n removeChild,n removeChildFromContainer,n } = mutation; nn function commitWork(current: Fiber | null, finishedWork: Fiber): void {n switch (finishedWork.tag) {n case ClassComponent: {n return;n }n case HostComponent: {n const instance: I = finishedWork.stateNode;n if (instance != null) {n // Commit the work prepared earlier.n const newProps = finishedWork.memoizedProps;n // For hydration we reuse the update path but we treat the oldPropsn // as the newProps. The updatePayload will contain the real change inn // this case.n const oldProps = current !== null ? current.memoizedProps : newProps;n const type = finishedWork.type;n // TODO: Type the updateQueue to be specific to host components.n const updatePayload = finishedWork.updateQueue:;n finishedWork.updateQueue = null;n if (updatePayload !== null) {n commitUpdate(n instance,n updatePayload,n type,n oldProps,n newProps,n finishedWork,n );n }n }n return;n }n case HostText: { n const textInstance = finishedWork.stateNode;n const newText = finishedWork.memoizedProps;n // For hydration we reuse the update path but we treat the oldPropsn // as the newProps. The updatePayload will contain the real change inn // this case.n const oldText: string =n current !== null ? current.memoizedProps : newText;n commitTextUpdate(textInstance, oldText, newText);n return;n }n case HostRoot: {n return;n }n default: {n }n }n }n}n

參考

  1. React Source Code
  2. React Fiber Architecture
  3. A look inside React Fiber
  4. An overview of React 16 features and Fiber
  5. requestIdleCallback

推薦閱讀:

阿里前端推出新的 React 框架:Mirror
基於 Webpack 3 的 React 工程項目腳手架
2016-我的前端之路:工具化與工程化
Parcel,零配置開發 React 應用!
Web Component 和類 React、Angular、Vue 組件化技術誰會成為未來?

TAG:React |