React 原始碼解析系列 - React 的 render 階段(二):beginWork

array_huang發表於2021-09-26

系列文章目錄(同步更新)

本系列文章均為討論 React v17.0.0-alpha 的原始碼

下面來介紹 React Render 的“遞”階段 —— beginWork ,在《React 原始碼解析系列 - React 的 render 階段(一):基本流程介紹》中我們可知 beginWork 的主要作用是建立本次迴圈(performUnitOfWork)主體(unitOfWork)的子 Fiber 節點,其流程如下:

beginWork 的流程圖

從上圖可知,beginWork 的工作路徑有四條:

  • mount (首屏渲染)時建立新的子 Fiber 節點,並返回該新建節點;
  • update時若不滿足複用條件,則與 mount 時一樣建立新的子 Fiber 節點,並 diff 出相應的 effectTag 掛在子 Fiber 節點上,並返回該新建節點;
  • update時若滿足複用條件,則複用 current 樹上對應的子 Fiber 節點(current.child),返回複用後的節點
  • update時若滿足複用條件,則複用 current 樹上對應的子 Fiber 節點(current.child),直接返回 null 值;

歸納一下:

  • 前兩者是主要的工作路徑;
  • 第三條工作路徑 —— “複用節點”實際上在第二條工作路徑 —— reconcileChildFibers(update) 時也會有類似的實現,或者說是不同層次的“複用節點”;
  • 而第四條工作路徑 —— “直接返回 null 值”這就是屬於“深度遍歷”過程中,名為“剪枝”的優化策略,可以減少不必要的渲染,提高效能。

beginWork 的入參

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // ...省略函式體
}

beginWork 有3個引數,但目前我們只關注前兩個:

  • current:與本次迴圈主體(unitOfWork)對應的 current 樹上的節點,即 workInProgress.alternate 。
  • workInProgress :本次迴圈主體(unitOfWork),也即待處理的 Fiber 節點。

判斷是 mount 還是 update

從 beginWork 的流程圖中可知,第一個流程分支是判斷當前為 mount(首屏渲染) 還是 update ;其判斷的依據是:入參 current 是否為null,這是因為 mount(首屏渲染) 時, FiberRootNode 的 current 指標指向null,後續還有很多地方都需要根據這個判斷來做不同的處理。

主要工作路徑

switch (workInProgress.tag) {
  case IndeterminateComponent: 
    // ...省略
  case LazyComponent: 
    // ...省略
  case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
  case ClassComponent: 
    // ...省略
  case HostRoot:
    // ...省略
  case HostComponent:
    // ...省略
  case HostText:
    // ...省略
  // ...省略其他型別
}

mount(首屏渲染) 時會根據不同的 workInProgress.tag(元件型別)來進入到不同的子節點建立邏輯,我們關注最常見的元件型別:FunctionComponent(函式元件) / ClassComponent(類元件) / HostComponent(對標 HTML 標籤),最終這些邏輯都會進入 reconcileChildren 方法。

reconcileChildren

下面來看看 reconcileChildren 方法:

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    // 對於mount的元件
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 對於update的元件
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

從函式名 —— reconcileChildren 就能看出這是 Reconciler 模組的核心部分;這裡我們看到會根據 mount(首屏渲染) 還是 update 來走不同的方法 —— mountChildFibers | reconcileChildFibers ,但不論走哪個邏輯,最終都會生成新的子 Fiber 節點並賦值給 workInProgress.child ,並作為下次迴圈(performUnitOfWork)執行時的迴圈主體(unitOfWork);
下面我們來看看這兩個方法是什麼。

export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

從上面程式碼可以看出, mount 時執行的 reconcileChildFibers 和 update 時執行的 mountChildFibers 方式,實際上都是由 ChildReconciler 這個方法封裝出來的,差別只在於傳參不同。

ChildReconciler

下面來看 ChildReconciler

// shouldTrackSideEffects 表示是否追蹤副作用
function ChildReconciler(shouldTrackSideEffects) {
    /* 內部函式集合 */
    function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
        if (!shouldTrackSideEffects) { // 如不需要追蹤副作用則直接返回
            // Noop.
            return;
        }
        /* 在當前節點(returnFiber)上標記刪除目標節點 */
        const deletions = returnFiber.deletions;
        if (deletions === null) {
            returnFiber.deletions = [childToDelete]; // 加入“待刪除子節點”的陣列中
            returnFiber.flags |= ChildDeletion; // 標記當前節點需要刪除子節點
        } else {
            deletions.push(childToDelete);
        }
    }
    function placeSingleChild(newFiber: Fiber): Fiber {
        /* 標記用新節點去替代原來的節點(如果有“原來的節點”的話) */
        if (shouldTrackSideEffects && newFiber.alternate === null) {
            newFiber.flags |= Placement;
        }
        return newFiber;
    }
    
    // ...還有其它很多內部函式

    /* 主流程 */
    function reconcileChildFibers(
        returnFiber: Fiber,
        currentFirstChild: Fiber | null,
        newChild: any,
        lanes: Lanes,
    ): Fiber | null {
        
    }

    return reconcileChildFibers; // 返回主方法,其中已經通過閉包聯絡上一堆內部方法了
}

從上面的程式碼我們可以看出 ChildReconciler 實際上是通過閉包封裝了一堆內部函式,其主要流程實際上就是 reconcileChildFibers 這個方法,而在 reconcileChildren 方法中的呼叫也正是呼叫的這個 reconcileChildFibers 方法;我們解讀一下該方法的入參:

  • returnFiber:當前 Fiber 節點,即 workInProgress
  • currentFirstChild:current 樹上對應的當前 Fiber 節點的第一個子 Fiber 節點,mount 時為 null
  • newChild:子節點(ReactElement)
  • lanes:優先順序相關

然後我們回過頭來看這 ChildReconciler 方法的入參 —— shouldTrackSideEffects ,這個引數的字面意思是“是否需要追蹤副作用”,所謂的“副作用”,指的就是是否需要做 DOM 操作,需要的話就會在當前 Fiber 節點中打上 EffectTag ,即“追蹤”副作用;而也僅有在 update 的時候,才需要“追蹤副作用”,即把 current 這個 Fiber 節點與本次更新元件狀態後的 ReactElement 做對比(diff),然後得出本次更新的 Fiber 節點,以及在該節點上打上 diff 的結果 —— EffectTag 。

子節點(ReactElement)

這裡需要展開說明一下子節點(ReactElement)是怎麼來的:

  • 針對元件中的 jsx 程式碼,babel 會在編譯階段將其轉換成一個 React.createElement() 呼叫的程式碼段。
  • 如果是類元件,則執行其 render 成員方法,並得到 React.createElement() 執行的結果 —— 一個ReactElement 物件。
  • 如果是函式元件,則直接執行,同樣得到一個 ReactElement 物件。
  • 如果是 HostComponent ,即一般的 HTML ,同樣也是獲得一個 ReactElement 物件。
  • React.createElement 的原始碼請看這裡

reconcileChildFibers

reconcileChildFibers 方法中,首先會判斷 newChild 的型別,來進入到不同邏輯中。

主要有這些型別:

  • ReactElement
  • Portal
  • React.Lazy包裹後的元素
  • 陣列
  • 純文字(包括 number 和 string)
    function reconcileChildFibers(
        returnFiber: Fiber,
        currentFirstChild: Fiber | null,
        newChild: any,
        lanes: Lanes,
    ): Fiber | null {
        if (typeof newChild === 'object' && newChild !== null) {
            switch (newChild.$$typeof) { // 根據$$typeof屬性來進一步區分型別
                case REACT_ELEMENT_TYPE:
                    return placeSingleChild(
                        reconcileSingleElement(
                            returnFiber,
                            currentFirstChild,
                            newChild,
                            lanes,
                        ),
                    );
                case REACT_PORTAL_TYPE:
                    // 省略
                case REACT_LAZY_TYPE:
                    // 省略
            }
            /* 處理子節點是一個陣列的情況 */
            if (isArray(newChild)) {
                return reconcileChildrenArray(
                    returnFiber,
                    currentFirstChild,
                    newChild,
                    lanes,
                );
            }

            // 省略
        }
        /* 處理純文字 */
        if (typeof newChild === 'string' || typeof newChild === 'number') {
            return placeSingleChild(
                reconcileSingleTextNode(
                    returnFiber,
                    currentFirstChild,
                    '' + newChild,
                    lanes,
                ),
            );
        }

        // 省略
    }

$$typeof

從上面的程式碼中,我們看到除了直接用 newChild 的資料型別來判斷走哪個程式碼分支外,還用了 newChild.$$typeof` 來判斷,這個 `$$typeof 就是當前 ReactElement 的型別,它的值是一個 Symbol 值,並且是已經預先定義好的,我們可以看到在 ReactElement 的工廠函式中,已經對 $$typeof 複製為 REACT_ELEMENT_TYPE 了。

為什麼需要有這 $$typeof` 屬性呢?是因為需要防止 **XSS 攻擊**:當應用允許儲存並回顯一個 JSON 物件時,惡意使用者可構建一個**偽 ReactElement 物件**,形如下面的例子,如果 React 不加分辨,則會直接將該偽 ReactElement 物件渲染到 DOM 樹上。因此從 React 0.14 版本後,React 會為每個真正的 ReactElement 新增 `$$typeof 屬性,只有擁有該屬性的 ReactElement 物件才會被 React 渲染;而由於該屬性為 Symbol 型別,無法使用 JSON 來構造,因此便能堵住這一漏洞。

/* 惡意的json物件 */
var xssJsonObject = {
  type: 'div',
  props: {
    dangerouslySetInnerHTML: {
      __html: '/* 惡意指令碼 */'
    },
  },
  // ...
};

reconcileSingleElement

接著,我們以 ReactElement 型別的處理邏輯為示例繼續往下走,會呼叫 reconcileSingleElement 方法。

嘗試複用 current 樹上對應的子 Fiber 節點

在該方法中,首先會有這麼一個 while 迴圈:

const key = element.key;
let child = currentFirstChild;
while (child !== null) {
  if (child.key === key) {
    const elementType = element.type;
    if (elementType === REACT_FRAGMENT_TYPE) {
        if (child.tag === Fragment) {
          deleteRemainingChildren(returnFiber, child.sibling); // 刪除掉該child節點的所有sibling節點
          const existing = useFiber(child, element.props.children); // 複用child節點
          existing.return = returnFiber; // 重置新Fiber節點的return指標,指向當前Fiber節點
          return existing;
        }
    } else {
        if (child.elementType === elementType) {
            deleteRemainingChildren(returnFiber, child.sibling); // 刪除掉該child節點的所有sibling節點
            const existing = useFiber(child, element.props); // 複用child節點
            existing.ref = coerceRef(returnFiber, child, element); // 處理ref
            existing.return = returnFiber; // 重置新Fiber節點的return指標,指向當前Fiber節點
            return existing;
        }

        // Didn't match.
        deleteRemainingChildren(returnFiber, child);
        break;
  } else {
    deleteChild(returnFiber, child); // 在returnFiber標記刪除該子節點
  }
  child = child.sibling; // 指標指向current樹中的下一個節點
}

上面這段程式碼的作用是找出上次更新中, current 樹對應 Fiber 節點中所有不可複用的子節點,並在 當前 Fiber 節點(returnFiber)中標記需要刪除的 effectTag ;判斷的標準大致是:

  • 若某個 current Fiber 子節點的 key 屬性與本次渲染中的 child.key 不一致,則標記刪除
  • 在 key 屬性相同的前提下:若某個 current Fiber 子節點與本次渲染中的 child 均為 Fragment ,或是它們的 elementType 屬性一致,那麼則執行復用。

複用的流程基本如下:

  1. deleteRemainingChildren(returnFiber, child.sibling),這是因為走到 reconcileSingleElement 這個方法中意味著當前處理節點只有一個子節點,因此找到可複用的子節點後,可以標記刪除掉剩下的(sibling)子節點。
  2. const existing = useFiber(child, element.props);,呼叫 useFiber 方法來複用子 Fiber 節點。
  3. existing.return = returnFiber;,建立子 Fiber 節點(existing)與當前 Fiber 節點(returnFiber)的父子關係(return屬性)。

無法複用,建立新的 Fiber 子節點

如果沒有可複用的子節點的話,會進入建立新的子節點的邏輯:

if (element.type === REACT_FRAGMENT_TYPE) {
    // ...建立Fragment型別的子節點,忽略
} else {
    const created = createFiberFromElement(element, returnFiber.mode, lanes); // 根據當前子節點的ReactElement來建立新的Fiber節點
    created.ref = coerceRef(returnFiber, currentFirstChild, element);
    created.return = returnFiber;
    return created;
}

下詳細介紹如何複用子節點以及如何建立一個全新的子節點

複用子節點 —— useFiber

複用子節點所呼叫的是 useFiber 方法,我們回顧下是怎麼呼叫這個方法的:const existing = useFiber(child, element.props);

這裡的 child 指的是確定可以複用的 current 樹子 Fiber 節點,而 element.props 則是本次更新時 ReactElement 獲得的 props 值(該值也被稱為 pendingProps)。

然後我們再看 useFiber 這個方法本身:

function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
    const clone = createWorkInProgress(fiber, pendingProps);
    clone.index = 0; // 重置一下:當前子節點必然為第一個子節點
    clone.sibling = null; // 重置一下:當前子節點沒有sibling
    return clone;
}

可以看出這個方法主要就是呼叫了 createWorkInProgress 方法。

createWorkInProgress

我們接下來看看 createWorkInProgress 方法幹了什麼:

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate;
  /*
    如果current.alternate為空(這裡先不要理解成是workInProgress),
    則複用current節點,再根據本次更新的props來new一個FiberNode物件
  */
  if (workInProgress === null) {
    // createFiber是Fiber節點(FiberNode)的工廠方法
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode, // mode屬性表示渲染模式,一個二進位制值
    );
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode; // DOM 節點

    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    // 如果current.alternate不為空,則重置workInProgress的pendingProps/type/effectTag等屬性
  }
  // 複製current的子節點、上次更新時的props和state
  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue;

  // 複製current的指標
  workInProgress.sibling = current.sibling;
  workInProgress.index = current.index;
  workInProgress.ref = current.ref;

  return workInProgress;
}

這裡需要關注的重點是:

  • 如果 current.alternate 不為空,那此時 current.alternate 應該是上上次更新時的樹節點,我們可以留意到這種場景下,並沒有建立新的 Fiber 節點,而是直接複用了這個 current.alternate 節點(只是對它的一些屬性進行重置),這就可以看出“雙快取”的本質,並非是“每建立一棵新的 Fiber 樹就把上上次更新時的 Fiber 樹拋棄掉”,而是”在建立本次更新的 Fiber 樹時,儘量複用上上次更新時的 Fiber 樹,保證任一時刻最多隻有兩棵 Fiber 樹”;而所謂的 current 和 workInProgress ,其實都是相對的,只是取決於此時的 FiberRootNode 的 current 屬性指向哪棵 Fiber 樹而已。
  • FiberNode 上的 node 屬性表示渲染模式,是一個二進位制值,具體定義在這裡

建立全新子節點 —— createFiberFromElement

建立全新子節點所呼叫的方法是 createFiberFromElement

export function createFiberFromElement(
  element: ReactElement,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
  let owner = null;
  const type = element.type;
  const key = element.key;
  const pendingProps = element.props;
  const fiber = createFiberFromTypeAndProps(
    type,
    key,
    pendingProps,
    owner,
    mode,
    lanes,
  );
  return fiber;
}

可以看出,createFiberFromElement 方法主要就是執行了 createFiberFromTypeAndProps 這個方法,而該方法主要是解析確定下新節點的 tag、type 屬性,並呼叫 createFiber 方法 new 了一個新節點物件。

reconcileChildrenArray

當一個節點有多個子節點(如:<div><span>2</span>3</div>),那麼此時 newChild 就是一個陣列,此時便會進入到 reconcileChildrenArray 的方法中

回顧下在 reconcileChildFibers 方法中是如何呼叫該方法的:

if (isArray(newChild)) {
    return reconcileChildrenArray(
        returnFiber, // 當前的Fiber節點
        currentFirstChild, // current樹中對應的子Fiber節點
        newChild, // 本次更新的子ReactElement
        lanes, // 優先順序相關
    );
}

與 reconcileSingleElement 方法類似,reconcileChildrenArray 實際上也是嘗試複用 current 樹上的對應子節點,如遇到無法複用的子節點,則建立新節點;但不同點在於, reconcileChildrenArray 需要處理的子節點實際上是一個陣列,因此需要進行新陣列(本次更新中建立的 ReactElement )與原陣列(current 樹上對應的子 Fiber 節點)間的對比,其大概思路如下:

  1. 根據 index 遍歷新老陣列元素,一一對比新老陣列,對比的依據是 key 屬性是否相同;
  2. 若 key 屬性相同,則複用節點並繼續進行遍歷,直到遇到不能複用的情況(或老陣列中的所有節點都已經被複用)則結束遍歷。
  3. 如果老陣列所有節點都已經被複用,但新陣列尚有未處理的部分,則依據新陣列該未處理部分來建立新的 Fiber 節點。
  4. 如果老陣列有節點尚未被遍歷(即在第一次遍歷中碰到不能複用的情況而中途退出),那麼將這部分放進一個 map 裡,然後繼續遍歷新陣列,看看有沒有能從 map 裡找到能複用的;若能複用的,則進行復用,否則建立新 Fiber 節點;對於未被複用的舊節點,則全部標記刪除(deleteChild)。

需要注意的是,雖然 reconcileChildrenArray 把整個陣列(newChild)的 Fiber 節點都建立出來了,但其最終 return 的實際上是陣列中的第一個 Fiber 節點,換句話說:在下次 performUnitOfWork 中的迴圈主體(unitOfWork)實際上是這個陣列中的第一個 Fiber 節點;而當這“第一個 Fiber 節點”執行到 completeWork 階段時,會取出它的 sibling —— 也就是這個陣列中的第二個 Fiber 節點來作為下次 performUnitOfWork 中的迴圈主體(unitOfWork)。

優化的工作路徑

上文花了非常多的篇幅來一路深入介紹 beginWork 的主要工作路徑,下面我們還是回到 beginWork 處:

if (current !== null) {
    const oldProps = current.memoizedProps
    const newProps = workInProgress.pendingProps

    if (
        oldProps !== newProps ||
        hasLegacyContextChanged() // 判斷context是否有變化
    ) {
        /* 該didReceiveUpdate變數代表本次更新中本Fiber節點是否有變化 */
        didReceiveUpdate = true
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
        didReceiveUpdate = false
        switch (
            workInProgress.tag
        ) {
            // 省略
        }
        return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes)
    } else {
        didReceiveUpdate = false
    }
} else {
    didReceiveUpdate = false
}

當前程式碼段的作用是判斷當前 Fiber 節點是否有變化,其判斷的依據是: props 和 fiber.type(如函式元件的函式、類元件的類、html 標籤等)和 context 沒有變化;並且在 Fiber 節點沒有變化的前提下(!includesSomeLane(renderLanes, updateLanes)涉及優先順序暫不討論),嘗試原封不動地複用子 Fiber 節點或是直接“剪枝”:bailoutOnAlreadyFinishedWork 方法。

bailoutOnAlreadyFinishedWork

接下來我們來看 bailoutOnAlreadyFinishedWork 方法:

function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // 省略

  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { // 判斷子節點中是否需要檢查更新
    return null; // 剪枝:不需要關注子節點(ReactElement)了
  } else {
    cloneChildFibers(current, workInProgress);
    return workInProgress.child;
  }
}
  • !includesSomeLane(renderLanes, workInProgress.childLanes) === true 時,會直接 return null ,這就是上文說到的“剪枝”策略:不再關注其下的子節點,轉到本節點的 completeWork 階段。
  • 不滿足上述條件時,則克隆 current 樹上對應的子 Fiber 節點並返回,作為下次 performUnitOfWork 的主體。

克隆 current 樹上對應的子 Fiber 節點 —— cloneChildFibers

這裡的“克隆 current 樹上對應的子 Fiber 節點”可能會造成一些迷惑,我們直接看 cloneChildFibers 程式碼:

export function cloneChildFibers(
  current: Fiber | null,
  workInProgress: Fiber,
): void {
  // 省略
  /* 判斷子節點為空,則直接返回 */
  if (workInProgress.child === null) {
    return;
  }

  let currentChild = workInProgress.child; // 這裡怎麼會是拿workInProgress.child來充當currentChild呢?解釋看下文
  let newChild = createWorkInProgress(currentChild, currentChild.pendingProps); // 複用currentChild
  workInProgress.child = newChild;

  newChild.return = workInProgress; // 讓子Fiber節點與當前Fiber節點建立聯絡
  /* 遍歷子節點的所有兄弟節點並進行節點複用 */
  while (currentChild.sibling !== null) {
    currentChild = currentChild.sibling;
    newChild = newChild.sibling = createWorkInProgress(
      currentChild,
      currentChild.pendingProps,
    );
    newChild.return = workInProgress;
  }
  newChild.sibling = null;
}

這裡我們看到明明是拿 workInProgress.child 去建立子節點的,怎麼會說成是克隆 current 樹上對應的子 Fiber 節點呢?而且按理說此時還沒建立子 Fiber 節點, workInProgress.child 怎麼會有值呢?

其實是這樣的,當前節點是在父節點的 beginWork 階段通過 createWorkInProgress 方法建立出來的,會執行 workInProgress.child = current.child,因此在本節點建立自己的子節點並覆蓋 workInProgress.child 之前,workInProgress.child 其實指向的就是 current.child

下面以圖例說明:

EffectTag

上文說到,在 update 的場景下,除了與 mount 時一樣建立子 Fiber 節點外,還會與上次渲染的子節點進行 diff ,從而得出需要進行什麼樣的 DOM 操作,並將其“標記”在新建的子 Fiber 節點上,下面就來介紹一下這個“標記” —— EffectTag

EffectTag 是 Fiber Reconciler 相對於 Stack Reconciler 的一大革新,以往 Stack Reconciler 是每 diff 出一個節點就進行 commit 的(當然,由於 Stack Reconciler 是同步執行的,因此直到所有節點都 commit 完了才會輪到瀏覽器 GUI 執行緒進行渲染,這樣就不會造成“僅部分更新”的問題),而 Fiber Recconciler 則在 diff 出來後,僅在目標節點打上 effectTag ,而不會走到 commit 階段,待所有節點都完成 render 階段後才統一進 commit 階段,這樣便實現了 reconciler(render 階段)renderer(commit 階段) 的解耦。

EffectTag 型別的定義

effectTag 實際上就是需要對節點需要執行的 DOM 操作(也可認為是副作用,即 sideEffect ),定義有以下這些型別(僅節選部分 EffectTag 型別):

// DOM需要插入到頁面中
export const Placement = /*                */ 0b00000000000010;
// DOM需要更新
export const Update = /*                   */ 0b00000000000100;
// DOM需要插入到頁面中並更新
export const PlacementAndUpdate = /*       */ 0b00000000000110;
// DOM需要刪除
export const Deletion = /*                 */ 0b00000000001000;

為什麼需要使用二進位制來表示 effectTag 呢?

這是因為同一個 Fiber 節點,可能需要執行多種型別的 DOM 操作,即需要打上多種型別的 effectTag,那麼這時候只要將這些 effectTag 做“按位或”(|)運算,那麼就可以彙總成當前 Fiber 節點擁有的所有 effectTag 型別了。

若要判斷某個 Fiber 節點是否有某種型別的 effectTag ,其實也很簡單,拿 fiber.effectTag 跟這個型別的 effectTag 所對應的二進位制值來做“按位與”(&)運算,再根據運算結果是否為 NoEffect(0) 即可。

renderer 根據 EffectTag 來執行 DOM 操作

以 renderer “判斷當前節點是否需要進行插入 DOM 操作”為例:

  • fiber.stateNode 存在,即Fiber節點中儲存了對應的 DOM 節點
  • (fiber.effectTag & Placement) !== 0,即Fiber節點存在 Placement effectTag。

以上對於 update 操作都很好理解,但 mount 時在 reconcileChildren 中呼叫的 mountChildFibers 的要怎麼辦呢?

mount 時的 fiber.stateNode 為 null ,那不就不會執行插入 DOM 操作?

fiber.stateNode 會在節點的“歸”階段,即 completeWork 中進行建立。

mount 時每個節點上都會有 Placement EffectTag

假設 mountChildFibers 也會賦值 effectTag ,那麼可以預見 mount 時整棵 Fiber 樹所有節點都會有 Placement effectTag 。那麼 commit 階段在執行 DOM 操作時每個節點都會執行一次插入操作,這樣大量的DOM操作是極低效的。

為了解決這個問題,在 mount 時只有 FiberRootNode 會賦值 Placement effectTag ,在 commit 階段只會執行一次插入操作。

我們回到 reconcileChildren 方法在下圖所示位置打上斷點,接著重新整理頁面,看看首屏渲染時會不會走到 reconcileChildFibers 這個位置:

斷點

接著,我們就能夠看到如下的斷點結果:當前的 workInProgress 入參實際上就是 FiberRootNode ,也就是<App />元件掛載的 DOM 元素(ReactDOM.render(<App />, document.getElementById('root')));而當前的 current 入參是不為空的,因此才會走到這個一般只有 update 才會執行的程式碼段來;而當我們恢復程式碼執行後,首屏便已經渲染了,並沒有再次停在斷點位置,因此,在 mount(首屏渲染) 時,只有 FiberRootNode 會“跟蹤副作用”(shouldTrackSideEffects === true),即打上 EffectTag 。

相關文章