前言

React 的Fiber reconciler演算法分為2個階段(phase):

  1. (可中斷)render/reconciliation 通過構造workInProgress 得出change
  2. (不可中斷)commit 應用這些DOM change

本文只對Fiber的數據結構做討論

名詞解釋:

  1. scheduling:確定work執行過程
  2. work:必須執行的工作,通常是update,如setState
  3. work-in-progress:尚未完成的Fiber,也就是尚未返回的堆棧幀,對象workInProgress是reconcile過程中從Fiber建立的當前進度快照,用於斷點恢復

一、初識Fiber

Fiber是一種低級別的抽象,Fiber首要目標是實現react的合理調度scheduling,特別是要做到:

  1. 可以暫停work,並能在之後繼續開啟
  2. 針對不同的work,設置不同的優先順序
  3. 可以重用之前已完成的work
  4. 丟棄不再需要使用的work

要實現work,首先需要把work分解成各個小任務單元,一個work的任務單元就是一個Fiber,在執行work時,每次執行work中的一個小任務單元,做完看是否還有時間繼續下一個任務,有的話繼續,沒有的話把自己掛起,主線程不忙的時候再繼續。為了便於理解Fiber,要明白渲染一個react應用就類似於調用一個函數,該函數的主體包含對其他函數的引用。計算機通常控制程序執行的方式是使用堆棧,在執行函數時,將向堆棧添加新的堆棧幀,該堆棧幀表示該函數將要執行的work(一般會是函數地址)。

在執行函數渲染UI時,如果同一時間太多的work被執行,會導致動畫掉幀,看起來不連貫。更重要的是,可能會出現重複性的更新,而這其中的一些更新可能是不必要的。較新的瀏覽器(和react native)中實現了有助於解決這些問題的API:

  1. RequestIdleCallback實現了在空閑期間調用低優先順序函數
  2. RequestAnimationFrame實現了在下一個動畫幀上調用高優先順序函數。

但另外的問題來了,為了使用這些API,你需要一種方法將渲染UI的work分解為多個單元。如果只把這些單元放在堆棧中,則程序將一直執行堆棧中的work單元,直到堆棧為空。設想,如果我們能夠自定義堆棧的調用方式來優化UI的渲染,做到能夠隨時中斷堆棧的調用,並可以手動的恢復調用,這不是很好嗎?而這也就是Fiber的目的。

可以認為Fiber是堆棧的另一種實現方式,專門用於react組件,單個Fiber就是一個虛擬的堆棧幀。重新實現堆棧的好處是,你可以將堆棧幀保存在內存中,並在需要時執行它們。這對於實現work的執行過程(scheduling)至關重要。除了scheduling之外,手動處理堆棧幀還可以解決並發性和錯誤邊界等問題。

二、Fiber主要結構

一個Fiber是一個包含組件及其輸入、輸出信息的JS對象,一個Fiber對應一個堆棧幀,也可以對應一個組件實例,下面列出Fiber(即代碼中的FiberNode)中的幾個重要屬性:

function FiberNode(tag, pendingProps, key, mode) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;

// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;

this.ref = null;

this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.contextDependencies = null;

this.mode = mode;

// Effects
this.effectTag = NoEffect;
this.nextEffect = null;

this.firstEffect = null;
this.lastEffect = null;

this.expirationTime = NoWork;
this.childExpirationTime = NoWork;

this.alternate = null;

if (enableProfilerTimer) {
this.actualDuration = Number.NaN;
this.actualStartTime = Number.NaN;
this.selfBaseDuration = Number.NaN;
this.treeBaseDuration = Number.NaN;

this.actualDuration = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
}

// ...
}

1、type和key

Fiber的key和type對於React Element來說,用途相同。(實際上,當從element創建Fiber時,type和key被直接賦值了)。

type:描述對應的組件。對於自定義組件,type是組件本身,類型是函數或類;對於默認元素,type可能是div、span、img等,類型是String。從概念上講,type是由堆棧幀指向的函數

自定義組件
默認元素

key:是在reconciliation階段判斷Fiber是否可以重用,即JSX中設置的key值

2、child和sibling

child:Fiber對象,對應的是組件render方法返回的值,下面是BaseInput組件的child

BaseInput FiberNode child
BaseInput.tsx render return

sibling: Fiber對象,當父組件render函數返回多個children時,child為其第一個子組件,其他子組件則為sibling,如下所示:form的child為第一個BaseInput(name=「useName」),第一個BaseInput的sibling為第二個BaseInput(name=「password」)

3、return

return也是Fiber類型,以上圖為例,是程序處理完當前Fiber(BaseInput)而要返回的Fiber(form)。在概念上,他與堆棧的返回地址含義相同,也可以認為是其parent fiber,如果一個parent fiber有好幾個child(如form),則其所有child(BaseInput)的return都是parent fiber

BaseInput FiberNode

4、pendingProps和memoizedProps

從概念上講,props是函數組件的參數,Fiber的pendingProps是在函數初始化時設置的props,memoizedProps是函數執行結束時props的值。當剛進入函數時pendingProps 值等於memoizedProps值,表示Fiber之前的輸出可以重用,從而避免一些不必要的work。

BaseInput的pendingProps
BaseInput的memoizedProps

5、alternate

在任何時候,一個組件實例最多有兩個與之對應的Fiber對象:當前即將渲染的(current fiber)和workInProgress fiber,diff產生出的變化會標記在workInProgress fiber上。

current fiber的alternate是workInProgress fiber,workInProgress fiber的alternate是current fiber,有點繞,看下面的代碼片段好理解些,workInProgress構造完畢,得到了新的fiber,然後把current指針指向workInProgress,丟掉舊的fiber。Fiber的alternate是一個叫cloneFiber的函數惰性的創建的,與總是創建一個新對象不同,cloneFiber將嘗試重用Fiber的alternate(如果存在的話),以實現最小化內存分配。

function createWorkInProgress(current, pendingProps, expirationTime) {
var workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
console.log("workInProgress", workInProgress)
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;

// ...

workInProgress.alternate = current;
current.alternate = workInProgress;
}

// ...

return workInProgress;
}

6、expirationTime

過期時間,與任務單元的優先順序相關,根據expirationTime來判斷是否進行下一個分片任務,過期時間決定了更新的批處理方式,所以我們希望在同一瀏覽器事件中發生的具有相同優先順序的所有更新都接受相同的過期時間。代碼中先通過computeExpirationForFiber函數根據不同的階段計算expirationTime,再使用scheduleWork函數更新任務隊列,可能的初始值如下:

var NoWork = 0; // 沒有任務等待處理
var Never = 1; // 暫不執行,優先順序最低
var Sync = maxSigned31BitInt; // 同步模式,立即處理,優先順序最高

computeExpirationForFiber函數入參currentTime,fiber

  1. currentTime是requestCurrentTime函數的返回值,currentTime計算是基於起始時間(scheduler.unstable_now() -originalStartTimeMs)計算的。react追蹤兩個單獨的時間,"renderer" time(currentRendererTime)和"scheduler" time(currentSchedulerTime)。"renderer" time可以隨時更新,目前他只是為了降低調用性能;"scheduler" time只有在沒有未完成的工作或者確定工作不在瀏覽器事件執行中才更新(即nextFlushedExpirationTime為NoWork或Never)。也就是說,當正在渲染或已有待處理的工作時,requestCurrentTime直接返回currentSchedulerTime,否則如果nextFlushedExpirationTime為NoWork或Never,則更新currentRendererTime和currentSchedulerTime。
  2. fiber:需要處理的fiber node

computeExpirationForFiber執行過程如下:

function computeExpirationForFiber(currentTime, fiber) {
// 獲取當前優先順序 currentPriorityLevel 緩存
var priorityLevel = scheduler.unstable_getCurrentPriorityLevel();

var expirationTime = void 0;
if ((fiber.mode & ConcurrentMode) === NoContext) {
// 非同步模式之外的,優先順序設置為同步模式
expirationTime = Sync;
} else if (isWorking && !isCommitting$1) {
// 在render階段,優先順序設置為下次渲染的到期時間
expirationTime = nextRenderExpirationTime;
} else {
// 在commit階段,根據priorityLevel進行expirationTime更新
switch (priorityLevel) {
case scheduler.unstable_ImmediatePriority:
// 立即執行的任務
expirationTime = Sync;
break;
case scheduler.unstable_UserBlockingPriority:
// 因用戶交互阻塞的優先順序
expirationTime = computeInteractiveExpiration(currentTime);
break;
case scheduler.unstable_NormalPriority:
// 一般優先順序,非同步更新
expirationTime = computeAsyncExpiration(currentTime);
break;
case scheduler.unstable_LowPriority:
case scheduler.unstable_IdlePriority:
// 低優先順序或空閑狀態
expirationTime = Never;
break;
default:
invariant(false, Unknown priority level. This error is likely caused by a bug in React. Please file an issue.);
}

// 避免在渲染樹的時候同時去更新已經渲染的樹
if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
expirationTime -= 1;
}
}

// 記錄下掛起的用戶交互任務中expirationTime最短的一個,在需要時同步刷新所有互動式更新
if (priorityLevel === scheduler.unstable_UserBlockingPriority
&& (lowestPriorityPendingInteractiveExpirationTime === NoWork
|| expirationTime < lowestPriorityPendingInteractiveExpirationTime)) {
lowestPriorityPendingInteractiveExpirationTime = expirationTime;
}

return expirationTime;
}

值得注意的是computeInteractiveExpiration和computeAsyncExpiration都調用了computeExpirationBucket,只是傳入不一樣參數以區分優先順序高低(Interactive優先順序高於Async),調用如下:

// 返回距離num最近的precision的倍數
function ceiling(num, precision) {
return ((num / precision | 0) + 1) * precision;
}

function computeExpirationBucket(currentTime, expirationInMs, bucketSizeMs) {
return MAGIC_NUMBER_OFFSET - ceiling(MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE, bucketSizeMs / UNIT_SIZE);
}

var LOW_PRIORITY_EXPIRATION = 5000;
var LOW_PRIORITY_BATCH_SIZE = 250;

function computeAsyncExpiration(currentTime) {
return computeExpirationBucket(currentTime, LOW_PRIORITY_EXPIRATION, LOW_PRIORITY_BATCH_SIZE);
}

var HIGH_PRIORITY_EXPIRATION = 500;
var HIGH_PRIORITY_BATCH_SIZE = 100;

function computeInteractiveExpiration(currentTime) {
return computeExpirationBucket(currentTime, HIGH_PRIORITY_EXPIRATION, HIGH_PRIORITY_BATCH_SIZE);
}

7、tag

用於標記組件類型,具體分類取值如下:

var FunctionComponent = 0;
var ClassComponent = 1;
var IndeterminateComponent = 2; // Before we know whether it is function or class
var HostRoot = 3; // Root of a host tree. Could be nested inside another node.
var HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
var HostComponent = 5;
var HostText = 6;
var Fragment = 7;
var Mode = 8;
var ContextConsumer = 9;
var ContextProvider = 10;
var ForwardRef = 11;
var Profiler = 12;
var SuspenseComponent = 13;
var MemoComponent = 14;
var SimpleMemoComponent = 15;
var LazyComponent = 16;
var IncompleteClassComponent = 17;
var DehydratedSuspenseComponent = 18;

8、mode

mode在創建時進行設置,在創建之後,mode在Fiber的整個生命週期內保持不變,可能的取值:

var NoContext = 0; // 同步模式
var ConcurrentMode = 1; // 非同步模式
var StrictMode = 2; // 嚴格模式,一般用於開發中,詳見https://reactjs.org/docs/strict-mode.html#identifying-unsafe-lifecycles
var ProfileMode = 4; // 分析模式,一般用於開發中

上面介紹了fiber的部分屬性,篇幅問題,其他一些屬性如Effects相關、updateQueue等下一篇繼續探討,有說明錯誤或需要探討等歡迎指出!


推薦閱讀:
相關文章