React Fiber初探

前端魔法師發表於2017-12-02

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';
複製程式碼

這句程式碼做的就是引入了React核心原始碼模組。

渲染

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

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

    import React from 'react';
    import { render } from 'react-dom';
    import App from './apps/App.js';

    render(
      <App />,
      document.getElementById('mainBox')
    );
    複製程式碼

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

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

    import { AppRegistry } from 'react-native';
    import App from './src/app.js';

    AppRegistry.registerComponent('fuc', () => App);
    複製程式碼

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

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

    import ReactTestRenderer from 'react-test-renderer';
     
    const renderer = ReactTestRenderer.create(
      <Link page="https://www.facebook.com/">Facebook</Link>
    );
     
    console.log(renderer.toJSON());
    // { type: 'a',
    //   props: { href: 'https://www.facebook.com/' },
    //   children: [ 'Facebook' ] }
    複製程式碼
  4. 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),執行一些任務。

requestIdleCallback

Fiber與requestIdleCallback

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

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

具體執行任務實現原始碼傳送

  1. 若支援原生API,具體原生實現見上文給出的連結:

    rIC = window.requestIdleCallback;
    cIC = window.cancelIdleCallback;
    export {now, rIC, cIC};
    複製程式碼
  2. 若不支援,則自定義實現:

    let isIdleScheduled = false; // 是否在執行空閒期回撥
    let frameDeadlineObject = {
      didTimeout: false,
      timeRemaining() {
        // now = Performance.now || Date.now
        const remaining = frameDeadline - now();
        // 計算得到當前幀執行剩餘時間
        return remaining > 0 ? remaining : 0;
      },
    };
    // 幀回撥
    const animationTick = function(rafTime) {
      ...
      if (!isIdleScheduled) {
        // 不在執行空閒期回撥,表明可以呼叫空閒期回撥
        isIdleScheduled = true;
        // 執行Idle空閒期回撥
        idleTick();
      }
    };
    // 空閒期回撥
    const idleTick = function() {
      // 重置為false,表明可以呼叫空閒期回撥
      isIdleScheduled = false;
      const currentTime = now();
      if (frameDeadline - currentTime <= 0) {
        // 幀到期時間小於當前時間,說明已過期
        if (timeoutTime !== -1 && timeoutTime <= currentTime) {
          // 此幀已過期,且發生任務處理函式(執行具體任務,傳入的回撥)的超時
          // 需要執行任務處理,下文將呼叫;
          frameDeadlineObject.didTimeout = true;
        } else {
          // 幀已過期,但沒有發生任務處理函式的超時,暫時不呼叫任務處理函式
          if (!isAnimationFrameScheduled) {
            // 當前沒有排程別的幀回撥函式
            // 排程下一幀
            isAnimationFrameScheduled = true;
            requestAnimationFrame(animationTick);
          }
          // Exit without invoking the callback.
          return;
        }
      } else {
        // 這一幀還有剩餘時間
        // 標記未超時,之後呼叫任務處理函式
        frameDeadlineObject.didTimeout = false;
      }

      // 快取的任務處理函式
      timeoutTime = -1;
      const callback = scheduledRICCallback;
      scheduledRICCallback = null;
      if (callback !== null) {
        // 執行回撥
        callback(frameDeadlineObject);
      }
    }

    // 自定義模擬requestIdleCallback
    rIC = function(
      callback: (deadline: Deadline) => void, // 傳入的任務處理函式引數
      options?: {timeout: number} // 其他引數
    ) {
      // 回撥函式
      scheduledRICCallback = callback;
      if (options != null && typeof options.timeout === 'number') {
        // 計算過期時間
        timeoutTime = now() + options.timeout;
      }
      if (!isAnimationFrameScheduled) {
        // 當前沒有排程別的幀回撥函式
        isAnimationFrameScheduled = true;
        // 初始開始執行幀回撥 
        requestAnimationFrame(animationTick);
      }
      return 0;
    };
    複製程式碼
    1. frameDeadline:是以啟發法,從30fps(即30幀)開始調整得到的更適於當前環境的一幀限制時間;
    2. timeRemaining:計算requestIdleCallback此次空閒(幀)執行任務剩餘時間,即距離deadline的時間;
    3. options.timeout:Fiber內部呼叫rICAPI執行非同步任務時,傳遞的任務到期時間引數;
    4. frameDeadlineObject:計算得到的某一幀可用時間物件,兩個屬性分別表示:
      1. didTimeout:傳入的非同步任務 處理函式是否超時;
      2. timeRemaining:當前幀可執行任務處理函式的剩餘空閒時間;
    5. 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物件作用於一個元件
export type Fiber = {|
  // 標記fiber型別tag.
  tag: TypeOfWork,
  // fiber對應的function/class/module型別元件名.
  type: any,
  // fiber所在元件樹的根元件FiberRoot物件
  stateNode: any,
  // 處理完當前fiber後返回的fiber,
  // 返回當前fiber所在fiber樹的父級fiber例項
  return: Fiber | null,
  // fiber樹結構相關連結
  child: Fiber | null,
  sibling: Fiber | null,
  index: number,

  // 當前處理過程中的元件props物件
  pendingProps: any, 
  // 快取的之前元件props物件
  memoizedProps: any, // The props used to create the output.
  // The state used to create the output
  memoizedState: any,

  // 元件狀態更新及對應回撥函式的儲存佇列
  updateQueue: UpdateQueue<any> | null,


  // 描述當前fiber例項及其子fiber樹的數位,
  // 如,AsyncUpdates特殊字表示預設以非同步形式處理子樹,
  // 一個fiber例項建立時,此屬性繼承自父級fiber,在建立時也可以修改值,
  // 但隨後將不可修改。
  internalContextTag: TypeOfInternalContext,

  // 更新任務的最晚執行時間
  expirationTime: ExpirationTime,

  // fiber的版本池,即記錄fiber更新過程,便於恢復
  alternate: Fiber | null,

  // Conceptual aliases
  // workInProgress : Fiber ->  alternate The alternate used for reuse happens
  // to be the same as work in progress.
|};
複製程式碼
  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(
  tag: TypeOfWork,
  key: null | string,
  internalContextTag: TypeOfInternalContext,
) {
  return new FiberNode(tag, key, internalContextTag);
};
複製程式碼

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

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

  workInProgress.expirationTime = expirationTime;
  workInProgress.pendingProps = pendingProps;
  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue;
  ...
  return workInProgress;
}
複製程式碼

Fiber型別

上一小節,Fiber物件中有個tag屬性,標記fiber型別,而fiber例項是和元件對應的,所以其型別基本上對應於元件型別,原始碼見ReactTypeOfWork模組

export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;

export const IndeterminateComponent = 0; // 尚不知是類元件還是函式式元件
export const FunctionalComponent = 1; // 函式式元件
export const ClassComponent = 2; // Class類元件
export const HostRoot = 3; // 元件樹根元件,可以巢狀
export const HostPortal = 4; // 子樹. Could be an entry point to a different renderer.
export const HostComponent = 5; // 標準元件,如地div, span等
export const HostText = 6; // 文字
export const CallComponent = 7; // 元件呼叫
export const CallHandlerPhase = 8; // 呼叫元件方法
export const ReturnComponent = 9; // placeholder(佔位符)
export const Fragment = 10; // 片段
複製程式碼

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

FiberRoot物件

FiberRoot物件,主要用來管理元件樹元件的更新程式,同時記錄元件樹掛載的DOM容器相關資訊,具體定義見ReactFiberRoot模組

export type FiberRoot = {
  // fiber節點的容器元素相關資訊,通常會直接傳入容器元素
  containerInfo: any,
  // 當前fiber樹中啟用狀態(正在處理)的fiber節點,
  current: Fiber,
  // 此節點剩餘的任務到期時間
  remainingExpirationTime: ExpirationTime,
  // 更新是否可以提交
  isReadyForCommit: boolean,
  // 準備好提交的已處理完成的work-in-progress
  finishedWork: Fiber | null,
  // 多元件樹FirberRoot物件以單連結串列儲存連結,指向下一個需要排程的FiberRoot
  nextScheduledRoot: FiberRoot | null,
};
複製程式碼

建立FiberRoot例項

import {
  ClassComponent,
  HostRoot
} from 'shared/ReactTypeOfWork';

// 建立返回一個初始根元件對應的fiber例項
function createHostRootFiber(): Fiber {
  // 建立fiber
  const fiber = createFiber(HostRoot, null, NoContext);
  return fiber;
}

export function createFiberRoot(
  containerInfo: any,
  hydrate: boolean,
) {
  // 建立初始根元件對應的fiber例項
  const uninitializedFiber = createHostRootFiber();
  // 元件樹根元件的FiberRoot物件
  const root = {
    // 根元件對應的fiber例項
    current: uninitializedFiber,
    containerInfo: containerInfo,
    pendingChildren: null,
    remainingExpirationTime: NoWork,
    isReadyForCommit: false,
    finishedWork: null,
    context: null,
    pendingContext: null,
    hydrate,
    nextScheduledRoot: null,
  };
  // 元件樹根元件fiber例項的stateNode指向FiberRoot物件
  uninitializedFiber.stateNode = root;
  return root;
}
複製程式碼

ReactChildFiber

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

// 調和(處理更新)子fibers
export const reconcileChildFibers = ChildReconciler(true);
// 掛載(初始化)子fibers
export const mountChildFibers = ChildReconciler(false);
複製程式碼

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

ChildReconciler方法,返回reconcileChildFibers方法:

  1. 判斷子級傳遞內容的資料型別,執行不同的處理,這也對應著我們寫React元件時傳遞props.children時,其型別可以是物件或陣列,字串,是數字等;
  2. 然後具體根據子元件型別,呼叫不同的具體調和處理函式;
  3. 最後返回根據子元件建立或更新得到的fiber例項;
function ChildReconciler(a) {
  function reconcileChildFibers(
    returnFiber: Fiber, currentFirstChild: Fiber | null,
    newChild: any, expirationTime: ExpirationTime,
  ) {
    // Handle object types
    const isObject = typeof newChild === 'object' && newChild !== null;

    if (isObject) {
      // 子元件例項型別,以Symbol符號表示的
      switch (newChild.$$typeof) {
        // React Element
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber, currentFirstChild,
              newChild, expirationTime
            )
          );
        // React元件呼叫
        case REACT_CALL_TYPE:
          return placeSingleChild(reconcileSingleCall(...));
        // placeholder
        case REACT_RETURN_TYPE:
          return ...;
        case REACT_PORTAL_TYPE:
          return ...;
      }
    }
    if (typeof newChild === 'string' || typeof newChild === 'number') {
      return placeSingleChild(reconcileSingleTextNode(...));
    }
    if (isArray(newChild)) {
      return reconcileChildrenArray(...);
    }
    if (getIteratorFn(newChild)) {
      return reconcileChildrenIterator(...);
    }
    ...   
  }
}
複製程式碼

Fiber架構

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

優先順序(ExpirationTime VS PriorityLevel)

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

ExpirationTime

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

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

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

export const NoWork = 0; // 沒有任務等待處理
export const Sync = 1; // 同步模式,立即處理任務
export const Never = 2147483647; // Max int32: Math.pow(2, 31) - 1
const UNIT_SIZE = 10; // 過期時間單元(ms)
const MAGIC_NUMBER_OFFSET = 2; // 到期時間偏移量

// 以ExpirationTime特定單位(1單位=10ms)表示的到期執行時間
// 1 unit of expiration time represents 10ms.
export function msToExpirationTime (ms) {
  // 總是增加一個偏移量,在ms<10時與Nowork模式進行區別
  return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET;
}
// 以毫秒錶示的到期執行時間
export function expirationTimeToMs(expirationTime: ExpirationTime) {
  return (expirationTime - MAGIC_NUMBER_OFFSET) * UNIT_SIZE;
}
// 向上取整(整數單位到期執行時間)
// precision範圍精度:彌補任務執行時間誤差
function ceiling(num, precision) {
  return (((num / precision) | 0) + 1) * precision;
}

// 計算處理誤差時間在內的到期時間
export function computeExpirationBucket(currentTime, expirationInMs, bucketSizeMs,) {
  return ceiling(
    currentTime + expirationInMs / UNIT_SIZE,
    bucketSizeMs / UNIT_SIZE
  );
}
複製程式碼

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

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

PriorityLevel

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

export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;

module.exports = {
  NoWork: 0, // No work is pending.
  SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.
  AnimationPriority: 2, // Needs to complete before the next frame.
  HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive.
  LowPriority: 4, // Data fetching, or result from updating stores.
  OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible.
};
複製程式碼

相對於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 () {
  ...
  return {
    computeAsyncExpiration,
    computeExpirationForFiber,
    scheduleWork,
    batchedUpdates,
    unbatchedUpdates,
    flushSync,
    deferredUpdates,
  };
}
複製程式碼

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

排程器與優先順序

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

import { 
  NoWork, Sync, Never, msToExpirationTime,
  expirationTimeToMs, computeExpirationBucket
} from './ReactFiberExpirationTime';

// 表示下一個要處理的任務的到期時間,預設為NoWork,即當前沒有正在等待執行的任務;
// Nowork預設更新策略:非同步模式下,非同步執行任務;同步模式下同步執行任務
let expirationContext = NoWork;
// 下一次渲染到期時間
let nextRenderExpirationTime = NoWork;
// 非同步更新
export const AsyncUpdates = 1;
// 初始時間(ms).
const startTime = now();
// ExpirationTime單位表示的當前時間(ExpirationTime單位,初始值傳入0)
let mostRecentCurrentTime = msToExpirationTime(0);

// 計算fiber的到期時間
function computeExpirationForFiber(fiber) {
  let expirationTime;

  if (isWorking) {
    if (isCommitting) {
      // 在提交階段的更新任務
      // 需要明確設定同步優先順序(Sync Priority)
      expirationTime = Sync;
    } else {
      // 在渲染階段發生的更新任務
      // 需要設定為下一次渲染時間的到期時間優先順序
      expirationTime = nextRenderExpirationTime;
    }
  } else {
    // 不在任務執行階段,需要計算新的過期時間

    // 明確傳遞useSyncScheduling為true表明期望同步呼叫
    // 且fiber.internalContextTag != AsyncUpdates
    if (useSyncScheduling && !(fiber.internalContextTag & AsyncUpdates)) {
      // 同步更新,設定為同步標記
      expirationTime = Sync;
    } else {
      // 非同步更新,計算非同步到期時間
      expirationTime = computeAsyncExpiration();
    }
  }
  return expirationTime;
}
複製程式碼
  1. 若當前處於任務提交階段(更新提交至DOM渲染)時,設定當前fiber到期時間為Sync,即同步執行模式;
  2. 若處於DOM渲染階段時,則需要延遲此fiber任務,將fiber到期時間設定為下一次DOM渲染到期時間;
  3. 若不在任務執行階段,則需重新設定fiber到期時間:
    1. 若明確設定useSyncSchedulingfiber.internalContextTag值不等於AsyncUpdates,則表明是同步模式,設定為Sync
    2. 否則,呼叫computeAsyncExpiration方法重新計算此fiber的到期時間;
// 重新計算當前時間(ExpirationTime單位表示)
function recalculateCurrentTime() {
  const ms = now() - startTime;
  // ExpirationTime單位表示的當前時間
  // 時間段值為 now() - startTime(起始時間)
  mostRecentCurrentTime = msToExpirationTime(ms);
  return mostRecentCurrentTime;
}

// 計算非同步任務的到期時間
function computeAsyncExpiration() {
  // 計算得到ExpirationTime單位的當前時間
  // 聚合相似的更新在一起
  // 更新應該在 ~1000ms,最多1200ms內完成
  const currentTime = recalculateCurrentTime();
  // 對於每個fiber的期望到期時間的增值,最大值為1000ms
  const expirationMs = 1000;
  // 到期時間的可接受誤差時間,200ms
  const bucketSizeMs = 200;
  // 返回包含誤差時間在內的到期時間
  return computeExpirationBucket(currentTime, expirationMs, bucketSizeMs);
}
複製程式碼

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

任務排程

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

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

主要排程邏輯實現在scheduleWork

  1. 通過fiber.return屬性,從當前fiber例項層層遍歷至元件樹根元件;
  2. 依次對每一個fiber例項進行到期時間判斷,若大於傳入的期望任務到期時間引數,則將其更新為傳入的任務到期時間;
  3. 呼叫requestWork方法開始處理任務,並傳入獲取的元件樹根元件FiberRoot物件和任務到期時間;
// 排程任務
// expirationTime為期望的任務到期時間
function scheduleWork(fiber, expirationTime: ExpirationTime) {
  return scheduleWorkImpl(fiber, expirationTime, false);
}

function scheduleWorkImpl(
  fiber, expirationTime
) {
  let node = fiber;
  while (node !== null) {
    // 向上遍歷至根元件fiber例項,並依次更新expirationTime到期時間
    if (
      node.expirationTime === NoWork ||
      node.expirationTime > expirationTime
    ) {
      // 若fiber例項到期時間大於期望的任務到期時間,則更新fiber到期時間
      node.expirationTime = expirationTime;
    }
    // 同時更新alternate fiber的到期時間
    if (node.alternate !== null) {
      if (
        node.alternate.expirationTime === NoWork ||
        node.alternate.expirationTime > expirationTime
      ) {
        // 若alternate fiber到期時間大於期望的任務到期時間,則更新fiber到期時間
        node.alternate.expirationTime = expirationTime;
      }
    }
    // node.return為空,說明到達元件樹頂部
    if (node.return === null) {
      if (node.tag === HostRoot) {
        // 確保是元件樹根元件並獲取FiberRoot例項
        const root = node.stateNode;
        // 請求處理任務
        requestWork(root, expirationTime);
      } else {
        return;
      }
    }
    // 獲取父級元件fiber例項
    node = node.return;
  }
}
複製程式碼

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

  1. 首先比較任務剩餘到期時間和期望的任務到期時間,若大於,則更新值;
  2. 判斷任務期望到期時間(expirationTime),區分同步或非同步執行任務;
// 當根節點發生更新時,排程器將呼叫requestWork方法開始任務處理過程
// It's up to the renderer to call renderRoot at some point in the future.
function requestWork(root: FiberRoot, expirationTime) {
  const remainingExpirationTime = root.remainingExpirationTime;
  if (remainingExpirationTime === NoWork ||
    expirationTime < remainingExpirationTime) {
    // 若任務剩餘到期時間大於期望的任務到期時間,則需要更新
    root.remainingExpirationTime = expirationTime;
  }

  if (expirationTime === Sync) {
    // 同步
    performWork(Sync, null);
  } else {
    // 非同步
    scheduleCallbackWithExpiration(expirationTime);
  }
}
複製程式碼

更新佇列(UpdateQueue)

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

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

Fiber更新佇列由ReactFiberUpdateQueue模組實現,主要涉及:

  1. 建立更新佇列;
  2. 新增更新至更新佇列;
  3. 新增更新至fiber(即fiber例項對應的更新佇列);
  4. 處理更新佇列中的更新並返回新狀態物件:
// 一個更新對應的資料結構
export type Update<State> = {
  expirationTime: ExpirationTime,
  partialState: PartialState<any, any>,
  callback: Callback | null,
  isReplace: boolean,
  isForced: boolean,
  next: Update<State> | null,
};

// 更新佇列,以單連結串列形式表示並持久化
// 排程一個更新任務時,將其新增至當前(current)fiber和work-in-progress fiber的更新佇列中;
// 這兩個更新佇列相互獨立但共享同一個持久化資料結構;
// work-in-progress更新佇列通常是current fiber更新佇列的子集;
// 發生調和時,更新任務從work-in-progress fiber更新佇列移除,
// current fiber內的更新任務則保留,當work-in-progress中斷時可以從current fiber恢復;
// 提交完更新時,work-in-progress fiber就會變成current fiber
export type UpdateQueue<State> = {
  // 若存在更早新增至佇列的更新未被處理,
  // 則此已處理的更新並不會從佇列中移除-先進先出原則
  // 所以需要維護baseState,代表第一個未處理的更新的基礎狀態,
  // 通常這就是佇列中的第一個更新,因為在佇列首部的已處理更新會被移除
  baseState: State,
  // 同理,需要維護最近的未處理的更新的到期時間,
  // 即未處理更新中到期時間值最小的
  expirationTime: ExpirationTime,
  first: Update<State> | null,
  last: Update<State> | null,
  callbackList: Array<Update<State>> | null,
  hasForceUpdate: boolean,
  isInitialized: boolean
};

// 新增更新至更新佇列
export function insertUpdateIntoQueue<State>(
  queue: UpdateQueue<State>,
  update: Update<State>
){
  // 新增更新至佇列尾部
  if (queue.last === null) {
    // 佇列為空
    queue.first = queue.last = update;
  } else {
    queue.last.next = update;
    queue.last = update;
  }
  if (
    queue.expirationTime === NoWork ||
    queue.expirationTime > update.expirationTime
  ) {
    // 更新最近到期時間
    queue.expirationTime = update.expirationTime;
  }
}
// 新增更新至fiber例項
export function insertUpdateIntoFiber<State>(
  fiber: Fiber,
  update: Update<State>,
) {
  // 可以建立兩個獨立的更新佇列
  // alternate主要用來儲存更新過程中各版本更新佇列,方便崩潰或衝突時回退
  const alternateFiber = fiber.alternate;
  let queue1 = fiber.updateQueue;
  if (queue1 === null) {
    // 更新佇列不存在,則建立一個空的更新佇列
    queue1 = fiber.updateQueue = createUpdateQueue((null));
  }

  let queue2;
  if (alternateFiber !== null) {
    // alternate fiber例項存在,則需要為此
    queue2 = alternateFiber.updateQueue;
    if (queue2 === null) {
      queue2 = alternateFiber.updateQueue = createUpdateQueue((null: any));
    }
  } else {
    queue2 = null;
  }
  queue2 = queue2 !== queue1 ? queue2 : null;

  // 如果只存在一個更新佇列
  if (queue2 === null) {
    insertUpdateIntoQueue(queue1, update);
    return;
  }

  // 如果任意更新佇列為空,則需要將更新新增至兩個更新佇列
  if (queue1.last === null || queue2.last === null) {
    insertUpdateIntoQueue(queue1, update);
    insertUpdateIntoQueue(queue2, update);
    return;
  }

  // 如果2個更新佇列均非空,則新增更新至第一個佇列,並更新另一個佇列的尾部更新項
  insertUpdateIntoQueue(queue1, update);
  queue2.last = update;
}

// 處理更新佇列任務,返回新狀態物件
export function processUpdateQueue<State>(
  current, workInProgress, queue, instance, props,
  renderExpirationTime,
) {
  if (current !== null && current.updateQueue === queue) {
    // 克隆current fiber以建立work-in-progress fiber
    const currentQueue = queue;
    queue = workInProgress.updateQueue = {
      baseState: currentQueue.baseState,
      expirationTime: currentQueue.expirationTime,
      first: currentQueue.first,
      last: currentQueue.last,
      isInitialized: currentQueue.isInitialized,
      // These fields are no longer valid because they were already committed. Reset them.
      callbackList: null,
      hasForceUpdate: false,
    };
  }

  // Reset the remaining expiration time. If we skip over any updates, we'll
  // increase this accordingly.
  queue.expirationTime = NoWork;

  let dontMutatePrevState = true;
  let update = queue.first;
  let didSkip = false;
  while (update !== null) {
    const updateExpirationTime = update.expirationTime;
    if (updateExpirationTime > renderExpirationTime) {
      // 此更新優先順序不夠,不處理,跳過
      if (queue.expirationTime === NoWork ||
          queue.expirationTime > updateExpirationTime
         ) {
        // 重新設定最近未處理更新的到期時間
        queue.expirationTime = updateExpirationTime;
      }
      update = update.next;
      continue;
    }

    // 優先順序足夠,處理
    let partialState;
    if (update.isReplace) {
      // 使用replaceState()直接替換狀態物件方式更新時
      // 獲取新狀態物件
      state = getStateFromUpdate(update, instance, state, props);
      // 不需要合併至之前狀態物件,標記為true
      dontMutatePrevState = true;
    } else {
      // 更新部分狀態方式
      // 獲取更新部分狀態時的狀態物件
      partialState = getStateFromUpdate(update, instance, state, props);
      if (partialState) {
        if (dontMutatePrevState) {
          // 上一次是替換狀態,所以不能影響state
          state = Object.assign({}, state, partialState);
        } else {
          // 更新部分狀態,直接將新狀態合併至上一次狀態
          state = Object.assign(state, partialState);
        }
        // 重置標記為false
        dontMutatePrevState = false;
      }
    }
    // 強制立即更新
    if (update.isForced) {
      queue.hasForceUpdate = true;
    }
    // 新增回撥函式
    if (update.callback !== null) {
      // Append to list of callbacks.
      let callbackList = queue.callbackList;
      if (callbackList === null) {
        callbackList = queue.callbackList = [];
      }
      callbackList.push(update);
    }
    // 遍歷下一個更新任務
    update = update.next;
  }
  // 返回最新的狀態物件
  return state;
}
複製程式碼

更新器(Updater)

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

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

  1. 找到元件例項對應的fiber例項;
  2. 詢問排程器當前元件fiber例項的優先順序;
  3. 將更新推入fiber的更新佇列;
  4. 根據優先順序排程更新任務;

更新器實現見ReactFiberClassComponent模組

export default function(
  scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
  computeExpirationForFiber: (fiber: Fiber) => ExpirationTime,
  memoizeProps: (workInProgress: Fiber, props: any) => void,
  memoizeState: (workInProgress: Fiber, state: any) => void,
) {
  // Class component state updater
  const updater = {
    isMounted,
    // 狀態變更,更新入佇列
    enqueueSetState(instance, partialState, callback) {
      // 獲取fiber
      const fiber = ReactInstanceMap.get(instance);
      const expirationTime = computeExpirationForFiber(fiber);
      // 建立更新任務
      const update = {
        expirationTime,
        partialState,
        callback,
        isReplace: false,
        isForced: false,
        nextCallback: null,
        next: null,
      };
      // 新增更新任務至fiber
      insertUpdateIntoFiber(fiber, update);
      // 呼叫排程器API以排程fiber任務
      scheduleWork(fiber, expirationTime);
    },
    // 替換狀態時
    enqueueReplaceState(instance, state, callback) {
      const fiber = ReactInstanceMap.get(instance);
      const expirationTime = computeExpirationForFiber(fiber);
      const update = {
        expirationTime,
        partialState: state,
        callback,
        isReplace: true,
        isForced: false,
        nextCallback: null,
        next: null,
      };
      // 新增更新任務至fiber
      insertUpdateIntoFiber(fiber, update);
      scheduleWork(fiber, expirationTime);
    },
    // 強制更新
    enqueueForceUpdate(instance, callback) {
      const fiber = ReactInstanceMap.get(instance);
      const expirationTime = computeExpirationForFiber(fiber);
      const update = {
        expirationTime,
        partialState: null,
        callback,
        isReplace: false,
        isForced: true,
        nextCallback: null,
        next: null,
      };
      insertUpdateIntoFiber(fiber, update);
      scheduleWork(fiber, expirationTime);
    },
  };
  
  // 呼叫元件例項生命週期方法並呼叫更新器API
  function callComponentWillReceiveProps(
    workInProgress, instance, newProps, newContext
  ) {
    const oldState = instance.state;
    instance.componentWillReceiveProps(newProps, newContext);

    if (instance.state !== oldState) {
      // 呼叫更新器入佇列方法
      updater.enqueueReplaceState(instance, instance.state, null);
    }
  }

  // 設定Class元件例項的更新器和fiber
  function adoptClassInstance(workInProgress, instance): {
    // 設定更新器
    instance.updater = updater;
    workInProgress.stateNode = instance;
    // 設定fiber
    ReactInstanceMap.set(instance, workInProgress);
  }

  // 例項化Class元件例項
  function constructClassInstance(workInProgress, props) {
    const ctor = workInProgress.type;
    const unmaskedContext = getUnmaskedContext(workInProgress);
    const needsContext = isContextConsumer(workInProgress);
    const context = needsContext
    ? getMaskedContext(workInProgress, unmaskedContext)
    : emptyObject;
    // 例項化元件型別
    const instance = new ctor(props, context);
    // 設定Class例項的更新器和fiber
    adoptClassInstance(workInProgress, instance);

    return instance;
  }
  
  // 掛載元件例項
  function mountClassInstance(
    workInProgress, renderExpirationTime) {
    if (typeof instance.componentWillMount === 'function') {
      callComponentWillMount(workInProgress, instance);
    }
  }
  
 // 更新元件例項 
  function updateClassInstance(
    current, workInProgress, renderExpirationTime
  ) {
    // 元件例項
    const instance = workInProgress.stateNode;
    // 原Props或新Props
    const oldProps = workInProgress.memoizedProps;
    let newProps = workInProgress.pendingProps;
    if (!newProps) {
      // 沒有新Props則直接使用原Props
      newProps = oldProps;
    }
    
    if (typeof instance.componentWillReceiveProps === 'function' &&
      (oldProps !== newProps)) {
      // 呼叫方法進行更新器相關處理
      callComponentWillReceiveProps(
        workInProgress, instance, newProps
      );
    }

    // 根據原狀態物件和更新佇列計算得到新狀態物件
    const oldState = workInProgress.memoizedState;
    let newState;
    if (workInProgress.updateQueue !== null) {
      // 處理更新佇列更新,計算得到新State物件
      newState = processUpdateQueue(
        current,
        workInProgress,
        workInProgress.updateQueue,
        instance,
        newProps,
        renderExpirationTime,
      );
    } else {
      newState = oldState;
    }

    // 檢查是否需要更新元件
    const shouldUpdate = checkShouldComponentUpdate(...);

    if (shouldUpdate) {
      if (typeof instance.componentWillUpdate === 'function') {      
        instance.componentWillUpdate(newProps, newState, newContext);      
      }
    }
    // 呼叫生命週期方法
    ...
    return shouldUpdate;
  }
  
  return {
    adoptClassInstance,
    constructClassInstance,
    mountClassInstance,
    updateClassInstance
  };
}
複製程式碼

主要實現以下幾個功能:

  1. 初始化元件例項併為其設定fibre例項和更新器;

  2. 初始化或更新元件例項,根據更新佇列計算得到新狀態等;

  3. 呼叫元件例項生命週期方法,並且呼叫更新器API更新fiber例項等,如更新元件例項呼叫的callComponentWillReceiveProps方法,該方法呼叫元件例項的componentWillReceiveProps生命週期方法,並呼叫更新器updater.enqueueReplaceState方法,更新fiber例項,並將更新新增至更新佇列:

    // 呼叫元件例項生命週期方法並呼叫更新器API
    function callComponentWillReceiveProps(
    workInProgress, instance, newProps, newContext
    ) {
      const oldState = instance.state;
      instance.componentWillReceiveProps(newProps, newContext);

      if (instance.state !== oldState) {
        // 呼叫更新器入佇列方法
        updater.enqueueReplaceState(instance, instance.state, null);
      }
    }
    複製程式碼

另外需要重點關注的是insertUpdateIntoFiber方法,該方法實現將更新任務新增至元件fiber例項,內部會處理將任務新增至fiber更新佇列,原始碼見上文更新佇列中介紹的ReactFiberUpdateQueue模組,最終還是呼叫insertUpdateIntoQueue

獲取fiber例項

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

export function get(key) {
  return key._reactInternalFiber;
}
export function set(key, value) {
  key._reactInternalFiber = value;
}
複製程式碼

使用節點上的_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 {
      // 當前最新版本fiber例項使用fiber的alternate屬性獲取
      const current = workInProgress.alternate;
      // 開始處理,返回子元件fiber例項
      let next = beginWork(current, workInProgress, nextRenderExpirationTime);
      if (next === null) {
        // 不存在子級fiber,完成單元任務的處理,之後繼續處理下一個任務
        next = completeUnitOfWork(workInProgress);
      }
      return next;
    }
    複製程式碼

  2. beginWork返回傳入fiber例項的子元件fiber例項,,若為空,則代表此元件樹任務處理完成,否則會在workLoop 方法內迭代呼叫performUnitOfWork方法處理:

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

  3. beginWork方法內根據元件型別呼叫不同方法,這些方法內呼叫更新器API將更新新增至更新佇列,具體實現見ReactFiberBeginWork模組:

    // 引入更新器模組
    import ReactFiberClassComponent from './ReactFiberClassComponent';

    export default function(
      config, hostContext, hydrationContext,
      scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
      computeExpirationForFiber: (fiber: Fiber) => ExpirationTime,
    ) {
      // 初始化更新器模組,獲取API
      const {
        adoptClassInstance, constructClassInstance,
        mountClassInstance, updateClassInstance
      } = ReactFiberClassComponent(
        scheduleWork, computeExpirationForFiber,
        memoizeProps, memoizeState
      );
      
      // beginWork,開始任務處理
      function beginWork(
        current, workInProgress, renderExpirationTime
      ) {
        switch (workInProgress.tag) {
          // 對應不同型別fiber,執行不同處理邏輯
          case IndeterminateComponent:
            ...
          case FunctionalComponent:
            return updateFunctionalComponent(current, workInProgress);
          case ClassComponent:
            // 更新類元件,返回子級fiber例項
            return updateClassComponent(
              current, workInProgress, renderExpirationTime
            );
          case HostRoot:
            return updateHostRoot(current, workInProgress, renderExpirationTime);
          case HostComponent:
            ...
          case HostText:
            return updateHostText(current, workInProgress);
          case CallHandlerPhase:
            // This is a restart. Reset the tag to the initial phase.
            workInProgress.tag = CallComponent;
          case CallComponent:
            ...
          case ReturnComponent:
            // A return component is just a placeholder, we can just run through the
            // next one immediately.
            return null;
          case HostPortal:
            ...
          case Fragment:
            return updateFragment(current, workInProgress);
          default:;
        }
      }
      
      return {
        beginWork,
        beginFailedWork
      };
    }
    複製程式碼
    1. 引入ReactFiberClassComponent更新器相關模組並初始化獲得API;

    2. beginWork方法內根據傳入的work-in-progress的fiber型別(tag)呼叫不同邏輯處理;

    3. 在邏輯處理裡面會呼叫更新期API,將更新新增至更新佇列;

    4. ClassComponent為例,將呼叫updateClassComponent方法:

      1. 判斷若第一次則初始化並掛載元件例項,否則呼叫updateClassInstance方法更新元件例項;

      2. 最後呼叫finishClassComponent方法,調和處理其子元件並返回其子級fiber例項;

        // 更新類元件
        function updateClassComponent(
          current, workInProgress, renderExpirationTime
        ) {
          let shouldUpdate;
          if (current === null) {
            if (!workInProgress.stateNode) {
              // fiber沒有元件例項時需要初始化元件例項
              constructClassInstance(workInProgress, workInProgress.pendingProps);
              // 掛載元件例項
              mountClassInstance(workInProgress, renderExpirationTime);
              // 預設需要更新
              shouldUpdate = true;
            }
          } else {
            // 處理例項更新並返回是否需要更新元件
            shouldUpdate = updateClassInstance(
              current,
              workInProgress,
              renderExpirationTime,
            );
          }
          // 更新完成後,返回子元件fiber例項
          return finishClassComponent(
            current, workInProgress, shouldUpdate, hasContext
          );
        }

        // 類元件更新完成
        function finishClassComponent(
          current, workInProgress, shouldUpdate, hasContext
        ) {
          if (!shouldUpdate) {
            // 明確設定不需要更新時,不處理更新,
            // 如shouldCOmponentUpdate方法return false
            return bailoutOnAlreadyFinishedWork(current, workInProgress);
          }

          const instance = workInProgress.stateNode;
          // 重新渲染
          ReactCurrentOwner.current = workInProgress;
          // 返回元件子元件樹等內容
          let nextChildren = instance.render();
          // 調和子元件樹,將迭代處理每一個元件
          // 函式內將呼叫ReactChildFiber模組提供的API
          reconcileChildren(current, workInProgress, nextChildren);
          // 返回子元件fiber例項
          return workInProgress.child;
        }
        複製程式碼

排程更新任務

上一節更新器已經能按照優先順序將更新新增至更新佇列,那麼如何排程執行更新任務呢?

在更新器實現ReactFiberClassComponent模組中,在enqueueSetStateenqueueReplaceStateenqueueForceUpdate入佇列方法中,均會呼叫如下方法:

insertUpdateIntoFiber(fiber, update);
scheduleWork(fiber, expirationTime);
複製程式碼
  1. insertUpdateIntoFiber:將更新新增至fiber例項,最終會新增至更新佇列;
  2. scheduleWork:排程任務,傳入fiber例項和任務到期時間;

渲染與調和

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

原始碼簡單分析

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

react-dom渲染模組

在專案中,如果要將應用渲染至頁面,通常會有如下程式碼:

import ReactDOM from 'react-dom';
import App form './App'; // 應用根元件

ReactDOM.render(
  <App>,
  document.querySelector('#App') // 應用掛載容器DOM
);
複製程式碼

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

const ReactDOM = {
  render(
    element: React$Element<any>, // React元素,通常是專案根元件
    container: DOMContainer, // React應用掛載的DOM容器
    callback: ?Function,  // 回撥函式
  ) {
    return renderSubtreeIntoContainer(
      null,
      element,
      container,
      false,
      callback,
    );
  }
};
複製程式碼

常用的渲染元件至DOM的render方法如上,呼叫renderSubtreeIntoContainer方法,渲染元件的子元件樹:

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

DOMRenderer是呼叫調和演算法返回的DOM渲染器物件,在此處會傳入渲染模組的渲染UI操作API,如:

// 呼叫調和演算法方法
const DOMRenderer = ReactFiberReconciler(
  // 傳遞至調和演算法中的渲染UI(react-dom模組即DOM)
  // 實際操作API
  {
  getPublicInstance(instance) {
    return instance;
  },
  createInstance(
    type: string,
    props: Props,
    rootContainerInstance: Container,
    hostContext: HostContext,
    internalInstanceHandle: Object,
  ) {
    // 建立DOM元素
    const domElement = createElement(
      type,
      props,
      rootContainerInstance,
      parentNamespace,
    );
    precacheFiberNode(internalInstanceHandle, domElement);
    updateFiberProps(domElement, props);
    return domElement;      
  },
  now: ReactDOMFrameScheduling.now,
  mutation: {
    // 提交渲染
    commitMount(
      domElement: Instance,
      type: string,
      newProps: Props,
      internalInstanceHandle: Object,
    ) {
      ((domElement: any):
        | HTMLButtonElement
        | HTMLInputElement
        | HTMLSelectElement
        | HTMLTextAreaElement).focus();
    },
  // 提交更新
    commitUpdate(
      domElement: Instance,
      updatePayload: Array<mixed>,
      type: string,
      oldProps: Props,
      newProps: Props,
      internalInstanceHandle: Object,
    ) {
      // 更新屬性
      updateFiberProps(domElement, newProps);
      // 對DOM節點進行Diff演算法分析
      updateProperties(domElement, updatePayload, type, oldProps, newProps);
    },
     // 清空文字內容
    resetTextContent(domElement: Instance): void {
      domElement.textContent = '';
    },
    // 新增為子級
    appendChild(
      parentInstance: Instance,
      child: Instance | TextInstance,
    ): void {
      parentInstance.appendChild(child);
    }
    ...
  }
});
複製程式碼

ReactDOMFrameScheduling.now原始碼見Github

在任務完成時將執行createInstance方法,然後呼叫createElement建立DOM元素並新增至文件。

調和演算法入口

調和演算法入口:

import ReactFiberScheduler from './ReactFiberScheduler';
import {insertUpdateIntoFiber} from './ReactFiberUpdateQueue';

export default function Reconciler(
  // all parameters as config object
  // 下文用到的config引數即從此處傳入
  getPublicInstance,
  createInstance,
  ...
) {
  // 生成排程器API
  var {
    computeAsyncExpiration, computeExpirationForFiber, scheduleWork,
    batchedUpdates, unbatchedUpdates, flushSync, deferredUpdates,
  } = ReactFiberScheduler(config);

  return {
    // 建立容器
    createContainer(containerInfo, hydrate: boolean) {
      // 建立根fiber例項
      return createFiberRoot(containerInfo, hydrate);
    },
    // 更新容器內容
    updateContainer(
      element: ReactNodeList,
      container: OpaqueRoot,
      parentComponent: ?React$Component<any, any>,
      callback: ?Function,
    ): void {
      const current = container.current;
      ...
      // 更新
      scheduleTopLevelUpdate(current, element, callback);
    },
    ...
    // 獲取容器fiber樹的根fiber例項
    getPublicRootInstance (container) {
      // 獲取fiber例項
      const containerFiber = container.current;
      if (!containerFiber.child) {
        return null;
      }
      switch (containerFiber.child.tag) {
        case HostComponent:
          return getPublicInstance(containerFiber.child.stateNode);
        default:
          return containerFiber.child.stateNode;
      }
    },
  unbatchedUpdates
  }
}
複製程式碼

react-dom渲染模組呼叫createContainer建立容器和根fiber例項,FiberRoot物件,呼叫updateContainer方法更新容器內容。

開始更新
// 更新
function scheduleTopLevelUpdate(
    current: Fiber,
    element: ReactNodeList,
    callback: ?Function,
  ) {
  callback = callback === undefined ? null : callback;
  const update = {
    expirationTime,
    partialState: {element},
    callback,
    isReplace: false,
    isForced: false,
    nextCallback: null,
    next: null,
  };
  // 更新fiber例項
  insertUpdateIntoFiber(current, update);
  // 執行任務
  scheduleWork(current, expirationTime);
}
複製程式碼
處理更新

呼叫scheduleWork方法處理更新任務,實現見上文,原始碼

提交更新

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

import ReactFiberCommitWork from './ReactFiberCommitWork';

const {
    commitResetTextContent,
    commitPlacement,
    commitDeletion,
    commitWork,
    commitLifeCycles,
    commitAttachRef,
    commitDetachRef,
  } = ReactFiberCommitWork(config, captureError);

function commitRoot(finishedWork) {
  ...
  commitAllHostEffects();
}
// 迴圈執行提交更新
function commitAllHostEffects() {
  while (nextEffect !== null) {
    let primaryEffectTag =
        effectTag & ~(Callback | Err | ContentReset | Ref | PerformedWork);
      switch (primaryEffectTag) {
        case Placement: {
          commitPlacement(nextEffect);
          nextEffect.effectTag &= ~Placement;
          break;
        }
        case PlacementAndUpdate: {
          // Placement
          commitPlacement(nextEffect);
          nextEffect.effectTag &= ~Placement;
          // Update
          const current = nextEffect.alternate;
          commitWork(current, nextEffect);
          break;
        }
        case Update: {
          const current = nextEffect.alternate;
          commitWork(current, nextEffect);
          break;
        }
        case Deletion: {
          isUnmounting = true;
          commitDeletion(nextEffect);
          isUnmounting = false;
          break;
        }
      }
      nextEffect = nextEffect.nextEffect;
  }
}
// Flush sync work.
let finishedWork = root.finishedWork;
if (finishedWork !== null) {
  // This root is already complete. We can commit it.
  root.finishedWork = null;
  root.remainingExpirationTime = commitRoot(finishedWork);
}
複製程式碼

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

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

參考

  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

相關文章