[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