React源碼解讀【三】任務調度

海從不平靜。他撞擊海岸,無休無止像顆年輕的心,在搜尋。——卡爾·桑德堡javascript

  • 微信公衆號 《JavaScript全棧
  • 掘金 《合一大師
  • Bilibili 《合一大師
  • 源碼Github地址:github.com/Walker-Leee…

每一個人,最終都會成爲歷史演化歷程中的炮灰。現在互聯網上各處充斥焦慮浮躁,萬事不能盡美,實爲惡性循環。java

React 設計體系如人類社會通常,撥動時間輪盤的那一刻,你便成了穿梭在輪片中的一粒細沙,角逐過程到處都須要亮出你的屬性,你重要嗎?你無可替代嗎?你有特殊權限嗎?沒有,那很差意思,請繼續在輪片中循環。屬於你的生命之火殆盡,前來悼念之人不少,這幕,像極了出生時的場景。react

幹啥玩意兒,這是技術文章不是抒情散文!下面進入正題。git

建立的準備上一節已經說明了,主要定義與更新相關的數據結構和變量,計算過時時間等。完成這些準備工做以後,正式進入調度工做,調度過程實現思路是:當與更新或掛載相關api被調用時,就會執行更新的邏輯,更新大體分爲如下幾個小階段github

scheduleWork

該步驟的主要工做有如下幾點api

  1. 經過 scheduleWorkOnParentPath 方法找到當前 Fiber 的root節點
  2. 遍歷當前更新節點父節點上的每一個節點,對比每一個節點的 expirationTime ,若是大於當前節點,則將其值賦值爲當前節點的 expirationTime 值。同時,childExpirationTime 的值也是該的邏輯
export function scheduleUpdateOnFiber(
  fiber: Fiber,
  expirationTime: ExpirationTime,
) {
  checkForNestedUpdates();
  warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber);

  const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
  if (root === null) {
    warnAboutUpdateOnUnmountedFiberInDEV(fiber);
    return;
  }

  checkForInterruption(fiber, expirationTime);
  recordScheduleUpdate();

  // TODO: computeExpirationForFiber also reads the priority. Pass the
  // priority as an argument to that function and this one.
  const priorityLevel = getCurrentPriorityLevel();

  if (expirationTime === Sync) {
    if (
      // Check if we're inside unbatchedUpdates (executionContext & LegacyUnbatchedContext) !== NoContext && // Check if we're not already rendering
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      // Register pending interactions on the root to avoid losing traced interaction data.
      schedulePendingInteractions(root, expirationTime);

      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root);
      schedulePendingInteractions(root, expirationTime);
      if (executionContext === NoContext) {
        flushSyncCallbackQueue();
      }
    }
  } else {
    ensureRootIsScheduled(root);
    schedulePendingInteractions(root, expirationTime);
  }

	...
}
export const scheduleWork = scheduleUpdateOnFiber;
複製代碼

若是過時時間等於咱們定義的Sync常量對應值,則進一步判斷此次更新的狀態,若是不是 batchUpdates 何時不是這個狀態呢?咱們前面認識過,好比reder時,判斷完這個狀態後還須要保證此次的更新渲染已準備好,則開始處理。不過處理以前,還要進行一個操做就是pending interaction,與咱們動做相關的內容數據須要保存於 pendingInteractionMap 中。bash

function scheduleInteractions(root, expirationTime, interactions) {
  if (!enableSchedulerTracing) {
    return;
  }

  if (interactions.size > 0) {
    const pendingInteractionMap = root.pendingInteractionMap;
    const pendingInteractions = pendingInteractionMap.get(expirationTime);
    if (pendingInteractions != null) {
      interactions.forEach(interaction => {
        if (!pendingInteractions.has(interaction)) {
          // Update the pending async work count for previously unscheduled interaction.
          interaction.__count++;
        }

        pendingInteractions.add(interaction);
      });
    } else {
      pendingInteractionMap.set(expirationTime, new Set(interactions));

      // Update the pending async work count for the current interactions.
      interactions.forEach(interaction => {
        interaction.__count++;
      });
    }

    const subscriber = __subscriberRef.current;
    if (subscriber !== null) {
      const threadID = computeThreadID(root, expirationTime);
      subscriber.onWorkScheduled(interactions, threadID);
    }
  }
}
複製代碼

通過以上處理,就能進入 performSyncWorkOnRoot 處理了微信

function performSyncWorkOnRoot(root) {
  // Check if there's expired work on this root. Otherwise, render at Sync.
  const lastExpiredTime = root.lastExpiredTime;
  const expirationTime = lastExpiredTime !== NoWork ? lastExpiredTime : Sync;
  if (root.finishedExpirationTime === expirationTime) {
    commitRoot(root);
  }
  ...
}
複製代碼

好了,到這裏一個expirationTimeSync 的且不是unbatchedUpdates,的調度就完成了,咱們發現這條流水線的操做仍是容易理解的,好,咱們如今進入另外一個分支,就是 batchedUpdatesmarkdown

ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
if (executionContext === NoContext) {
  // Flush the synchronous work now, unless we're already working or inside
  // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
  // scheduleCallbackForFiber to preserve the ability to schedule a callback
  // without immediately flushing it. We only do this for user-initiated
  // updates, to preserve historical behavior of legacy mode.
  flushSyncCallbackQueue();
}
複製代碼

首先須要確保一點,Root是否已經處理過調度相關工做,經過 ensureRootIsScheduled 方法爲root建立調度任務,且一個root只有一個task,假如某個root已經存在了任務,換言之已經調度過,那麼咱們須要從新爲這個task計算一些值。然後一樣有一個 schedulePendingInteractions ,用來處理交互引發的更新,方式與上面提到的 pending interaction 相似。數據結構

另外,若是executionContextNoContext ,則須要刷新用於處理同步更新的回調隊列 flushSyncCallbackQueue ,該方法定義在 SchedulerWithReactIntegration.js 中。

如此,周而復始,完成更新的調度過程,最終調用 performSyncWorkOnRoot ,進入下一階段

performSyncWorkOnRoot

一樣的選擇題,當前是否能直接去提交更新,yes or no ?

if (root.finishedExpirationTime === expirationTime) {
  // There's already a pending commit at this expiration time.
  // TODO: This is poorly factored. This case only exists for the
  // batch.commit() API.
  commitRoot(root);
}
複製代碼

這種狀況是不多的,通常會進入這個判斷的else,也就是

...
workLoopSync();
...

function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}
複製代碼

又開始了遍歷,這個遍歷中一樣有咱們上節分析過一些技巧,好比unitOfWork.alternate 用於節點屬性的對比與暫存

function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
  // 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 = unitOfWork.alternate;

  startWorkTimer(unitOfWork);
  setCurrentDebugFiberInDEV(unitOfWork);

  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    next = beginWork(current, unitOfWork, renderExpirationTime);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, renderExpirationTime);
  }

  resetCurrentDebugFiberInDEV();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    next = completeUnitOfWork(unitOfWork);
  }

  ReactCurrentOwner.current = null;
  return next;
}
複製代碼

能夠看到執行完相關操做後,隨着 beginWork 函數的調用正式進入更新階段。

beginWork

該部分主要的工做就是更新,更新什麼呢?咱們第一節講到 React 不一樣的組件使用?typeof 指定,針對這些不一樣類型的組件,定義了各自的處理方法,咱們以經常使用的 ClassComponent 爲例。

function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {
  const updateExpirationTime = workInProgress.expirationTime;
  ...
複製代碼

然後首先判斷當前的更新節點是否爲空,若不爲空,則執行相關邏輯

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

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      // Force a re-render if the implementation changed due to hot reload:
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      // If props or context changed, mark the fiber as having performed work.
      // This may be unset if the props are determined to be equal later (memo).
      didReceiveUpdate = true;
    } else if (updateExpirationTime < renderExpirationTime) {
      didReceiveUpdate = false;
      ...
複製代碼

此刻略知一二,先後props是否發生更改?根據不一樣的條件判斷爲 didReceiveUpdate 賦值。然後根據當前 workInProgress 的tag值判斷當前的節點對應組件類型是什麼,根據不一樣類型,進入不一樣方法進行處理。

switch (workInProgress.tag) {
	...
}
複製代碼

然後,一樣根據該tag,執行更新組件邏輯

case ClassComponent: {
  const Component = workInProgress.type;
  const unresolvedProps = workInProgress.pendingProps;
  const resolvedProps =
        workInProgress.elementType === Component
  ? unresolvedProps
  : resolveDefaultProps(Component, unresolvedProps);
  return updateClassComponent(
    current,
    workInProgress,
    Component,
    resolvedProps,
    renderExpirationTime,
  );
}
複製代碼

reconcileChildren

更新組件過程當中,若是還有子節點,須要調度並更新

export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderExpirationTime: ExpirationTime, ) {
  if (current === null) {
    // If this is a fresh new component that hasn't been rendered yet, we
    // won't update its child set by applying minimal side-effects. Instead,
    // we will add them all to the child before it gets rendered. That means
    // we can optimize this reconciliation pass by not tracking side-effects.
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderExpirationTime,
    );
  } else {
    // If the current child is the same as the work in progress, it means that
    // we haven't yet started any work on these children. Therefore, we use
    // the clone algorithm to create a copy of all the current children.

    // If we had any progressed work already, that is invalid at this point so
    // let's throw it out.
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderExpirationTime,
    );
  }
}
複製代碼

其子節點的 Fiber 調度定義在 ReactChildFiber.js 中,這裏不展開了。

commitRoot

輪迴中完成以上調度過程,也該到了提交更新的時候了,該方法咱們在剛開始就講到了,那時略過,如今拾起。

function commitRoot(root) {
  const renderPriorityLevel = getCurrentPriorityLevel();
  runWithPriority(
    ImmediatePriority,
    commitRootImpl.bind(null, root, renderPriorityLevel),
  );
  return null;
}
複製代碼

具體的實如今 commitRootImpl 方法中,該方法調用 prepareForCommit 爲更新作準備,最終根據更新的類型不一樣使用不一樣策略進行更新

let primaryEffectTag =
      effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
	case Placement: {
    commitPlacement(nextEffect);
      // Clear the "placement" from effect tag so that we know that this is
      // inserted, before any life-cycles like componentDidMount gets called.
      // TODO: findDOMNode doesn't rely on this any more but isMounted does
      // and isMounted is deprecated anyway so we should be able to kill this.
      nextEffect.effectTag &= ~Placement;
      break;
    }
  case PlacementAndUpdate: {
    // Placement
    commitPlacement(nextEffect);
    // Clear the "placement" from effect tag so that we know that this is
    // inserted, before any life-cycles like componentDidMount gets called.
    nextEffect.effectTag &= ~Placement;

    // Update
    const current = nextEffect.alternate;
    commitWork(current, nextEffect);
    break;
  }
  case Hydrating: {
    nextEffect.effectTag &= ~Hydrating;
    break;
  }
  case HydratingAndUpdate: {
    nextEffect.effectTag &= ~Hydrating;

    // Update
    const current = nextEffect.alternate;
    commitWork(current, nextEffect);
    break;
  }
  case Update: {
    const current = nextEffect.alternate;
    commitWork(current, nextEffect);
    break;
  }
  case Deletion: {
    commitDeletion(root, nextEffect, renderPriorityLevel);
    break;
  }
}
複製代碼

提交更新相關的處理定義於 ReactFiberCommitWork.js 一樣也要藉助 tag,作不一樣策略的處理。

至此完成了任務調度的全部工做,固然在後面的過程,事件相關的處理是隻字未提,React最新源碼對於事件系統作了很大改動,咱們放在後面章節詳細講解。React 源碼設計之精妙沒法言盡,而且只是略讀,完成本系列的粗略講解後,後續會有更深刻源碼講解。讀源碼爲了什麼?

  1. 理解咱們天天使用的框架工做原理
  2. 學習做者NB的設計和對於代碼極致的追求,運用到本身的項目中

好了,廢話很少說,若是看了不懂請移步公衆號或者B站查看視頻講解。若是有意見或疑問,請私信聯繫。

我是合一,英雄再會。

相關文章
相關標籤/搜索