iT邦幫忙

0

ReactFiber節點的更新入口:beginWork

nero 2021-01-23 14:20:551396 瀏覽
  • 分享至 

  • xImage
  •  

React的更新任務主要是調用壹個叫做workLoop的工作循環去構建workInProgress樹,構建過程分為兩個階段:向下遍歷和向上回溯,向下和向上的過程中會對途徑的每個節點進行beginWork和completeWork。

本文即將提到的beginWork是處理節點更新的入口,它會依據fiber節點的類型去調用不同的處理函數。

React對每個節點進行beginWork操作,進入beginWork後,首先判斷節點及其子樹是否有更新,若有更新,則會在計算新狀態和diff之後生成新的Fiber,然後在新的fiber上標記flags(effectTag),最後return它的子節點,以便繼續針對子節點進行beginWork。若它沒有子節點,則返回null,這樣說明這個節點是末端節點,可以進行向上回溯,進入completeWork階段。

點擊進入React源碼調試倉庫。

beginWork的工作流程如下圖,圖中簡化了流程,只對App節點進行了beginWork處理,其余節點流程相似

beginWork流程

職責

通過概述可知beginWork階段的整體工作是去更新節點,並返回子樹,但真正的beginWork函數只是節點更新的入口,不會直接進行更新操作。作為入口,它的職責很明顯,攔截無需更新的節點。同時,它還會將context信息入到棧中(beginWork入棧,completeWork出棧),暫時先不關註。

function beginWork(
    current: Fiber | null,
    workInProgress: Fiber,
    renderLanes: Lanes
): Fiber | null {
  // 獲取workInProgress.lanes,可通過判斷它是否為空去判斷該節點是否需要更新
  const updateLanes = workInProgress.lanes;

  // 依據current是否存在判斷當前是首次掛載還是後續的更新
  // 如果是更新,先看優先級夠不夠,不夠的話就能調用bailoutOnAlreadyFinishedWork
  // 復用fiber節點來跳出對當前這個節點的處理了。
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    if (
        oldProps !== newProps ||
        hasLegacyContextChanged()
    ) {
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      // 此時無需更新
      didReceiveUpdate = false;
      switch (workInProgress.tag) {
        case HostRoot:
          ...
        case HostComponent:
          ...
        case ClassComponent:
          ...
        case HostPortal:
          ...
      }

      // 攔截無需更新的節點
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
  } else {
    didReceiveUpdate = false;
  }

  // 代碼走到這裏說明確實要去處理節點了,此時會根據不同fiber的類型
  // 去調用它們各自的處理函數

  // 先清空workInProgress節點上的lanes,因為更新過程中用不到,
  // 在處理完updateQueue之後會重新賦值
  workInProgress.lanes = NoLanes;

  // 依據不同的節點類型來處理節點的更新
  switch (workInProgress.tag) {
    case IndeterminateComponent:
      ...
    case LazyComponent:
      ...
    case FunctionComponent:
      ...
      return updateFunctionComponent(
          current,
          workInProgress,
          Component,
          resolvedProps,
          renderLanes,
      );
    }
    case ClassComponent:
      ...
      return updateClassComponent(
          current,
          workInProgress,
          Component,
          resolvedProps,
          renderLanes,
      );
    }
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostText:
      return updateHostText(current, workInProgress);

    ......
  }
}

可以看出,壹旦節點進入beginWork,會先去識別該節點是否需要處理,若無需處理,則調用bailoutOnAlreadyFinishedWork復用節點,否則才真正去更新。

如何區分更新與初始化過程

判斷current是否存在。

這首先要理解current是什麽,基於雙緩沖的規則,調度更新時有兩棵樹,展示在屏幕上的current Tree和正在後臺基於current樹構建的
workInProgress Tree。那麽,current和workInProgress可以理解為鏡像的關系。workLoop循環當前遍歷到的workInProgress節點來自於它對應的current節點父級fiber的子節點(即current節點),所以workInProgress節點和current節點也是鏡像的關系。

如果是首次渲染,對具體的workInProgress節點來說,它是沒有current節點的,如果是在更新過程,由於current節點已經在首次渲染時產生了,所以workInProgress節點有對應的current節點存在。

最終會根據節點是首次渲染還是更新來決定是創建fiber還是diff fiber。只不過更新時,如果節點的優先級不夠會直接復用已有節點,即走跳出(bailout)的邏輯,而不是去走下面的更新邏輯。

復用節點過程

節點可復用表示它無需更新。在上面beginWork的代碼中可以看到,若節點的優先級不滿足要求,說明它不用更新,會調用bailoutOnAlreadyFinishedWork函數,去復用current節點作為新的workInProgress樹的節點。

beginWork函數中攔截無需更新節點的邏輯

if (!includesSomeLane(renderLanes, updateLanes)) {
  ...

  // 此時無需更新,攔截無需更新的節點
  return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

beginWork它的返回值有兩種情況:

  • 返回當前節點的子節點,然後會以該子節點作為下壹個工作單元繼續beginWork,不斷往下生成fiber節點,構建workInProgress樹。
  • 返回null,當前fiber子樹的遍歷就此終止,從當前fiber節點開始往回進行completeWork。

bailoutOnAlreadyFinishedWork函數的返回值也是如此。

  • 返回當前節點的子節點,前置條件是當前節點的子節點有更新,此時當前節點未經處理,是可以直接復用的,復用的過程就是復制壹份current節點的子節點,並把它return出去。
  • 返回null,前提是當前子節點沒有更新,當前子樹的遍歷過程就此終止。開始completeWork。

從這個函數中,我們也可以意識到,識別當前fiber節點的子樹有無更新顯得尤為重要,這可以決定是否終止當前Fiber子樹的遍歷,將復雜度直接降低。實際上可以通過fiber.childLanes去識別,childLanes如果不為空,表明子樹中有需要更新的節點,那麽需要繼續往下走。

標記fiber.childLanes的過程是在開始調度時發生的,在markUpdateLaneFromFiberToRoot 函數中

帶著上邊的認知,來看壹下源碼了解具體的復用過程:

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

  if (current !== null) {
    workInProgress.dependencies = current.dependencies;
  }

  // 標記有跳過的更新
  markSkippedUpdateLanes(workInProgress.lanes);

  // 如果子節點沒有更新,返回null,終止遍歷
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    return null;
  } else {
    // 子節點有更新,那麽從current上復制子節點,並return出去
    cloneChildFibers(current, workInProgress);
    return workInProgress.child;
  }
}

總結

beginWork的主要功能就是處理當前遍歷到的fiber,經過壹番處理之後返回它的子fiber,壹個壹個地往外吐出fiber節點,那麽workInProgress樹也就會被壹點壹點地構建出來。

這是beginWork的大致流程,但實際上,核心更新的工作都是在各個更新函數中,這些函數會安排fiber節點依次進入兩大處理流程:計算新狀態和Diff算法,限於篇幅,這兩個內容會分兩篇文章詳細講解,可以持續關註。


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言