[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