React源碼解析之commitRoot整體流程概覽

NO IMAGE
React源碼解析之commitRoot整體流程概覽

前言
React源碼解析之renderRoot概覽 中,renderRoot()的最後一段的switch...case即進入到了commit階段:

  switch (workInProgressRootExitStatus) {
    //...

    case RootErrored: {
      //...

      // If we're already rendering synchronously, commit the root in its
      // errored state.
      //commit階段
      return commitRoot.bind(null, root);
    }
    case RootSuspended: {
      //...

      // The work expired. Commit immediately.
      //commit階段
      return commitRoot.bind(null, root);
    }
    case RootSuspendedWithDelay: {
      //...

      // The work expired. Commit immediately.
      //commit階段
      return commitRoot.bind(null, root);
    }
    case RootCompleted: {
      // The work completed. Ready to commit.
      //...

      //commit階段
      return commitRoot.bind(null, root);
    }
    default: {
      invariant(false, 'Unknown root exit status.');
    }
  }

那本篇文章就來整體看下commitRoot()/commitRootImpl()的整體流程,之後的文章再細講內部各個function的源碼。

一、commitRoot()
作用:
① 以最高優先級去執行commitRootImpl()
② 如果有髒作用的話,用一個callback回調函數去清除掉它們

源碼:

function commitRoot(root) {
  //ImmediatePriority,優先級為 99,最高優先級,立即執行
  //bind函數,請看:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

  //獲取調度優先級,並臨時替換當前的優先級,去執行傳進來的 callback
  runWithPriority(ImmediatePriority, commitRootImpl.bind(null, root));
  // If there are passive effects, schedule a callback to flush them. This goes
  // outside commitRootImpl so that it inherits the priority of the render.
  //如果還有髒作用的話,用一個 callback 回調函數去清除掉它們
  //因為是在commitRootImpl()外執行的,所以會繼承 render 時的優先級
  if (rootWithPendingPassiveEffects !== null) {
    //獲取render 時的優先級
    //請看:[React源碼解析之scheduleWork(上)](https://juejin.im/post/5d7fa983f265da03cf7ac048)中的「五、getCurrentPriorityLevel()」
    const priorityLevel = getCurrentPriorityLevel();
    //對callback進行包裝處理,並更新調度隊列的狀態

    //請看[React源碼解析之scheduleWork(下)](https://juejin.im/post/5d885b75f265da03e83baaa7)中的[十、scheduleSyncCallback()]的解析
    scheduleCallback(priorityLevel, () => {
      //清除髒作用
      flushPassiveEffects();
      return null;
    });
  }
  return null;
}

解析:
(1) 執行runWithPriority(),獲取調度優先級,並臨時替換當前的優先級,去執行傳進來的 callback

由於ImmediatePriority是最高等級的優先級,所以會立即執行commitRootImpl()方法

runWithPriority()的源碼如下:

//獲取調度優先級,並臨時替換當前的優先級,去執行傳進來的 callback
export function runWithPriority<T>(
  reactPriorityLevel: ReactPriorityLevel,
  fn: () => T,
): T {
  //獲取調度優先級
  const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
  //臨時替換當前的優先級,去執行傳進來的 callback
  return Scheduler_runWithPriority(priorityLevel, fn);
}

reactPriorityToSchedulerPriority()的源碼如下:

//獲取調度優先級
function reactPriorityToSchedulerPriority(reactPriorityLevel) {
  switch (reactPriorityLevel) {
    case ImmediatePriority:
      return Scheduler_ImmediatePriority;
    case UserBlockingPriority:
      return Scheduler_UserBlockingPriority;
    case NormalPriority:
      return Scheduler_NormalPriority;
    case LowPriority:
      return Scheduler_LowPriority;
    case IdlePriority:
      return Scheduler_IdlePriority;
    default:
      invariant(false, 'Unknown priority level.');
  }
}

Scheduler_runWithPriority()unstable_runWithPriority(),源碼如下:

//臨時替換當前的優先級,去執行傳進來的 callback
function unstable_runWithPriority(priorityLevel, eventHandler) {
  //默認是 NormalPriority
  switch (priorityLevel) {
    case ImmediatePriority:
    case UserBlockingPriority:
    case NormalPriority:
    case LowPriority:
    case IdlePriority:
      break;
    default:
      priorityLevel = NormalPriority;
  }

  //緩存當前優先級 currentPriorityLevel
  var previousPriorityLevel = currentPriorityLevel;
  //臨時替換優先級,去執行 eventHandler()
  currentPriorityLevel = priorityLevel;
  //try 裡 return 了,還是會執行 finally 內的語句
  try {
    return eventHandler();
  } finally {
    //恢復當前優先級為之前的優先級
    currentPriorityLevel = previousPriorityLevel;
  }
}

(2) 執行完runWithPriority()後,如果還有髒作用的話,用一個callback回調函數去清除掉它們

getCurrentPriorityLevel()的作用是:獲取render時的優先級,
具體請看:
React源碼解析之scheduleWork(上)中的五、getCurrentPriorityLevel()

scheduleCallback()的作用是:對callback進行包裝處理,並更新調度隊列的狀態
具體請看:
React源碼解析之scheduleWork(下)中的十、scheduleSyncCallback()

③ 這個callback回調函數就是flushPassiveEffects(),作用是:清除髒作用
具體的源碼解析,我們在以後的文章裡講。

(3) commitRoot()的核心函數是commitRootImpl(),接下來就來看下它的整體結構及流程。

二、commitRootImpl()
作用:
(1) 根據effect鏈判斷是否進行commit
① 當執行commit時,進行before mutationmutationlayout三個子階段
② 否則快速過掉commit階段,走個 report 流程

(2) 判斷本次commit是否會產生新的更新,也就是髒作用,如果有髒作用則處理它

(3) 檢查目標fiber是否有剩餘的work要做
① 如果有剩餘的work的話,執行這些調度任務
② 沒有的話,說明也沒有報錯,清除「錯誤邊界」

(4) 刷新同步隊列

源碼:

function commitRootImpl(root) {
  //清除髒作用
  flushPassiveEffects();
  //dev 代碼可不看
  //flushRenderPhaseStrictModeWarningsInDEV();
  //flushSuspensePriorityWarningInDEV();

  //===context判斷====
  invariant(
    (executionContext & (RenderContext | CommitContext)) === NoContext,
    'Should not already be working.',
  );

  //調度完的任務
  const finishedWork = root.finishedWork;
  //調度完的優先級
  const expirationTime = root.finishedExpirationTime;
  //表示該節點沒有要更新的任務,直接 return
  if (finishedWork === null) {
    return null;
  }
  //賦值給變量 finishedWork、expirationTime 後重置成初始值
  //因為下面在對finishedWork、expirationTime 進行 commit後,任務就完成了
  root.finishedWork = null;
  root.finishedExpirationTime = NoWork;

  //error 判斷
  invariant(
    finishedWork !== root.current,
    'Cannot commit the same tree as before. This error is likely caused by ' +
      'a bug in React. Please file an issue.',
  );

  // commitRoot never returns a continuation; it always finishes synchronously.
  // So we can clear these now to allow a new callback to be scheduled.
  //commitRoot 是最後階段,不會再被異步調用了,所以會清除 callback 相關的屬性
  root.callbackNode = null;
  root.callbackExpirationTime = NoWork;

  //計時器,可跳過
  startCommitTimer();

  // Update the first and last pending times on this root. The new first
  // pending time is whatever is left on the root fiber.
  //目標節點的更新優先級
  const updateExpirationTimeBeforeCommit = finishedWork.expirationTime;
  //子節點的更新優先級,也就是所有子節點中優先級最高的任務
  //關於 childExpirationTime,請看:https://juejin.im/post/5dcdfee86fb9a01ff600fe1d
  const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime;
  //獲取優先級最高的 expirationTime
  const firstPendingTimeBeforeCommit =
    childExpirationTimeBeforeCommit > updateExpirationTimeBeforeCommit
      ? childExpirationTimeBeforeCommit
      : updateExpirationTimeBeforeCommit;
  //firstPendingTime即優先級最高的任務的 expirationTime
  root.firstPendingTime = firstPendingTimeBeforeCommit;
  //如果firstPendingTime<lastPendingTime的話,一般意味著所有的更新任務都已經完成了,更新lastPendingTime
  if (firstPendingTimeBeforeCommit < root.lastPendingTime) {
    // This usually means we've finished all the work, but it can also happen
    // when something gets downprioritized during render, like a hidden tree.
    root.lastPendingTime = firstPendingTimeBeforeCommit;
  }
  //如果目標節點root就是正在更新的節點 workInProgressRoot 的話
  //將相關值置為初始值,因為接下來會完成它的更新操作
  if (root === workInProgressRoot) {
    // We can reset these now that they are finished.
    workInProgressRoot = null;
    workInProgress = null;
    renderExpirationTime = NoWork;
  } else {
    // This indicates that the last root we worked on is not the same one that
    // we're committing now. This most commonly happens when a suspended root
    // times out.
  }

  // Get the list of effects.
  //獲取 effect 鏈
  let firstEffect;
  //如果RootFiber 的 effectTag 有值的話,也就是說RootFiber也要commit的話
  //將它的 finishedWork 也插入到 effect 鏈上,放到effect 鏈的最後 lastEffect.nextEffect 上
  if (finishedWork.effectTag > PerformedWork) {
    // A fiber's effect list consists only of its children, not itself. So if
    // the root has an effect, we need to add it to the end of the list. The
    // resulting list is the set that would belong to the root's parent, if it
    // had one; that is, all the effects in the tree including the root.
    if (finishedWork.lastEffect !== null) {
      finishedWork.lastEffect.nextEffect = finishedWork;
      firstEffect = finishedWork.firstEffect;
    } else {
      firstEffect = finishedWork;
    }
  } else {
    // There is no effect on the root.
    firstEffect = finishedWork.firstEffect;
  }

  //effect 鏈上第一個需要更新的 fiber 對象
  if (firstEffect !== null) {
    //=======context 相關,暫時跳過=========
    // const prevExecutionContext = executionContext;
    // executionContext |= CommitContext;
    // let prevInteractions: Set<Interaction> | null = null;
    // if (enableSchedulerTracing) {
    //   prevInteractions = __interactionsRef.current;
    //   __interactionsRef.current = root.memoizedInteractions;
    // }

    // Reset this to null before calling lifecycles
    ReactCurrentOwner.current = null;

    // The commit phase is broken into several sub-phases. We do a separate pass
    // of the effect list for each phase: all mutation effects come before all
    // layout effects, and so on.
    // 提交階段分為幾個子階段。我們對每個階段的效果列表進行單獨的遍歷:所有的mutation(突變)效果都在所有的layout效果之前

    // The first phase a "before mutation" phase. We use this phase to read the
    // state of the host tree right before we mutate it. This is where
    // getSnapshotBeforeUpdate is called.
    //第一個子階段是「在mutation突變之前」階段,在這個階段 React 會讀取 fiber 樹的 state 狀態,
    //也是用 getSnapshotBeforeUpdate 命名的原因

    //標記開始進行「before mutation」子階段了
    startCommitSnapshotEffectsTimer();
    //更新當前選中的DOM節點,一般為 document.activeElement || document.body
    prepareForCommit(root.containerInfo);
    nextEffect = firstEffect;
    //===========第一個 while 循環==============
    do {
      if (__DEV__) {
        //刪除了 dev 代碼
      } else {
        try {
          //調用 classComponent 上的生命週期方法 getSnapshotBeforeUpdate
          //關於getSnapshotBeforeUpdate,請看:https://zh-hans.reactjs.org/docs/react-component.html#getsnapshotbeforeupdate
          commitBeforeMutationEffects();
        } catch (error) {
          invariant(nextEffect !== null, 'Should be working on an effect.');
          captureCommitPhaseError(nextEffect, error);
          nextEffect = nextEffect.nextEffect;
        }
      }
    } while (nextEffect !== null);
    //標記「before mutation」子階段已經結束
    stopCommitSnapshotEffectsTimer();

    //======profiler相關,暫時跳過======
    if (enableProfilerTimer) {
      // Mark the current commit time to be shared by all Profilers in this
      // batch. This enables them to be grouped later.
      recordCommitTime();
    }

    // The next phase is the mutation phase, where we mutate the host tree.
    //標記開始進行「mutation」子階段了
    startCommitHostEffectsTimer();
    nextEffect = firstEffect;
    //=============第二個 while 循環=================
    do {
      if (__DEV__) {
        //刪除了 dev 代碼
      } else {
        try {
          //提交HostComponent的 side effect,也就是 DOM 節點的操作(增刪改)
          commitMutationEffects();
        } catch (error) {
          invariant(nextEffect !== null, 'Should be working on an effect.');
          captureCommitPhaseError(nextEffect, error);
          nextEffect = nextEffect.nextEffect;
        }
      }
    } while (nextEffect !== null);
    //標記「mutation」子階段已經結束
    stopCommitHostEffectsTimer();
    //當進行 DOM 操作時,比如刪除,可能會丟失選中 DOM 的焦點,此方法能保存丟失的值
    resetAfterCommit(root.containerInfo);

    // The work-in-progress tree is now the current tree. This must come after
    // the mutation phase, so that the previous tree is still current during
    // componentWillUnmount, but before the layout phase, so that the finished
    // work is current during componentDidMount/Update.

    //在「mutation」子階段後,正在進行的fiber樹(work-in-progress tree)就成了 current tree
    //以便在 componentWillUnmount 期間,保證 先前的 fiber 樹是 current tree
    //以便在「layout」子階段之前,保證 work-in-progress 的 finishedWork 是 current

    //沒看懂註釋,大概意思應該是隨著不同子階段的進行,及時更新 root.current,也就是當前的 fiber 樹更新成正在執行 commit 的 fiber 樹
    root.current = finishedWork;

    // The next phase is the layout phase, where we call effects that read
    // the host tree after it's been mutated. The idiomatic use case for this is
    // layout, but class component lifecycles also fire here for legacy reasons.
    //標記開始進行「layout」子階段了
    //這個階段會觸發所有組件的生命週期(lifecycles)的提交
    startCommitLifeCyclesTimer();
    nextEffect = firstEffect;
    //=============第三個 while 循環==========================
    do {
      if (__DEV__) {
        //刪除了 dev 代碼
      } else {
        try {
          //commit lifecycles,也就是觸發生命週期的 api
          commitLayoutEffects(root, expirationTime);
        } catch (error) {
          invariant(nextEffect !== null, 'Should be working on an effect.');
          captureCommitPhaseError(nextEffect, error);
          nextEffect = nextEffect.nextEffect;
        }
      }
    } while (nextEffect !== null);
    //標記「layout」子階段已經結束
    stopCommitLifeCyclesTimer();
    //正在 commit 的 effect 置為 null,表示 commit 結束
    nextEffect = null;

    // Tell Scheduler to yield at the end of the frame, so the browser has an
    // opportunity to paint.
    //React 佔用的資源已結束,告知瀏覽器可以去繪製 ui 了
    requestPaint();

    //=======暫時跳過=============
    if (enableSchedulerTracing) {
      __interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
    }
    executionContext = prevExecutionContext;
  }
  //如果 effect 鏈沒有需要更新的 fiber 對象
  else {
    // No effects.
    root.current = finishedWork;
    // Measure these anyway so the flamegraph explicitly shows that there were
    // no effects.
    // TODO: Maybe there's a better way to report this.

    //快速過掉 commit 階段,走個 report 流程
    startCommitSnapshotEffectsTimer();
    stopCommitSnapshotEffectsTimer();
    if (enableProfilerTimer) {
      recordCommitTime();
    }
    startCommitHostEffectsTimer();
    stopCommitHostEffectsTimer();
    startCommitLifeCyclesTimer();
    stopCommitLifeCyclesTimer();
  }
  //標記 commit 階段結束
  stopCommitTimer();
  //判斷本次 commit 是否會產生新的更新,也就是髒作用
  const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
  //如果有髒作用的處理
  if (rootDoesHavePassiveEffects) {
    // This commit has passive effects. Stash a reference to them. But don't
    // schedule a callback until after flushing layout work.
    rootDoesHavePassiveEffects = false;
    rootWithPendingPassiveEffects = root;
    pendingPassiveEffectsExpirationTime = expirationTime;
  }

  // Check if there's remaining work on this root
  //檢查是否有剩餘的 work
  const remainingExpirationTime = root.firstPendingTime;
  //如果有剩餘的 work 的話
  if (remainingExpirationTime !== NoWork) {
    //計算當前時間
    const currentTime = requestCurrentTime();
    //通過 expirationTime 推斷優先級
    const priorityLevel = inferPriorityFromExpirationTime(
      currentTime,
      remainingExpirationTime,
    );

    if (enableSchedulerTracing) {
      //render 階段衍生的 work,可能指新的 update 或者新的 error
      if (spawnedWorkDuringRender !== null) {
        const expirationTimes = spawnedWorkDuringRender;
        spawnedWorkDuringRender = null;
        //循環執行 scheduleInteractions
        for (let i = 0; i < expirationTimes.length; i++) {
          //與schedule的交互
          //請看:[React源碼解析之scheduleWork(上)](https://juejin.im/post/5d7fa983f265da03cf7ac048)中的「六、schedulePendingInteractions()」
          scheduleInteractions(
            root,
            expirationTimes[i],
            root.memoizedInteractions,
          );
        }
      }
    }
    // 同步調用callback
    // 流程是在root上存取callback和expirationTime,
    // 當新的callback調用時,比較更新expirationTime

    //請看:[React源碼解析之scheduleWork(下)](https://juejin.im/post/5d885b75f265da03e83baaa7)中的「八、scheduleCallbackForRoot()」
    scheduleCallbackForRoot(root, priorityLevel, remainingExpirationTime);
  }
  //如果沒有剩餘的 work 的話,說明 commit 成功,那麼就清除「錯誤邊界」的 list
  else {
    // If there's no remaining work, we can clear the set of already failed
    // error boundaries.
    legacyErrorBoundariesThatAlreadyFailed = null;
  }

  if (enableSchedulerTracing) {
    //當本次 commit 產生的髒作用被清除後,React就可以清除已經完成的交互
    if (!rootDidHavePassiveEffects) {
      // If there are no passive effects, then we can complete the pending interactions.
      // Otherwise, we'll wait until after the passive effects are flushed.
      // Wait to do this until after remaining work has been scheduled,
      // so that we don't prematurely signal complete for interactions when there's e.g. hidden work.

      //清除已經完成的交互,如果被 suspended 掛起的話,把交互留到後續呈現
      finishPendingInteractions(root, expirationTime);
    }
  }
  //devTools 相關的,可不看
  onCommitRoot(finishedWork.stateNode, expirationTime);

  //剩餘的 work 是同步任務的話
  if (remainingExpirationTime === Sync) {
    // Count the number of times the root synchronously re-renders without
    // finishing. If there are too many, it indicates an infinite update loop.

    //計算同步 re-render 重新渲染的次數,判斷是否是無限循環
    if (root === rootWithNestedUpdates) {
      nestedUpdateCount++;
    } else {
      nestedUpdateCount = 0;
      rootWithNestedUpdates = root;
    }
  } else {
    nestedUpdateCount = 0;
  }
  //如果捕獲到錯誤的話,就 throw error
  if (hasUncaughtError) {
    hasUncaughtError = false;
    const error = firstUncaughtError;
    firstUncaughtError = null;
    throw error;
  }

  //可不看
  if ((executionContext & LegacyUnbatchedContext) !== NoContext) {
    // This is a legacy edge case. We just committed the initial mount of
    // a ReactDOM.render-ed root inside of batchedUpdates. The commit fired
    // synchronously, but layout updates should be deferred until the end
    // of the batch.
    return null;
  }

  // If layout work was scheduled, flush it now.
  //「layout」階段的任務已經被調度的話,立即清除它

  //刷新同步任務隊列
  //請看:[React源碼解析之scheduleWork(下)](https://juejin.im/post/5d885b75f265da03e83baaa7)中的「十二、flushSyncCallbackQueue()」
  flushSyncCallbackQueue();
  return null;
}

解析:
雖然很長,但核心部分是那三個子階段,也就是三個do...while循環。
整體可以看成三個部分:
(1) 準備部分
(2) commit部分(三個子階段)
(3) 收尾部分


按源碼順序(有些我直接寫進註釋裡了,就不講了),看下commitRootImpl()做了些什麼:

(1) 執行flushPassiveEffects(),清除髒作用

(2) 根據目標節點的更新優先級expirationTime和子節點的更新優先級childExpirationTime,來比較獲取優先級最高的expirationTime,藉此來判斷是否所有render階段的work都已完成。

① 關於childExpirationTime,請看:
React之childExpirationTime

(3) 判斷目標 fiber自身是否也需要 commit,需要的話,則進行鏈表操作,把它的finishedWorkeffect鏈的最後——lastEffect.nextEffect

(4) 如果firstEffect不為 null 的話,說明有提交任務,則進行三個子階段
① 第一個子階段before mutation

執行commitBeforeMutationEffects(),本質是調用classComponent上的生命週期方法——getSnapshotBeforeUpdate()

關於getSnapshotBeforeUpdate的介紹及作用,請看:
zh-hans.reactjs.org/docs/react-…

② 第二個子階段mutation
執行commitMutationEffects(),作用是:提交HostComponentside effect,也就是DOM 節點的操作(增刪改)

關於HostComponent的相關知識,請看:
React源碼解析之HostComponent的更新(上)

React源碼解析之HostComponent的更新(下)

③ 第三個子階段layout
執行commitLayoutEffects(),作用是:觸發組件生命週期的api

(5) 如果firstEffect為 null 的話,說明effect鏈沒有需要更新的fiber對象,那麼就快速過掉 commit階段,走個 report 流程

所以會看到三組startCommitXXXTimer()endCommitXXXTimer()

(6) 至此,commit 基本結束了,但是 commit 階段可能也會產生新的 work,即remainingExpirationTime

當有剩餘的 work 的話,循環它們,依次執行scheduleInteractions(),排到調度任務中去,並通過scheduleCallbackForRoot()去執行它們

① 關於scheduleInteractions(),請看:
React源碼解析之scheduleWork(上)中的六、schedulePendingInteractions()

② 關於scheduleCallbackForRoot(),請看:
React源碼解析之scheduleWork(下)中的八、scheduleCallbackForRoot()

(7) 最後,執行flushSyncCallbackQueue(),刷新同步任務隊列

① 關於flushSyncCallbackQueue(),請看:
React源碼解析之scheduleWork(下)中的十二、flushSyncCallbackQueue()

(8) 最後,我在源碼上的每行都寫了註釋,希望能幫你熟悉commitRoot的整體流程。

GitHub
commitRoot()/commitRootImpl()
github.com/AttackXiaoJ…


React源碼解析之commitRoot整體流程概覽

(完)

相關文章

SameSite小識

徹底弄清元素的offsetHeight、scrollHeight、clientHeight…

代碼模板|我的代碼沒有else

自己動手開發一個Android持續集成工具5