前言
React 的Fiber reconciler演算法分為2個階段(phase):
本文只對Fiber的數據結構做討論
名詞解釋:
一、初識Fiber
Fiber是一種低級別的抽象,Fiber首要目標是實現react的合理調度scheduling,特別是要做到:
要實現work,首先需要把work分解成各個小任務單元,一個work的任務單元就是一個Fiber,在執行work時,每次執行work中的一個小任務單元,做完看是否還有時間繼續下一個任務,有的話繼續,沒有的話把自己掛起,主線程不忙的時候再繼續。為了便於理解Fiber,要明白渲染一個react應用就類似於調用一個函數,該函數的主體包含對其他函數的引用。計算機通常控制程序執行的方式是使用堆棧,在執行函數時,將向堆棧添加新的堆棧幀,該堆棧幀表示該函數將要執行的work(一般會是函數地址)。
在執行函數渲染UI時,如果同一時間太多的work被執行,會導致動畫掉幀,看起來不連貫。更重要的是,可能會出現重複性的更新,而這其中的一些更新可能是不必要的。較新的瀏覽器(和react native)中實現了有助於解決這些問題的API:
但另外的問題來了,為了使用這些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
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
4、pendingProps和memoizedProps
從概念上講,props是函數組件的參數,Fiber的pendingProps是在函數初始化時設置的props,memoizedProps是函數執行結束時props的值。當剛進入函數時pendingProps 值等於memoizedProps值,表示Fiber之前的輸出可以重用,從而避免一些不必要的work。
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
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等下一篇繼續探討,有說明錯誤或需要探討等歡迎指出!