[React] React Fiber - 2 - 深入 React 原始碼
此篇原始碼解說文主要以剛出現 Fiber 架構的 React v16.0 為主,因為相對完整和簡單,後續的版本差異請自行參考官方原始碼
Fiber tree
就像 HTML DOM 是 tree data structure,React 的 Virtual DOM 也是 tree data structure,React
但在內部,React 會有兩個 Virtual DOM tree(或稱 Fiber tree),一個是 current tree,另一個是 work in progress tree
- Current tree:目前正在瀏覽器渲染的 Fiber tree
- Work in progress tree:正在計算更新的 Fiber tree
而且 React 內部會有一個 pointer,指向現在要渲染出來的 Fiber tree 是哪一個,當 Work In Progress Tree 計算完成後,會取代 Current Tree,成為新的 Current Tree,示意動畫如下:
Work Loop
因為狀態更新而開始計算 Work In Progress Tree 的過程中,React 會類似 Recursion 的方式,處理 Fiber nodes
- 當從 root fiber node 開始計算時,會呼叫
beginWork(v16 Line 710) 函式,生成或更新現在的 fiber node - 當計算到 leaf fiber node 時,會呼叫
completeWork(v16 Line 618) 函式回到 parent fiber node
Fiber node 的結構
那在遍歷每個 Fiber node 的過程中,究竟處理了什麼呢? 這就要來看看 Fiber node 的結構了
Fiber node 的結構如下 (v16 ReactFiber.js Line 64):
export type Fiber = {
// These first fields are conceptually members of an Instance. This used to
// be split into a separate type and intersected with the other Fiber fields,
// but until Flow fixes its intersection bugs, we've merged them into a
// single type.
// An Instance is shared between all versions of a component. We can easily
// break this out into a separate object to avoid copying so much to the
// alternate versions of the tree. We put this on a single object for now to
// minimize the number of objects created during the initial render.
// Tag identifying the type of fiber.
tag: TypeOfWork,
// Unique identifier of this child.
key: null | string,
// The function/class/module associated with this fiber.
type: any,
return: Fiber | null,
// Singly Linked List Tree Structure.
child: Fiber | null,
sibling: Fiber | null,
index: number,
// Input is the data coming into process this fiber. Arguments. Props.
pendingProps: any, // This type will be more specific once we overload the tag.
memoizedProps: any, // The props used to create the output.
// A queue of state updates and callbacks.
updateQueue: UpdateQueue | null,
// The state used to create the output
memoizedState: any,
// This will be used to quickly determine if a subtree has no pending changes.
pendingWorkPriority: PriorityLevel,
// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
alternate: Fiber | null,
...
};
tag
tag 是給 React 內部辨識用的,用於區分此 Fiber node 要怎麼處理,像是 FunctionComponent、ClassComponent、HostComponent 等等,
在 ReactTypeOfWork.js file 中定義了所有可能的 tag 類型,如下:
export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
module.exports = {
IndeterminateComponent: 0, // Before we know whether it is functional or class
FunctionalComponent: 1,
ClassComponent: 2,
HostRoot: 3, // Root of a host tree. Could be nested inside another node.
HostPortal: 4, // A subtree. Could be an entry point to a different renderer.
HostComponent: 5,
HostText: 6,
CoroutineComponent: 7,
CoroutineHandlerPhase: 8,
YieldComponent: 9,
Fragment: 10,
};
舉例來說:
function MyButton() {
return <button>Click me</button>;
}
// 對應的 Fiber 節點
{
tag: FunctionComponent, // 表示這是一個函數組件
// ...
}
使用的情境,像是在原始碼中 beginWork 函式中,會根據 tag 類型,決定要更新 Fiber node 的方式,如下:
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
priorityLevel: PriorityLevel,
): Fiber | null {
if (
workInProgress.pendingWorkPriority === NoWork ||
workInProgress.pendingWorkPriority > priorityLevel
) {
return bailoutOnLowPriority(current, workInProgress);
}
if (__DEV__) {
ReactDebugCurrentFiber.setCurrentFiber(workInProgress, null);
}
switch (workInProgress.tag) {
case IndeterminateComponent:
return mountIndeterminateComponent(
current,
workInProgress,
priorityLevel,
);
case FunctionalComponent:
return updateFunctionalComponent(current, workInProgress);
case ClassComponent:
return updateClassComponent(current, workInProgress, priorityLevel);
case HostRoot:
return updateHostRoot(current, workInProgress, priorityLevel);
case HostComponent:
return updateHostComponent(current, workInProgress, priorityLevel);
case HostText:
return updateHostText(current, workInProgress);
case CoroutineHandlerPhase:
// This is a restart. Reset the tag to the initial phase.
workInProgress.tag = CoroutineComponent;
// Intentionally fall through since this is now the same.
case CoroutineComponent:
return updateCoroutineComponent(current, workInProgress);
case YieldComponent:
// A yield component is just a placeholder, we can just run through the
// next one immediately.
return null;
case HostPortal:
return updatePortalComponent(current, workInProgress);
case Fragment:
return updateFragment(current, workInProgress);
default:
invariant(
false,
'Unknown unit of work tag. This error is likely caused by a bug in ' +
'React. Please file an issue.',
);
}
}
key & type
key 和 type 的主要目的都是用來辨識 Fiber node 跟舊的 Fiber node 是否相同,以決定是否可以復用舊的 Fiber node
key:Fiber node 的 id,用於識別同一個父節點下的子節點type:指到對應的 element 類型,如果是 Component,就是對應 Component 的名稱,如果是 DOM element,就是對應的 DOM tag name
舉例來說:
function MyButton() {
return <button>Click me</button>;
}
// 對應的 Fiber 節點
{
type: MyButton, // 指向 MyButton 函數
// ...
}
{
type: 'button', // 指向 button DOM element
// ...
}
在原始碼 ReactChildFiber.js中,可以看到 key 和 type 的用途,
就是 reconciliation 的核心邏輯:
- 嘗試覆用現有 Fiber 節點:逐一檢查當前的 Fiber 節點(currentFirstChild 和它的 siblings),判斷它們是否可以覆用。
- 如果可以覆用,更新該節點的相關屬性(props、ref 等)。
- 如果不能覆用,則刪除舊節點。
- 比較的核心條件:
- key 是否相同:React 使用 key 來唯一標識列表中的每個元素。如果 key 不同,則認為是全新的節點,刪除舊節點。
- type 是否相同:即使 key 相同,還需檢查節點類型是否一致(例如 div 與 span 是不同的 type)。
若無法覆用,則刪除舊節點,並為新的 ReactElement 創建一個新的 Fiber 節點。
往 sibling 移動:如果當前節點無法覆用,會繼續檢查它的 sibling,直到所有可能覆用的節點都被檢查完畢
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
priority: PriorityLevel,
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
if (child.type === element.type) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, priority);
existing.ref = coerceRef(child, element);
existing.pendingProps = element.props;
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
} else {
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
}
child, sibling, return
child:現在 fiber node 的第一個子 nodesibling:現在 fiber node 的下一個鄰近的 nodereturn:現在 fiber node 的 parent node,在執行completeWork會回到的 fiber node
pendingWorkPriority
pendingWorkPriority 用於定義 Fiber node 的優先級,根據 v16 的原始碼,pendingWorkPriority 的 Type 為 PriorityLevel,並定義了 5 種優先級,分別是:
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.
TaskPriority: 2, // Completes at the end of the current tick.
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.
};
我們可以看到
SynchronousPriority:同步的優先級,用於控制文字輸入等同步的 side-effects,為最高的優先級TaskPriority:在當前 tick (也就是每一次 Event Loop 的循環) 結束時完成的優先級,為第二高的優先級HighPriority:用於需要快速響應的交互,例如點擊、輸入等,為第三高的優先級LowPriority:用於數據獲取或更新 store 等,為第四高的優先級OffscreenPriority:不會顯示在畫面上的 Task,用於不會立即顯示但可能在顯示時需要完成的工作,為最低的優先級
對於優先順序大於 TaskPriority(也就是 PriorityLevel > 2) 的更新,React 會使用 scheduleDeferredCallback 來處理,
if (nextPriorityLevel > TaskPriority && !isCallbackScheduled) {
scheduleDeferredCallback(performDeferredWork);
isCallbackScheduled = true;
}
而 scheduleDeferredCallback 就是使用了 requestIdleCallback,來達到在瀏覽器每幀渲染的空閑時間中執行,使畫面不會卡頓
requestAnimationFrame Task雖然在 v16 中,React 已經不再使用 requestAnimationFrame 來處理動畫相關的更新,但在比 v16 更早的版本中,React 也有使用 requestAnimationFrame 來處理動畫相關的更新,(source code)
後續在 Concurrent 功能出來後,React 也有恢復使用 requestAnimationFrame 來實現 Concurrent features
pendingProps, memoizedProps
這兩個屬性,主要是要當前 & 要更新的 props,用來決定 Fiber node 是否需要更新
pendingProps:當前 Fiber node 準備更新的 propsmemoizedProps:當前 Fiber node 的 props
例如在 ReactFiberBeginWork.js 中的 updateFunctionalComponent 函式中,
可以看到 pendingProps 和 memoizedProps 的用途:
當我們發現
pendingProps和memoizedProps相同時,我們可以跳過更新,直接 clone child fiber nodes
function updateFunctionalComponent(current, workInProgress) {
var fn = workInProgress.type;
var nextProps = workInProgress.pendingProps;
const memoizedProps = workInProgress.memoizedProps;
if (hasContextChanged()) {
// Normally we can bail out on props equality but if context has changed
// we don't do the bailout and we have to reuse existing props instead.
if (nextProps === null) {
nextProps = memoizedProps;
}
} else {
if (nextProps === null || memoizedProps === nextProps) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
// TODO: consider bringing fn.shouldComponentUpdate() back.
// It used to be here.
}
var unmaskedContext = getUnmaskedContext(workInProgress);
var context = getMaskedContext(workInProgress, unmaskedContext);
var nextChildren;
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
ReactDebugCurrentFiber.setCurrentFiber(workInProgress, 'render');
nextChildren = fn(nextProps, context);
ReactDebugCurrentFiber.setCurrentFiber(workInProgress, null);
} else {
nextChildren = fn(nextProps, context);
}
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren);
memoizeProps(workInProgress, nextProps);
return workInProgress.child;
}
...
function bailoutOnAlreadyFinishedWork(
current,
workInProgress: Fiber,
): Fiber | null {
if (__DEV__) {
cancelWorkTimer(workInProgress);
}
// TODO: We should ideally be able to bail out early if the children have no
// more work to do. However, since we don't have a separation of this
// Fiber's priority and its children yet - we don't know without doing lots
// of the same work we do anyway. Once we have that separation we can just
// bail out here if the children has no more work at this priority level.
// if (workInProgress.priorityOfChildren <= priorityLevel) {
// // If there are side-effects in these children that have not yet been
// // committed we need to ensure that they get properly transferred up.
// if (current && current.child !== workInProgress.child) {
// reuseChildrenEffects(workInProgress, child);
// }
// return null;
// }
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
alternate
alternate 讓 React 可以實現 reconcilation 的機制,對於 workInProgress tree,我們可以指向 current tree 的每一個 Fiber node,以決定是否可以復用舊的
結論
- React Fiber 的底層會有兩個 tree data structure,分別是 current tree 和 workInProgress tree
- React Work Loop 是 Fiber 底層遍歷所有 Fiber node 的過程,會呼叫
beginWork和completeWork函式,生成或更新 Fiber node - Fiber node 的結構中包含以下屬性
tag:用於內部區分 Fiber node 的類型,決定要怎麼處理此類型的 Fiber node,例如ClassComponent和FunctionalComponent的處理方式就不太一樣key和type:用於識別 Fiber node 跟舊的 Fiber node 是否相同child,sibling,return:用於決定每個 Fiber node 的遍歷順序pendingWorkPriority:用於定義 Fiber node 的優先級,次要的會用requestIdleCallback來處理,避免阻塞主執行緒pendingProps和memoizedProps:用於決定 Fiber node 是否需要更新alternate:用於實現 reconcilation 的機制
參考資料
- What Is React Fiber? React.js Deep Dive #2
- React v16.0 source code
- Vercel & ex-React Core Team member - Andrew Clark - React Fiber Architecture