React源碼解析之completeUnitOfWork

前言:
(1) 關於completeUnitOfWork()在哪裏使用到,請看下 React源碼解析之workLoop 中的2、performUnitOfWorkjavascript

(2) 本文須要瞭解的基礎知識
workInProgressfiber對象,它的相關屬性請看:
React源碼解析之RootFiberphp

② 源碼裏A&B===C&運算符的含義,請看 前端小知識10點 中的第八點前端

1、completeUnitOfWork
做用:
完成當前節點的work,並賦值Effect鏈,而後移動到兄弟節點,重複該操做,當沒有更多兄弟節點時,返回至父節點,最終返回至root節點java

源碼:react

//完成當前節點的 work,而後移動到兄弟節點,重複該操做,當沒有更多兄弟節點時,返回至父節點
function completeUnitOfWork(unitOfWork: Fiber)Fiber | null {
  // Attempt to complete the current unit of work, then move to the next
  // sibling. If there are no more siblings, return to the parent fiber.

  //從下至上,移動到該節點的兄弟節點,若是一直往上沒有兄弟節點,就返回父節點
  //可想而知,最終會到達 root 節點
  workInProgress = unitOfWork;
  do {
    // The current, flushed, state of this fiber is the alternate. Ideally
    // nothing should rely on this, but relying on it here means that we don't
    // need an additional field on the work in progress.

    //獲取當前節點
    const current = workInProgress.alternate;
    //獲取父節點
    const returnFiber = workInProgress.return;

    // Check if the work completed or if something threw.
    //判斷節點的操做是否完成,仍是有異常丟出
    //Incomplete表示捕獲到該節點拋出的 error

    //&是表示位的與運算,把左右兩邊的數字轉化爲二進制,而後每一位分別進行比較,若是相等就爲1,不相等即爲0

    //若是該節點沒有異常拋出的話,便可正常執行
    if ((workInProgress.effectTag & Incomplete) === NoEffect) {
      //dev 環境,可不看
      setCurrentDebugFiberInDEV(workInProgress);

      let next;
      //若是不能使用分析器的 timer 的話,直接執行completeWork,
      //不然執行分析器timer,並執行completeWork
      if (
        !enableProfilerTimer ||
        (workInProgress.mode & ProfileMode) === NoMode
      ) {
        //完成該節點的更新
        next = completeWork(current, workInProgress, renderExpirationTime);
      } else {
        //啓動分析器的定時器,並賦成當前時間
        startProfilerTimer(workInProgress);
        //完成該節點的更新
        next = completeWork(current, workInProgress, renderExpirationTime);
        // Update render duration assuming we didn't error.
        //在沒有報錯的前提下,更新渲染持續時間

        //記錄分析器的timer的運行時間間隔,並中止timer
        stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
      }
      //中止 work 計時,可不看
      stopWorkTimer(workInProgress);
      //dev 環境,可不看
      resetCurrentDebugFiberInDEV();
      //更新該節點的 work 時長和子節點的 expirationTime
      resetChildExpirationTime(workInProgress);
      //若是next存在,則表示產生了新 work
      if (next !== null) {
        // Completing this fiber spawned new work. Work on that next.
        //返回 next,以便執行新 work
        return next;
      }
      //若是父節點存在,而且其 Effect 鏈沒有被賦值的話
      if (
        returnFiber !== null &&
        // Do not append effects to parents if a sibling failed to complete
        (returnFiber.effectTag & Incomplete) === NoEffect
      ) {
        // Append all the effects of the subtree and this fiber onto the effect
        // list of the parent. The completion order of the children affects the
        // side-effect order.
        //子節點的完成順序會影響反作用的順序

        //若是父節點沒有掛載firstEffect的話,將當前節點的firstEffect賦值給父節點的firstEffect
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = workInProgress.firstEffect;
        }
        //同上,根據當前節點的lastEffect,初始化父節點的lastEffect
        if (workInProgress.lastEffect !== null) {
          //若是父節點的lastEffect有值的話,將nextEffect賦值
          //目的是串聯Effect鏈
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
          }
          returnFiber.lastEffect = workInProgress.lastEffect;
        }

        // If this fiber had side-effects, we append it AFTER the children's
        // side-effects. We can perform certain side-effects earlier if needed,
        // by doing multiple passes over the effect list. We don't want to
        // schedule our own side-effect on our own list because if end up
        // reusing children we'll schedule this effect onto itself since we're
        // at the end.
        //獲取反作用標記
        const effectTag = workInProgress.effectTag;

        // Skip both NoWork and PerformedWork tags when creating the effect
        // list. PerformedWork effect is read by React DevTools but shouldn't be
        // committed.
        //若是該反作用標記大於PerformedWork
        if (effectTag > PerformedWork) {
          //當父節點的lastEffect不爲空的時候,將當前節點掛載到父節點的反作用鏈的最後
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress;
          } else {
            //不然,將當前節點掛載在父節點的反作用鏈的頭-firstEffect上
            returnFiber.firstEffect = workInProgress;
          }
          //不管父節點的lastEffect是否爲空,都將當前節點掛載在父節點的反作用鏈的lastEffect上
          returnFiber.lastEffect = workInProgress;
        }
      }
    }
    //若是該 fiber 節點未能完成 work 的話(報錯)
    else {
      // This fiber did not complete because something threw. Pop values off
      // the stack without entering the complete phase. If this is a boundary,
      // capture values if possible.
      //節點未能完成更新,捕獲其中的錯誤
      const next = unwindWork(workInProgress, renderExpirationTime);

      // Because this fiber did not complete, don't reset its expiration time.
      //因爲該 fiber 未能完成,因此沒必要重置它的 expirationTime
      if (
        enableProfilerTimer &&
        (workInProgress.mode & ProfileMode) !== NoMode
      ) {
        // Record the render duration for the fiber that errored.
        //記錄分析器的timer的運行時間間隔,並中止timer
        stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);

        // Include the time spent working on failed children before continuing.
        //雖然報錯了,但仍然會累計 work 時長
        let actualDuration = workInProgress.actualDuration;
        let child = workInProgress.child;
        while (child !== null) {
          actualDuration += child.actualDuration;
          child = child.sibling;
        }
        workInProgress.actualDuration = actualDuration;
      }
      //若是next存在,則表示產生了新 work
      if (next !== null) {
        // If completing this work spawned new work, do that next. We'll come
        // back here again.
        // Since we're restarting, remove anything that is not a host effect
        // from the effect tag.
        // TODO: The name stopFailedWorkTimer is misleading because Suspense
        // also captures and restarts.
        //中止失敗的 work 計時,可不看
        stopFailedWorkTimer(workInProgress);
        //更新其 effectTag,標記是 restart 的
        next.effectTag &= HostEffectMask;
        //返回 next,以便執行新 work
        return next;
      }
      //中止 work 計時,可不看
      stopWorkTimer(workInProgress);
      //若是父節點存在的話,重置它的 Effect 鏈,標記爲「未完成」
      if (returnFiber !== null) {
        // Mark the parent fiber as incomplete and clear its effect list.
        returnFiber.firstEffect = returnFiber.lastEffect = null;
        returnFiber.effectTag |= Incomplete;
      }
    }
    //獲取兄弟節點
    const siblingFiber = workInProgress.sibling;
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      return siblingFiber;
    }
    // Otherwise, return to the parent
    //若是能執行到這一步的話,說明 siblingFiber 爲 null,
    //那麼就返回至父節點
    workInProgress = returnFiber;
  } while (workInProgress !== null);

  // We've reached the root.
  //當執行到這裏的時候,說明遍歷到了 root 節點,已完成遍歷
  //更新workInProgressRootExitStatus的狀態爲「已完成」
  if (workInProgressRootExitStatus === RootIncomplete) {
    workInProgressRootExitStatus = RootCompleted;
  }
  return null;
}
複製代碼

解析:
① 總體上看是一個大的while循環:
從當前節點開始,遍歷到兄弟節點,當無兄弟節點時,返回至父節點,
再從父節點開始,遍歷到兄弟節點,當無兄弟節點時,返回至父父節點,
可想而知,最終會返回至rootFiber節點git

② 當整顆 fiber 樹遍歷完成後,更新workInProgressRootExitStatus的狀態爲「已完成」github

咱們來看一下do...while內部的邏輯:
(1) 若是該節點可正常執行的話web

① 直接執行completeWork()方法,更新該節點(從fiber對象轉變成真實的DOM節點)app

ps:下篇解析該方法ide

② 若是能夠啓用ProfilerTimer的話,則執行startProfilerTimer()stopProfilerTimerIfRunningAndRecordDelta(),用來記錄fiber節點執行work的實際開始時間(actualStartTime)和work時長(actualDuration)

詳細解析請看本文的「2、startProfilerTimer和stopProfilerTimerIfRunningAndRecordDelta」

stopWorkTimer()的做用是中止work計時,不是很重要,可不看

resetChildExpirationTime的做用是更新該節點的work時長和獲取優先級最高的子節點的expirationTime

詳細解析請看本文的「3、resetChildExpirationTime」

⑤ 若是next存在,則表示該節點在此次更新完成後,產生了新的更新,那麼就返回該next,並將其做爲completeUnitOfWork()的參數,再次執行

⑥ 接下來這一段比較重要,是 Effect 鏈的賦值,看個例子:
假設Span1有更新,Span2也有更新

那麼父節點DIVfirstEffectlastEffectSpan1執行completeUnitOfWork()後,會是下面這個樣子:

workInProgress1即表示Span1對應的fiber對象

當輪到Span2執行completeUnitOfWork()後,又會變成下面這個樣子:

也就是說:Effect鏈是幫助父節點簡單判斷子節點是否有更新及更新順序的

else的狀況就是執行更新的過程當中捕獲到error的狀況,此時執行的是unwindWork而不是completeWork,與completeWork最大的區別是有ShouldCapture的判斷,也是後續文章會講到

else後面的邏輯跟上面大同小異了,再也不贅述

以後是遍歷兄弟節點,返回父節點,再次遍歷,再也不贅述

⑧ 能夠看到,completeUnitOfWork主要作了三件事:
(1) 執行completeWork,完成節點更新
(2) 執行resetChildExpirationTime,獲取優先級最高的childExpirationTime
(3) 賦值Effect

2、startProfilerTimer和stopProfilerTimerIfRunningAndRecordDelta
做用:
記錄fiber節點執行work的實際開始時間(actualStartTime)和work時長

源碼:
startProfilerTimer()

//啓動分析器的timer,並賦成當前時間
function startProfilerTimer(fiber: Fiber): void {
  //若是不能啓動分析器的timer的話,就 return
  if (!enableProfilerTimer) {
    return;
  }
  //分析器的開始時間
  profilerStartTime = now();
  //若是 fiber 節點的實際開始時間 < 0 的話,則賦成當前時間
  if (((fiber.actualStartTime: any): number) < 0) {
    fiber.actualStartTime = now();
  }
}
複製代碼

stopProfilerTimerIfRunningAndRecordDelta()

//記錄分析器的timer的work 時間,並中止timer
function stopProfilerTimerIfRunningAndRecordDelta(
  fiber: Fiber,
  overrideBaseTime: boolean,
)
void 
{
  //若是不能啓動分析器的定時器的話,就 return
  if (!enableProfilerTimer) {
    return;
  }
  //若是分析器的開始時間>=0的話
  if (profilerStartTime >= 0) {
    //獲取運行的時間間隔
    const elapsedTime = now() - profilerStartTime;
    //累計實際 work 時間間隔
    fiber.actualDuration += elapsedTime;
    if (overrideBaseTime) {
      //記錄時間間隔
      fiber.selfBaseDuration = elapsedTime;
    }
    //上述操做完成後,將分析器的timer的開始時間重置爲-1
    profilerStartTime = -1;
  }
}
複製代碼

解析:
邏輯比較簡單,就不額外補充了

3、resetChildExpirationTime
做用:
更新該節點的work時長和獲取優先級最高的子節點的expirationTime

源碼:

//更新該節點的 work 時長和獲取優先級最高的子節點的 expirationTime
function resetChildExpirationTime(completedWork: Fiber{
  //若是當前渲染的節點須要更新,可是子節點不須要更新的話,則 return
  if (
    renderExpirationTime !== Never &&
    completedWork.childExpirationTime === Never
  ) {
    // The children of this component are hidden. Don't bubble their
    // expiration times.
    return;
  }

  let newChildExpirationTime = NoWork;

  // Bubble up the earliest expiration time.
  if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
    // In profiling mode, resetChildExpirationTime is also used to reset
    // profiler durations.
    //獲取當前節點的實際 work 時長
    let actualDuration = completedWork.actualDuration;
    //獲取 fiber 樹的 work 時長
    let treeBaseDuration = completedWork.selfBaseDuration;

    // When a fiber is cloned, its actualDuration is reset to 0. This value will
    // only be updated if work is done on the fiber (i.e. it doesn't bailout).
    // When work is done, it should bubble to the parent's actualDuration. If
    // the fiber has not been cloned though, (meaning no work was done), then
    // this value will reflect the amount of time spent working on a previous
    // render. In that case it should not bubble. We determine whether it was
    // cloned by comparing the child pointer.
    // 當一個 fiber 節點被克隆後,它的實際 work 時長被重置爲 0.
    // 這個值只會在 fiber 自身上的 work 完成時被更新(順利執行的話)
    // 當 fiber 自身 work 完成後,將自身的實際 work 時長冒泡賦給父節點的實際 work 時長
    // 若是 fiber 沒有被克隆,即 work 未被完成的話,actualDuration 反映的是上次渲染的實際 work 時長
    // 若是是這種狀況的話,不該該冒泡賦給父節點
    // React 經過比較 子指針 來判斷 fiber 是否被克隆

    // 關於 alternate 的做用,請看:https://juejin.im/post/5d5aa4695188257573635a0d
    // 是否將 work 時間冒泡至父節點的依據是:
    // (1) 該 fiber 節點是不是第一次渲染
    // (2) 該 fiber 節點的子節點有更新
    const shouldBubbleActualDurations =
      completedWork.alternate === null ||
      completedWork.child !== completedWork.alternate.child;

    //獲取當前節點的第一個子節點
    let child = completedWork.child;

    //當該子節點存在時,經過newChildExpirationTime來獲取子節點、子子節點二者中優先級最高的那個expirationTime
    while (child !== null) {
      //獲取該子節點的 expirationTime
      const childUpdateExpirationTime = child.expirationTime;
      //獲取該子節點的 child 的 expirationTime
      const childChildExpirationTime = child.childExpirationTime;
      //若是子節點的優先級大於NoWork的話,則將newChild的 expirationTime 賦值爲該子節點的 expirationTime
      if (childUpdateExpirationTime > newChildExpirationTime) {
        newChildExpirationTime = childUpdateExpirationTime;
      }
      //子節點的 child 同上
      if (childChildExpirationTime > newChildExpirationTime) {
        newChildExpirationTime = childChildExpirationTime;
      }

      if (shouldBubbleActualDurations) {
        //累計子節點的 work 時長
        actualDuration += child.actualDuration;
      }
      //累計 fiber 樹的 work 時長
      treeBaseDuration += child.treeBaseDuration;
      //移動到兄弟節點,重複上述過程
      child = child.sibling;
    }
    //更新 fiber 的 work 時長
    completedWork.actualDuration = actualDuration;
    //更新 fiber 樹的 work 時長
    completedWork.treeBaseDuration = treeBaseDuration;
  }
  //邏輯同上,再也不贅述
  else {
    let child = completedWork.child;
    while (child !== null) {
      const childUpdateExpirationTime = child.expirationTime;
      const childChildExpirationTime = child.childExpirationTime;
      if (childUpdateExpirationTime > newChildExpirationTime) {
        newChildExpirationTime = childUpdateExpirationTime;
      }
      if (childChildExpirationTime > newChildExpirationTime) {
        newChildExpirationTime = childChildExpirationTime;
      }
      child = child.sibling;
    }
  }

  completedWork.childExpirationTime = newChildExpirationTime;
}
複製代碼

解析:
(1) 將累計的子節點的work時長冒泡賦值到父節點的actualDuration

(2) 循環遍歷目標節點的子節點們,將子節點中優先級最高的expirationTime更新到目標及誒按的childExpirationTime

(3) 關於childExpirationTime的詳細解釋,請看:
React之childExpirationTime

4、GitHub
github.com/AttackXiaoJ…


(完)

相關文章
相關標籤/搜索