React16源碼解析(四)-Scheduler

React源碼解析系列文章歡迎閱讀:
React16源碼解析(一)- 圖解Fiber架構
React16源碼解析(二)-建立更新
React16源碼解析(三)-ExpirationTime
React16源碼解析(四)-Scheduler
React16源碼解析(五)-更新流程渲染階段1
React16源碼解析(六)-更新流程渲染階段2
React16源碼解析(七)-更新流程渲染階段3
React16源碼解析(八)-更新流程提交階段
正在更新中...node

在 React16源碼解析(二)-建立更新 這篇文章的最後,三種類型的更新最後都調用 scheduleWork 進入了任務調度。react

// current爲RootFiber
scheduleWork(current, expirationTime)

設計思想

一、React將全部任務按照過時時間從小到大排列,數據結構採用雙向循環鏈表。
二、任務鏈表的執行準則:當前幀先執行瀏覽器的渲染等任務,若是當前幀還有空閒時間,則執行任務,直到當前幀的時間用完。若是當前幀已經沒有空閒時間,就等到下一幀的空閒時間再去執行。注意,若是當前幀沒有空閒時間可是當前任務鏈表有任務到期了或者有當即執行任務,那麼必須執行的時候就以丟失幾幀的代價,執行這些任務。執行完的任務都會被從鏈表中刪除。segmentfault

核心功能

一、維護時間片
二、模擬瀏覽器 requestldleCallback API
三、調度列表和超時判斷api

基礎知識

閱讀本文須要具有的基礎知識:
一、window.requestAnimationFrame
二、window.MessageChannel
三、鏈表操做瀏覽器

不會的童鞋能夠先去了解哦~ 我這裏就不詳細介紹了。性能優化

進入正題 開始解讀

ReactScheduler.png

咱們從以前的scheduleWork講起。數據結構

全局變量

這裏面用到了大量的全局變量,我在這裏進行羅列,下面的講解遇到全局變量能夠到這裏來查看:架構

isWorking:commitRoot和renderRoot開始都會設置爲true,而後在他們各自階段結束的時候都重置爲false。用來標誌是否當前有更新正在進行,不區分階段。

nextRoot:用於記錄下一個將要渲染的root節點

nextRenderExpirationTime:下一個要渲染的任務的ExpirationTime

firstScheduledRoot & lastScheduledRoot:用於存放有任務的全部root的單列表結構。在findHighestPriorityRoot用來檢索優先級最高的root,在addRootToSchedule中會修改。

callbackExpirationTime & callbackID:callbackExpirationTime記錄請求ReactScheduler的時候用的過時時間,若是在一次調度期間有新的調度請求進來了,並且優先級更高,那麼須要取消上一次請求,若是更低則無需再次請求調度。callbackID是ReactScheduler返回的用於取消調度的 ID。

nextFlushedRoot & nextFlushedExpirationTime:用來標誌下一個須要渲染的root和對應的expirtaionTime,注意:經過findHighestPriorityRoot找到最高優先級的,經過flushRoot會直接設置指定的,不進行篩選

scheduleWork(計劃任務)

咱們更新完 fiber的 updateQueue以後,就調用 scheduleWork 開始調度此次的工做。scheduleWork 主要的事情就是找到咱們要處理的 root設置剛纔獲取到的執行優先級,而後調用 requestWork。app

一、找到更新對應的FiberRoot節點(scheduleWorkToRoot)按照樹的結構經過fiber.return一層層的返回,直到找到根節點。在向上找的過程當中不斷的更新每一個節點對應的fiber對象的childExpirationTime。而且alternate同步更新。
注:childExpirationTime子樹中最高優先級的expirationTime。less

二、存在上一個任務,而且上一個執行沒有執行完,執行權交給了瀏覽器,發現當前更新的優先級高於上一個任務,則重置stack(resetStack)
注:resetStack會從nextUnitOfWork開始一步一步往上恢復,能夠說前一個任務執行的那一半白作了~由於如今有更高優先級的任務來插隊了!你說氣不氣,可是世界就是這麼殘忍。

三、OK上面的2符合條件以後,若是如今不處於render階段,或者nextRoot !== root,則做爲享受vip待遇的任務能夠請求調度了:requestWork。
注:若是正在處於render階段,咱們就不須要請求調度了,由於render階段會處理掉這個update。

function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
  // 獲取FiberRoot
  const root = scheduleWorkToRoot(fiber, expirationTime);
  if (root === null) {
    return;
  }

  // 這個分支表示高優先級任務打斷低優先級任務
  // 這種狀況發生於如下場景:有一個優先級較低的任務(必然是異步任務)沒有執行完,
  // 執行權交給了瀏覽器,這個時候有一個新的高優先級任務進來了
  // 這時候須要去執行高優先級任務,因此須要打斷低優先級任務
  if (
    !isWorking &&
    nextRenderExpirationTime !== NoWork &&
    expirationTime < nextRenderExpirationTime
  ) {
    // 記錄被誰打斷的
    interruptedBy = fiber;
    // 重置 stack
    resetStack();
  }
  // ......
  if (
    // If we're in the render phase, we don't need to schedule this root
    // for an update, because we'll do it before we exit...
    !isWorking ||
    isCommitting ||
    // ...unless this is a different root than the one we're rendering.
    nextRoot !== root
  ) {
    const rootExpirationTime = root.expirationTime;
    // 請求任務
    requestWork(root, rootExpirationTime);
  }

  // 在某些生命週期函數中 setState 會形成無限循環
  // 這裏是告知你的代碼觸發無限循環了
  if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
    // Reset this back to zero so subsequent updates don't throw.
    nestedUpdateCount = 0;
    invariant(
      false,
      'Maximum update depth exceeded. This can happen when a ' +
        'component repeatedly calls setState inside ' +
        'componentWillUpdate or componentDidUpdate. React limits ' +
        'the number of nested updates to prevent infinite loops.',
    );
  }
}

requestWork(請求任務)

一、將Root加入到Schedule(addRootToSchedule),若是此root已經調度過(已經在scheduledRoot的單向鏈表中),可能更新root.expirationTime。
它維護了一條 scheduledRoot 的單向鏈表,好比說 lastScheduleRoot == null,意味着咱們當前已經沒有要處理的 root,這時候就把 firstScheduleRoot、lastScheduleRoot、root.nextScheduleRoot 都設置爲 root。若是 lastScheduleRoot !== null,則把 lastScheduledRoot.nextScheduledRoot設置爲root,等 lastScheduledRoot調度完就會開始處理當前 root。

二、是不是同步任務?是:performSyncWork 否:scheduleCallbackWithExpirationTime

function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
  // 將Root加入到Schedule,更新root.expirationTime
  addRootToSchedule(root, expirationTime);
  if (isRendering) {
    // Prevent reentrancy. Remaining work will be scheduled at the end of
    // the currently rendering batch.
    return;
  }

  // 判斷是否須要批量更新
  // 當咱們觸發事件回調時,其實回調會被 batchedUpdates 函數封裝一次
  // 這個函數會把 isBatchingUpdates 設爲 true,也就是說咱們在事件回調函數內部
  // 調用 setState 不會立刻觸發 state 的更新及渲染,只是單純建立了一個 updater,而後在這個分支 return 了
  // 只有當整個事件回調函數執行完畢後恢復 isBatchingUpdates 的值,而且執行 performSyncWork
  // 想必不少人知道在相似 setTimeout 中使用 setState 之後 state 會立刻更新,若是你想在定時器回調中也實現批量更新,
  // 就可使用 batchedUpdates 將你須要的代碼封裝一下
  if (isBatchingUpdates) {
    // Flush work at the end of the batch.
    // 判斷是否不須要批量更新
    if (isUnbatchingUpdates) {
      // ...unless we're inside unbatchedUpdates, in which case we should
      // flush it now.
      nextFlushedRoot = root;
      nextFlushedExpirationTime = Sync;
      performWorkOnRoot(root, Sync, true);
    }
    return;
  }

  // TODO: Get rid of Sync and use current time?
  // 判斷優先級是同步仍是異步,異步的話須要調度
  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    // 函數核心是實現了 requestIdleCallback 的 polyfill 版本
    // 由於這個函數瀏覽器的兼容性不好
    // 具體做用能夠查看 MDN 文檔 https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
    // 這個函數可讓瀏覽器空閒時期依次調用函數,這就可讓開發者在主事件循環中執行後臺或低優先級的任務,
    // 並且不會對像動畫和用戶交互這樣延遲敏感的事件產生影響
    scheduleCallbackWithExpirationTime(root, expirationTime);
  }
}

function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) {
  // Add the root to the schedule.
  // Check if this root is already part of the schedule.
  // 判斷 root 是否調度過
  if (root.nextScheduledRoot === null) {
    // This root is not already scheduled. Add it.
    // root 沒有調度過
    root.expirationTime = expirationTime;
    if (lastScheduledRoot === null) {
      firstScheduledRoot = lastScheduledRoot = root;
      root.nextScheduledRoot = root;
    } else {
      lastScheduledRoot.nextScheduledRoot = root;
      lastScheduledRoot = root;
      lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
    }
  } else {
    // This root is already scheduled, but its priority may have increased.
    // root 已經調度過,判斷是否須要更新優先級
    const remainingExpirationTime = root.expirationTime;
    if (
      remainingExpirationTime === NoWork ||
      expirationTime < remainingExpirationTime
    ) {
      // Update the priority.
      root.expirationTime = expirationTime;
    }
  }
}

scheduleCallbackWithExpirationTime

一、若是有一個callback已經在調度(callbackExpirationTime !== NoWork )的狀況下,優先級大於當前callback(expirationTime > callbackExpirationTime),函數直接返回。若是優先級小於當前callback,就取消它的callback(cancelDeferredCallback(callbackID))

二、計算出timeout而後scheduleDeferredCallback(performAsyncWork, {timeout})

function scheduleCallbackWithExpirationTime(
  root: FiberRoot,
  expirationTime: ExpirationTime,
) {
  // 判斷上一個 callback 是否執行完畢
  if (callbackExpirationTime !== NoWork) {
    // A callback is already scheduled. Check its expiration time (timeout).
    // 當前任務若是優先級小於上個任務就退出
    if (expirationTime > callbackExpirationTime) {
      // Existing callback has sufficient timeout. Exit.
      return;
    } else {
      // 不然的話就取消上個 callback
      if (callbackID !== null) {
        // Existing callback has insufficient timeout. Cancel and schedule a
        // new one.
        cancelDeferredCallback(callbackID);
      }
    }
    // The request callback timer is already running. Don't start a new one.
  } else {
    // 沒有須要執行的上一個 callback,開始定時器,這個函數用於 devtool
    startRequestCallbackTimer();
  }

  callbackExpirationTime = expirationTime;
  // 當前 performance.now() 和程序剛執行時的 performance.now() 相減
  const currentMs = now() - originalStartTimeMs;
  // 轉化成 ms
  const expirationTimeMs = expirationTimeToMs(expirationTime);
  // 當前任務的延遲過時時間,由過時時間 - 當前任務建立時間得出,超過期表明任務過時須要強制更新
  const timeout = expirationTimeMs - currentMs;
  // 生成一個 callbackID,用於關閉任務
  callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
}

scheduleDeferredCallback

scheduleDeferredCallback 函數在是:Scheduler.js中的unstable_scheduleCallback
一、建立一個任務節點newNode,按照優先級插入callback鏈表
二、咱們把任務按照過時時間排好順序了,那麼什麼時候去執行任務呢?怎麼去執行呢?答案是有兩種狀況,1是當添加第一個任務節點的時候開始啓動任務執行,2是當新添加的任務取代以前的節點成爲新的第一個節點的時候。由於1意味着任務從無到有,應該 馬上啓動。2意味着來了新的優先級最高的任務,應該中止掉以前要執行的任務,從新重新的任務開始執行。上面兩種狀況就對應ensureHostCallbackIsScheduled方法執行的兩種狀況。

function unstable_scheduleCallback(callback, deprecated_options) {
  var startTime =
    currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();

  // 這裏其實只會進第一個 if 條件,由於外部寫死了必定會傳 deprecated_options.timeout
  // 越小優先級越高,同時也表明一個任務的過時時間
  var expirationTime;
  if (
    typeof deprecated_options === 'object' &&
    deprecated_options !== null &&
    typeof deprecated_options.timeout === 'number'
  ) {
    // FIXME: Remove this branch once we lift expiration times out of React.
    expirationTime = startTime + deprecated_options.timeout;
  } else {
    switch (currentPriorityLevel) {
      case ImmediatePriority:
        expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
        break;
      case UserBlockingPriority:
        expirationTime = startTime + USER_BLOCKING_PRIORITY;
        break;
      case IdlePriority:
        expirationTime = startTime + IDLE_PRIORITY;
        break;
      case NormalPriority:
      default:
        expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
    }
  }

  // 環形雙向鏈表結構
  var newNode = {
    callback,
    priorityLevel: currentPriorityLevel,
    expirationTime,
    next: null,
    previous: null,
  };

  // Insert the new callback into the list, ordered first by expiration, then
  // by insertion. So the new callback is inserted any other callback with
  // equal expiration.
  // 核心思路就是 firstCallbackNode 優先級最高 lastCallbackNode 優先級最低
  // 新生成一個 newNode 之後,就從頭開始比較優先級
  // 若是新的高,就把新的往前插入,不然就日後插,直到沒有一個 node 的優先級比他低
  // 那麼新的節點就變成 lastCallbackNode
  // 在改變了firstCallbackNode的狀況下,須要從新調度
  if (firstCallbackNode === null) {
    // This is the first callback in the list.
    firstCallbackNode = newNode.next = newNode.previous = newNode;
    ensureHostCallbackIsScheduled();
  } else {
    var next = null;
    var node = firstCallbackNode;
    do {
      if (node.expirationTime > expirationTime) {
        // The new callback expires before this one.
        next = node;
        break;
      }
      node = node.next;
    } while (node !== firstCallbackNode);

    if (next === null) {
      // No callback with a later expiration was found, which means the new
      // callback has the latest expiration in the list.
      next = firstCallbackNode;
    } else if (next === firstCallbackNode) {
      // The new callback has the earliest expiration in the entire list.
      firstCallbackNode = newNode;
      ensureHostCallbackIsScheduled();
    }

    var previous = next.previous;
    previous.next = next.previous = newNode;
    newNode.next = next;
    newNode.previous = previous;
  }

  return newNode;
}

ensureHostCallbackIsScheduled

一、判斷是否已經存在有host callback,若是已經存cancelHostCallback(),而後開始requestHostCallback(flushWork, expirationTime),傳入flushWork就是沖刷任務的函數(隨後講解)和隊首的任務節點的過時時間。這裏咱們沒有立馬執行flushWork,而是交給了requestHostCallback。由於咱們並不想直接把任務鏈表中的任務立馬執行掉,也不是一口氣把鏈表中的全部任務所有都執行掉。JS是單線程的,咱們執行這些任務一直佔據着主線程,會致使瀏覽器的其餘任務一直等待,好比動畫,就會出現卡頓,因此咱們要選擇合適的時期去執行它。因此咱們交給requestHostCallback去處理這件事情,把flushWork交給了它。這裏你能夠暫時把flushWork簡單的想成執行鏈表中的任務。

注:這裏咱們想一想,咱們須要保證應用的流暢性,由於瀏覽器是一幀一幀渲染的,每一幀渲染結束以後會有一些空閒時間能夠執行別的任務,那麼咱們就想利用這點空閒時間來執行咱們的任務。這樣咱們立馬想到一個原生api: requestIdleCallback。但因爲某些緣由,react團隊放棄了這個api,轉而利用requestAnimationFrame和MessageChannel pollyfill了一個requestIdleCallback。

function ensureHostCallbackIsScheduled() {
  // 調度正在執行 返回 也就是不能打斷已經在執行的
  if (isExecutingCallback) {
    // Don't schedule work yet; wait until the next time we yield.
    return;
  }
  // Schedule the host callback using the earliest expiration in the list.
  // 讓優先級最高的 進行調度 若是存在已經在調度的 直接取消
  var expirationTime = firstCallbackNode.expirationTime;
  if (!isHostCallbackScheduled) {
    isHostCallbackScheduled = true;
  } else {
    // Cancel the existing host callback.
    // 取消正在調度的callback
    cancelHostCallback();
  }
  // 發起調度
  requestHostCallback(flushWork, expirationTime);
}

requestHostCallback

一、這裏有兩個全局變量scheduledHostCallback、timeoutTime會被賦值,
分別表明第一個任務的callback和過時時間。
二、進入這個函數就會立馬判斷一下當前的任務是否過時,若是過時了,啥也別說了,趕忙去立馬執行啊,管他瀏覽器空不空閒,瀏覽器你沒得空也得趕忙給我執行了,這個任務是甲方提的,交付期限都過了,那還不趕忙的給辦了,甲方爸爸是上帝啊。這裏留一個疑問:是直接執行咱們以前傳入進來的flushWork嗎?
三、若是任務沒有過時,交付時間還沒到,那沒事慢慢來,瀏覽器有空了咋們在作,畢竟咱們都很忙,能拖就拖吧。因此不緊急的任務,咱們交給requestAnimationFrameWithTimeout(animationTick)。

requestHostCallback = function(callback, absoluteTimeout) {
    scheduledHostCallback = callback;
    timeoutTime = absoluteTimeout;
    // isFlushingHostCallback 只在 channel.port1.onmessage 被設爲 true
    // isFlushingHostCallback表示所添加的任務須要當即執行
    // 也就是說當正在執行任務或者新進來的任務已通過了過時時間
    // 立刻執行新的任務,再也不等到下一幀
    if (isFlushingHostCallback || absoluteTimeout < 0) {
      // Don't wait for the next frame. Continue working ASAP, in a new event.
      // 發送消息,channel.port1.onmessage 會監聽到消息並執行
      window.postMessage(messageKey, '*');
    } else if (!isAnimationFrameScheduled) {
      // If rAF didn't already schedule one, we need to schedule a frame.
      // TODO: If this rAF doesn't materialize because the browser throttles, we
      // might want to still have setTimeout trigger rIC as a backup to ensure
      // that we keep performing work.
      // isAnimationFrameScheduled 設爲 true 的話就不會再進這個分支了
      // 可是內部會有機制確保 callback 執行
      isAnimationFrameScheduled = true;
      requestAnimationFrameWithTimeout(animationTick);
    }
  };

requestAnimationFrameWithTimeout

這個函數其實能夠理解爲優化後的requestAnimationFrame。

一、當咱們調用requestAnimationFrameWithTimeout並傳入一個callback的時候,會啓動一個requestAnimationFrame和一個setTimeout,二者都會去執行callback。但因爲requestAnimationFrame執行優先級相對較高,它內部會調用clearTimeout取消下面定時器的操做。因此在頁面active狀況下的表現跟requestAnimationFrame是一致的。

二、requestAnimationFrame在頁面切換到未激活的時候是不工做的,這時requestAnimationFrameWithTimeout就至關於啓動了一個100ms的定時器,接管任務的執行工做。這個執行頻率不高也不低,既能不影響cpu能耗,又能保證任務能有必定效率的執行。

稍等一下,咱們之前使用requestAnimationFrame的時候,是須要循環調用本身的,否則不就只執行了一次…..它在哪裏遞歸調用的呢? 咱們在仔細觀察,這個函數傳入了一個參數callback,這個callback是上一個函數傳入進來的animationTick,這是什麼東東?沒見過啊?

var ANIMATION_FRAME_TIMEOUT = 100;
var rAFID;
var rAFTimeoutID;
var requestAnimationFrameWithTimeout = function(callback) {
  // schedule rAF and also a setTimeout
  // 這裏的 local 開頭的函數指的是 request​Animation​Frame 及 setTimeout
  // request​Animation​Frame 只有頁面在前臺時纔會執行回調
  // 若是頁面在後臺時就不會執行回調,這時候會經過 setTimeout 來保證執行 callback
  // 兩個回調中均可以互相 cancel 定時器
  // callback 指的是 animationTick
  rAFID = localRequestAnimationFrame(function(timestamp) {
    // cancel the setTimeout
    localClearTimeout(rAFTimeoutID);
    callback(timestamp);
  });
  rAFTimeoutID = localSetTimeout(function() {
    // cancel the requestAnimationFrame
    localCancelAnimationFrame(rAFID);
    callback(getCurrentTime());
  }, ANIMATION_FRAME_TIMEOUT);
};

animationTick

一、有任務再進行遞歸請求下一幀,沒任務的話能夠結束了,退出遞歸。
二、這裏有幾個比較重要的全局變量:
frameDeadline 初始值爲0,計算當前幀的截止時間
activeFrameTime 初始值爲33 ,一幀的渲染時間33ms,這裏假設 1s 30幀
var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
rafTime是傳入這個函數的參數,也就是當前幀開始的時間戳。nextFrameTime就表明實際上一幀的渲染時間(第一次執行除外)。以後會根據這個值更新activeFrameTime
。動態的根據不一樣的環境調每一幀的渲染時間,達到系統的刷新頻率。
三、在每一幀的回調函數最後,都會調用window.postMessage(messageKey, ‘’);啥?這是個啥?不是應該調用flushWork來執行任務嗎?還有咱們上面提到的一個疑問,requestHostCallback裏面若是任務過時,立馬執行任務。他執行的是flushWork嗎?咱們去瞧一瞧:在以前的requestHostCallback函數中,瞪大眼睛一看:window.postMessage(messageKey, ''); What???他執行的也是這個方法。

var animationTick = function(rafTime) {
    if (scheduledHostCallback !== null) {
      // Eagerly schedule the next animation callback at the beginning of the
      // frame. If the scheduler queue is not empty at the end of the frame, it
      // will continue flushing inside that callback. If the queue *is* empty,
      // then it will exit immediately. Posting the callback at the start of the
      // frame ensures it's fired within the earliest possible frame. If we
      // waited until the end of the frame to post the callback, we risk the
      // browser skipping a frame and not firing the callback until the frame
      // after that.
      // scheduledHostCallback 不爲空的話就繼續遞歸
      // 可是注意這裏的遞歸併非同步的,下一幀的時候纔會再執行 animationTick
      requestAnimationFrameWithTimeout(animationTick);
    } else {
      // No pending work. Exit.
      isAnimationFrameScheduled = false;
      return;
    }
    // rafTime 就是 performance.now(),不管是執行哪一個定時器
    // 假如咱們應用第一次執行 animationTick,那麼 frameDeadline = 0 activeFrameTime = 33
    // 也就是說此時 nextFrameTime = performance.now() + 33
    // 便於後期計算,咱們假設 nextFrameTime = 5000 + 33 = 5033
    // 而後 activeFrameTime 爲何是 33 呢?由於 React 這裏假設你的刷新率是 30hz
    // 一秒對應 1000 毫秒,1000 / 30 ≈ 33
    // ------------------------------- 如下注釋是第二次的
    // 第二次進來這裏執行,由於 animationTick 回調確定是下一幀執行的,假如咱們屏幕是 60hz 的刷新率
    // 那麼一幀的時間爲 1000 / 60 ≈ 16
    // 此時 nextFrameTime = 5000 + 16 - 5033 + 33 = 16
    // ------------------------------- 如下注釋是第三次的
    // nextFrameTime = 5000 + 16 * 2 - 5048 + 33 = 17
    var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
    // 這個 if 條件第一次確定進不去
    // ------------------------------- 如下注釋是第二次的
    // 此時 16 < 33 && 5033 < 33 = false,也就是說第二幀的時候這個 if 條件仍是進不去
    // ------------------------------- 如下注釋是第三次的
    // 此時 17 < 33 && 16 < 33 = true,進條件了,也就是說若是刷新率大於 30hz,那麼得等兩幀纔會調整 activeFrameTime
    if (
      nextFrameTime < activeFrameTime &&
      previousFrameTime < activeFrameTime
    ) {
      // 這裏小於 8 的判斷,是由於不能處理大於 120 hz 刷新率以上的瀏覽器了
      if (nextFrameTime < 8) {
        // Defensive coding. We don't support higher frame rates than 120hz.
        // If the calculated frame time gets lower than 8, it is probably a bug.
        nextFrameTime = 8;
      }
      // If one frame goes long, then the next one can be short to catch up.
      // If two frames are short in a row, then that's an indication that we
      // actually have a higher frame rate than what we're currently optimizing.
      // We adjust our heuristic dynamically accordingly. For example, if we're
      // running on 120hz display or 90hz VR display.
      // Take the max of the two in case one of them was an anomaly due to
      // missed frame deadlines.
      // 第三幀進來之後,activeFrameTime = 16 < 17 ? 16 : 17 = 16
      // 而後下次就按照一幀 16 毫秒來算了
      activeFrameTime =
        nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
    } else {
      // 第一次進來 5033
      // 第二次進來 16
      previousFrameTime = nextFrameTime;
    }
    //  第一次 frameDeadline = 5000 + 33 = 5033
    // ------------------------------- 如下注釋是第二次的
    // frameDeadline = 5016 + 33 = 5048
    frameDeadline = rafTime + activeFrameTime;
    // 確保這一幀內再也不 postMessage
    // postMessage 屬於宏任務
    // const channel = new MessageChannel();
    // const port = channel.port2;
    // channel.port1.onmessage = function(event) {
    //   console.log(1)
    // }
    // requestAnimationFrame(function (timestamp) {
    //   setTimeout(function () {
    //     console.log('setTimeout')
    //   }, 0)
    //   port.postMessage(undefined)
    //   Promise.resolve(1).then(function (value) {
    //     console.log(value, 'Promise')
    //   })
    // })
    // 以上代碼輸出順序爲 Promise -> onmessage -> setTimeout
    // 由此可知微任務最早執行,而後是宏任務,而且在宏任務中也有順序之分
    // onmessage 會優先於 setTimeout 回調執行
    // 對於瀏覽器來講,當咱們執行 request​Animation​Frame 回調後
    // 會先讓頁面渲染,而後判斷是否要執行微任務,最後執行宏任務,而且會先執行 onmessage
    // 固然其實比 onmessage 更快的宏任務是 set​Immediate,可是這個 API 只能在 IE 下使用
    if (!isMessageEventScheduled) {
      isMessageEventScheduled = true;
      window.postMessage(messageKey, '*');
    }
  };

window.postMessage(messageKey, '*')

一、其實咱們想一個問題,咱們想要的是在每一幀裏面,先執行瀏覽器的渲染任務,若是把這一幀的渲染任務執行以後,還有空閒的時間,咱們在執行咱們的任務。
二、可是若是這裏直接開始執行任務的話,會在這一幀的一開始就執行,難道你想要霸佔一幀的時間來執行你的任務嗎?那豈不是我上面講的白講了……
三、因此咱們使用window.postMessage,他是macrotask,onmessage的回調函數的調用時機是在一幀的paint完成以後,react scheduler內部正是利用了這一點來在一幀渲染結束後的剩餘時間來執行任務的。
四、window.postMessage(messageKey, '*')對應的window.addEventListener('message', idleTick, false)的監聽,會觸發idleTick函數的調用。
四、因此接下來咋們瞧瞧idleTick,咱們的任務確定是在這個事件回調中執行的。

var messageKey =
    '__reactIdleCallback$' +
    Math.random()
      .toString(36)
      .slice(2);
  var idleTick = function(event) {
    if (event.source !== window || event.data !== messageKey) {
      return;
    }
    // 一些變量的設置
    isMessageEventScheduled = false;

    var prevScheduledCallback = scheduledHostCallback;
    var prevTimeoutTime = timeoutTime;
    scheduledHostCallback = null;
    timeoutTime = -1;
    // 獲取當前時間
    var currentTime = getCurrentTime();

    var didTimeout = false;
    // 判斷以前計算的時間是否小於當前時間,時間超了說明瀏覽器渲染等任務執行時間超過一幀了,這一幀沒有空閒時間了
    if (frameDeadline - currentTime <= 0) {
      // There's no time left in this idle period. Check if the callback has
      // a timeout and whether it's been exceeded.
      // 判斷當前任務是否過時
      if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
        // Exceeded the timeout. Invoke the callback even though there's no
        // time left.
        didTimeout = true;
      } else {
        // No timeout.
        // 沒過時的話再丟到下一幀去執行
        if (!isAnimationFrameScheduled) {
          // Schedule another animation callback so we retry later.
          isAnimationFrameScheduled = true;
          requestAnimationFrameWithTimeout(animationTick);
        }
        // Exit without invoking the callback.
        scheduledHostCallback = prevScheduledCallback;
        timeoutTime = prevTimeoutTime;
        return;
      }
    }

    // 最後執行 flushWork,這裏涉及到的 callback 全是 flushWork
    if (prevScheduledCallback !== null) {
      isFlushingHostCallback = true;
      try {
        prevScheduledCallback(didTimeout);
      } finally {
        isFlushingHostCallback = false;
      }
    }
  };

flushWork

你們能夠想想,這個flushWork會是一個簡單的把任務鏈表從頭至尾執行完嗎?要是這樣的話,我上面bb的一大堆豈不是又白講了……都一口氣執行完了,還談何性能優化呢。一口氣回到解放前。因此,不是咱們想象的這麼簡單哦。
一、flushWork根據didTimeout參數有兩種處理邏輯,若是爲true,就會把任務鏈表裏的過時任務全都給執行一遍;若是爲false則在當前幀到期以前儘量多的去執行任務。
二、最後,若是還有任務的話,再啓動一輪新的任務執行調度,ensureHostCallbackIsScheduled(),來重置callback鏈表。重置全部的調度常量,老 callback 就不會被執行。
三、這裏的執行任務是調用flushFirstCallback,執行callback中優先級最高的任務

function flushWork(didTimeout) {
  // 一些變量的設置
  isExecutingCallback = true;
  deadlineObject.didTimeout = didTimeout;
  try {
    // 判斷是否超時
    if (didTimeout) {
      // Flush all the expired callbacks without yielding.
      while (firstCallbackNode !== null) {
        // Read the current time. Flush all the callbacks that expire at or
        // earlier than that time. Then read the current time again and repeat.
        // This optimizes for as few performance.now calls as possible.
        // 超時的話,獲取當前時間,判斷任務是否過時,過時的話就執行任務
        // 而且判斷下一個任務是否也已通過期
        var currentTime = getCurrentTime();
        if (firstCallbackNode.expirationTime <= currentTime) {
          do {
            flushFirstCallback();
          } while (
            firstCallbackNode !== null &&
            firstCallbackNode.expirationTime <= currentTime
          );
          continue;
        }
        break;
      }
    } else {
      // Keep flushing callbacks until we run out of time in the frame.
      // 沒有超時說明還有時間能夠執行任務,執行任務完成後繼續判斷
      if (firstCallbackNode !== null) {
        do {
          flushFirstCallback();
        } while (
          firstCallbackNode !== null &&
          getFrameDeadline() - getCurrentTime() > 0
        );
      }
    }
  } finally {
    isExecutingCallback = false;
    if (firstCallbackNode !== null) {
      // There's still work remaining. Request another callback.
      ensureHostCallbackIsScheduled();
    } else {
      isHostCallbackScheduled = false;
    }
    // Before exiting, flush all the immediate work that was scheduled.
    flushImmediateWork();
  }
}

flushFirstCallback

這裏就是鏈表操做,執行完firstCallback後把這個callback從鏈表中刪除。

這裏調用的是當前任務節點flushedNode.callback,那咱們這個callback是啥呢?時間開始倒流,回到scheduleCallbackWithExpirationTime函數scheduleDeferredCallback(performAsyncWork, {timeout})相信你們對這個還有印象,它其實就是咱們進入Scheduler.js的入口函數。如它傳入performAsyncWork做爲回調函數,也就是在此函數中調用的回調函數就是這個。

function flushFirstCallback() {
  var flushedNode = firstCallbackNode;

  // Remove the node from the list before calling the callback. That way the
  // list is in a consistent state even if the callback throws.
  // 鏈表操做
  var next = firstCallbackNode.next;
  if (firstCallbackNode === next) {
    // This is the last callback in the list.
    // 當前鏈表中只有一個節點
    firstCallbackNode = null;
    next = null;
  } else {
    // 有多個節點,從新賦值 firstCallbackNode,用於以前函數中下一次的 while 判斷
    var lastCallbackNode = firstCallbackNode.previous;
    firstCallbackNode = lastCallbackNode.next = next;
    next.previous = lastCallbackNode;
  }

  // 清空指針
  flushedNode.next = flushedNode.previous = null;

  // Now it's safe to call the callback.
  // 這個 callback 是 performAsyncWork 函數
  var callback = flushedNode.callback;
  var expirationTime = flushedNode.expirationTime;
  var priorityLevel = flushedNode.priorityLevel;
  var previousPriorityLevel = currentPriorityLevel;
  var previousExpirationTime = currentExpirationTime;
  currentPriorityLevel = priorityLevel;
  currentExpirationTime = expirationTime;
  var continuationCallback;
  try {
    // 執行回調函數
    continuationCallback = callback(deadlineObject);
  } finally {
    currentPriorityLevel = previousPriorityLevel;
    currentExpirationTime = previousExpirationTime;
  }
  // ......
}

這裏有個地方要注意,在調用任務的callback的時候咱們傳入了一個對象:deadlineObject。
timeRemaining:當前幀還有多少空閒時間
didTimeout:任務是否過時

var deadlineObject = {
  timeRemaining,
  didTimeout: false,
};

這個deadlineObject是個全局對象,主要用於shouldYield函數
函數中的deadline就是這個對象

function shouldYield() {
  if (deadlineDidExpire) {
    return true;
  }
  if (
    deadline === null ||
    deadline.timeRemaining() > timeHeuristicForUnitOfWork
  ) {
    // Disregard deadline.didTimeout. Only expired work should be flushed
    // during a timeout. This path is only hit for non-expired work.
    return false;
  }
  deadlineDidExpire = true;
  return true;
}

performAsyncWork

一、這個函數獲得一個參數dl,這個參數就是以前調用回調函數傳入的deadlineObject。
二、調用performWork(NoWork, dl);第一個參數爲minExpirationTime這裏傳入NoWork=0,第二個參數Deadline=dl。

function performAsyncWork(dl) {
  // 判斷任務是否過時
  if (dl.didTimeout) {
    // The callback timed out. That means at least one update has expired.
    // Iterate through the root schedule. If they contain expired work, set
    // the next render expiration time to the current time. This has the effect
    // of flushing all expired work in a single batch, instead of flushing each
    // level one at a time.
    if (firstScheduledRoot !== null) {
      recomputeCurrentRendererTime();
      let root: FiberRoot = firstScheduledRoot;
      do {
        didExpireAtExpirationTime(root, currentRendererTime);
        // The root schedule is circular, so this is never null.
        root = (root.nextScheduledRoot: any);
      } while (root !== firstScheduledRoot);
    }
  }
  performWork(NoWork, dl);
}

到這裏須要插一句,還記得 requestWork 中若是是同步的狀況嗎?退到這個函數咱們瞧瞧,若是是同步的狀況,直接調用performSyncWork。performSyncWork和performAsyncWork長得如此相像,莫非是失散多年的親兄弟?去到performSyncWork去看看,嗯…沒錯,他和performAsyncWork調用了同一個方法,只是參數傳遞的不同,performWork(Sync, null);,他傳入的第一個參數爲Sync=1。第二個參數爲null。

在requestWork函數中:

if (expirationTime === Sync) {
    // 同步
    performSyncWork();
  } else {
    // 異步,開始調度
    scheduleCallbackWithExpirationTime(root, expirationTime);
  }
function performSyncWork() {
  performWork(Sync, null);
}

performWork(執行任務)

一、若是是同步(deadline == null),壓根不考慮幀渲染是否有空餘時間,同步任務也沒有過時時間之說,遍歷全部的root,而且把全部root中同步的任務所有執行掉。
注:有可能存在多個root,即有可能屢次調用了ReactDOM.render。
二、若是是異步(deadline !== null),遍歷全部的root,執行完全部root中的過時任務,由於過時任務是必需要執行的。若是這一幀還有空閒時間,儘量的執行更多任務。
三、上面兩種狀況都執行了任務,看看他們調用了什麼方法呢?performWorkOnRoot。

// currentRendererTime 計算從頁面加載到如今爲止的毫秒數
// currentSchedulerTime 也是加載到如今的時間,isRendering === true的時候用做固定值返回,否則每次requestCurrentTime都會從新計算新的時間
function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
  // 這裏注意deadline指向了傳進來的deadlineObject對象(dl)
  deadline = dl;

  // Keep working on roots until there's no more work, or until we reach
  // the deadline.
  // 找到優先級最高的下一個須要渲染的 root: nextFlushedRoot 和對應的 expirtaionTime: nextFlushedExpirationTime
  findHighestPriorityRoot();

  // 異步
  if (deadline !== null) {
    // 從新計算 currentRendererTime
    recomputeCurrentRendererTime();
    currentSchedulerTime = currentRendererTime;

    // ......

    while (
      nextFlushedRoot !== null &&
      nextFlushedExpirationTime !== NoWork &&
      (minExpirationTime === NoWork ||
        minExpirationTime >= nextFlushedExpirationTime) &&
        // deadlineDidExpire 判斷時間片是否過時, shouldYield 中判斷
        // 當前渲染時間 currentRendererTime 比較 nextFlushedExpirationTime 判斷任務是否已經超時
        // currentRendererTime >= nextFlushedExpirationTime 超時了
      (!deadlineDidExpire || currentRendererTime >= nextFlushedExpirationTime)
    ) {
      performWorkOnRoot(
        nextFlushedRoot,
        nextFlushedExpirationTime,
        currentRendererTime >= nextFlushedExpirationTime,
      );
      findHighestPriorityRoot();
      recomputeCurrentRendererTime();
      currentSchedulerTime = currentRendererTime;
    }
  } else {
    // 同步
    while (
      nextFlushedRoot !== null &&
      nextFlushedExpirationTime !== NoWork &&
      // 普通狀況 minExpirationTime 應該就等於nextFlushedExpirationTime 由於都來自同一個 root,nextFlushedExpirationTime 是在 findHighestPriorityRoot 階段讀取出來的 root.expirationTime
      (minExpirationTime === NoWork ||
        minExpirationTime >= nextFlushedExpirationTime)
    ) {
      performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, true);
      findHighestPriorityRoot();
    }
  }

  // We're done flushing work. Either we ran out of time in this callback,
  // or there's no more work left with sufficient priority.

  // If we're inside a callback, set this to false since we just completed it.
  if (deadline !== null) {
    callbackExpirationTime = NoWork;
    callbackID = null;
  }
  // If there's work left over, schedule a new callback.
  if (nextFlushedExpirationTime !== NoWork) {
    scheduleCallbackWithExpirationTime(
      ((nextFlushedRoot: any): FiberRoot),
      nextFlushedExpirationTime,
    );
  }

  // Clean-up.
  deadline = null;
  deadlineDidExpire = false;

  finishRendering();
}

performWorkOnRoot

一、首先說明執行任務的兩個階段:
renderRoot 渲染階段
completeRoot 提交階段
二、若是是同步或者任務已通過期的狀況下,先renderRoot(傳入參數isYieldy=false,表明任務不能夠中斷),隨後completeRoot
三、若是是異步的話,先renderRoot(傳入參數isYieldy=true,表明任務能夠中斷),完了以後看看這一幀是否還有空餘時間,若是有的話completeRoot,沒有時間了的話,只能等下一幀了。
四、在二、3步調用renderRoot以前還會作一件事,判斷 finishedWork !== null ,由於前一個時間片可能 renderRoot 結束了沒時間 completeRoot,若是在這個時間片中有完成 renderRoot 的 finishedWork 就直接 completeRoot。

function performWorkOnRoot(
  root: FiberRoot,
  expirationTime: ExpirationTime,
  isExpired: boolean,
) {
  // ......

  isRendering = true;

  // Check if this is async work or sync/expired work.
  if (deadline === null || isExpired) {
    // 同步或者任務已通過期,不可打斷任務
    // Flush work without yielding.
    // TODO: Non-yieldy work does not necessarily imply expired work. A renderer
    // may want to perform some work without yielding, but also without
    // requiring the root to complete (by triggering placeholders).

    // 判斷是否存在已完成的 finishedWork,存在話就完成它
    let finishedWork = root.finishedWork;
    if (finishedWork !== null) {
      // This root is already complete. We can commit it.
      completeRoot(root, finishedWork, expirationTime);
    } else {
      root.finishedWork = null;
      // If this root previously suspended, clear its existing timeout, since
      // we're about to try rendering again.
      const timeoutHandle = root.timeoutHandle;
      if (timeoutHandle !== noTimeout) {
        root.timeoutHandle = noTimeout;
        // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
        cancelTimeout(timeoutHandle);
      }
      const isYieldy = false;
      // 不然就去渲染成 DOM
      renderRoot(root, isYieldy, isExpired);
      finishedWork = root.finishedWork;
      if (finishedWork !== null) {
        // We've completed the root. Commit it.
        completeRoot(root, finishedWork, expirationTime);
      }
    }
  } else {
    // 異步任務未過時,可打斷任務
    // Flush async work.
    let finishedWork = root.finishedWork;
    if (finishedWork !== null) {
      // This root is already complete. We can commit it.
      completeRoot(root, finishedWork, expirationTime);
    } else {
      root.finishedWork = null;
      // If this root previously suspended, clear its existing timeout, since
      // we're about to try rendering again.
      const timeoutHandle = root.timeoutHandle;
      if (timeoutHandle !== noTimeout) {
        root.timeoutHandle = noTimeout;
        // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
        cancelTimeout(timeoutHandle);
      }
      const isYieldy = true;
      renderRoot(root, isYieldy, isExpired);
      finishedWork = root.finishedWork;
      if (finishedWork !== null) {
        // We've completed the root. Check the deadline one more time
        // before committing.
        if (!shouldYield()) {
          // Still time left. Commit the root.
          completeRoot(root, finishedWork, expirationTime);
        } else {
          // There's no time left. Mark this root as complete. We'll come
          // back and commit it later.
          root.finishedWork = finishedWork;
        }
      }
    }
  }

  isRendering = false;
}

renderRoot & completeRoot

以後就進入了組件更新的這兩個階段,後續章節詳細講解。

文章若有不妥,歡迎指正~

相關文章
相關標籤/搜索