<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入"時(shí)間管理大師" —— React Scheduler

          共 24572字,需瀏覽 50分鐘

           ·

          2021-06-05 19:12

          眾所周知 React 的愿景就是快速響應(yīng)用戶, 讓用戶覺(jué)得夠快, 不能阻塞用戶的交互. 而 Scheduler 作為 React 的調(diào)度中樞, 通過(guò)劃分優(yōu)先級(jí), 時(shí)間切片, 可中斷、可恢復(fù)任務(wù)等策略來(lái)保證高優(yōu)任務(wù)先被執(zhí)行, 以提高性能. 可謂"時(shí)間管理大師", 羅志祥本祥了.

          什么是 Scheduler

          Scheduler[1] 是內(nèi)置于 React 項(xiàng)目下的一個(gè)包, 你只需要將任務(wù)以及任務(wù)的優(yōu)先級(jí)交給它, 它就可以幫你進(jìn)行任務(wù)的協(xié)調(diào)調(diào)度. 目前 Scheduler 只被用于 React, 但團(tuán)隊(duì)的愿景是希望它能夠更通用化.

          Scheduler 用來(lái)做什么

          Scheduler 從宏觀和微觀對(duì)任務(wù)進(jìn)行管控. 宏觀上, 也就是對(duì)于多個(gè)任務(wù), Scheduler 根據(jù)優(yōu)先級(jí)來(lái)安排執(zhí)行順序; 而對(duì)于單個(gè)任務(wù)(微觀上), 需要"有節(jié)制"的執(zhí)行. 什么是"有節(jié)制"呢? 我們知道 JavaScript 是單線程的, 如果一個(gè)同步任務(wù)占用時(shí)間很長(zhǎng), 就會(huì)導(dǎo)致掉幀和卡頓. 因此需要把一個(gè)耗時(shí)的任務(wù)及時(shí)中斷掉, 去執(zhí)行更重要的任務(wù)(比如用戶交互), 后續(xù)再執(zhí)行該耗時(shí)任務(wù), 如此往復(fù). Scheduler 就是用這樣的模式, 將任務(wù)細(xì)粒度切分, 來(lái)避免一直占用有限的資源執(zhí)行耗時(shí)較長(zhǎng)的任務(wù), 實(shí)現(xiàn)更快的響應(yīng).

          原理綜述

          為了實(shí)現(xiàn)多個(gè)任務(wù)的管理 和 單個(gè)任務(wù)的控制, Scheduler 引入了兩個(gè)概念: 任務(wù)優(yōu)先級(jí)時(shí)間片. 任務(wù)優(yōu)先級(jí)讓任務(wù)按照自身的緊急程度排序, 這樣可以讓優(yōu)先級(jí)最高的任務(wù)最先被執(zhí)行到. 時(shí)間片規(guī)定的是單個(gè)任務(wù)在這一幀內(nèi)最大的執(zhí)行時(shí)間(yieldInterval = 5ms), 任務(wù)一旦執(zhí)行時(shí)間超過(guò)時(shí)間片, 則會(huì)被打斷, 轉(zhuǎn)而去執(zhí)行更高優(yōu)的任務(wù), 這樣可以保證頁(yè)面不會(huì)因?yàn)槿蝿?wù)執(zhí)行時(shí)間過(guò)長(zhǎng)而產(chǎn)生掉幀或者影響用戶交互.

          多個(gè)任務(wù)的管理

          在 Scheduler 中, 任務(wù)被分成了兩種: 未過(guò)期的任務(wù)已過(guò)期的任務(wù), 分別存儲(chǔ)在 timerQueue 和 taskQueue 兩個(gè)隊(duì)列中.

          如何區(qū)分兩種任務(wù)

          通過(guò)任務(wù)的開(kāi)始時(shí)間(startTime) 和 當(dāng)前時(shí)間(currentTime) 比較:

          ?當(dāng) startTime > currentTime, 說(shuō)明未過(guò)期, 存到 timerQueue?當(dāng) startTime <= currentTime, 說(shuō)明已過(guò)期, 存到 taskQueue

          入隊(duì)的任務(wù)如何排序

          即便是區(qū)分了 timerQueue 和 taskQueue, 但每個(gè)隊(duì)列中的任務(wù)也是有不同優(yōu)先級(jí)的, 因此在入隊(duì)時(shí)需要根據(jù)緊急程度將緊急的任務(wù)排在前面. 老版本的 React Scheduler 使用循環(huán)鏈表來(lái)串聯(lián), 代碼比較難懂, 這里不展開(kāi).

          目前源碼中使用小頂堆[2]這個(gè)數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn), 堆是優(yōu)先隊(duì)列[3]的底層實(shí)現(xiàn), 它在插入或者刪除元素的時(shí)候, 通過(guò)"上浮"和"下沉"操作來(lái)使元素自動(dòng)排序(優(yōu)先隊(duì)列經(jīng)常用來(lái)解決算法中 topK[4] 問(wèn)題). 需要注意的是, 堆的元素存儲(chǔ)在數(shù)組中, 而非鏈?zhǔn)浇Y(jié)構(gòu). 關(guān)于二叉堆相關(guān)的邏輯本文不去展開(kāi), 有興趣可以參考我學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)與算法[5]的倉(cāng)庫(kù).


          回到源碼, 當(dāng)我們插入任務(wù)時(shí), timerQueue 和 taskQueue 能保證元素是從小到大排序的. 那排序的依據(jù)是什么呢?

          ?timerQueue 中, 依據(jù)任務(wù)的開(kāi)始時(shí)間(startTime)排序, 開(kāi)始時(shí)間越早, 說(shuō)明會(huì)越早開(kāi)始, 開(kāi)始時(shí)間小的排在前面. 任務(wù)進(jìn)來(lái)的時(shí)候, 開(kāi)始時(shí)間默認(rèn)是當(dāng)前時(shí)間, 如果進(jìn)入調(diào)度的時(shí)候傳了延遲時(shí)間, 開(kāi)始時(shí)間則是當(dāng)前時(shí)間與延遲時(shí)間的和.?taskQueue 中, 依據(jù)任務(wù)的過(guò)期時(shí)間(expirationTime)排序, 過(guò)期時(shí)間越早, 說(shuō)明越緊急, 過(guò)期時(shí)間小的排在前面. 過(guò)期時(shí)間根據(jù)任務(wù)優(yōu)先級(jí)計(jì)算得出, 優(yōu)先級(jí)越高, 過(guò)期時(shí)間越早.

          任務(wù)的執(zhí)行

          ?對(duì)于 taskQueue, 因?yàn)槔锩娴娜蝿?wù)已經(jīng)過(guò)期了, 需要在 workLoop 中循環(huán)執(zhí)行完這些任務(wù)?對(duì)于 timerQueue, 它里面的任務(wù)都不會(huì)立即執(zhí)行, 但在 workLoop 方法中會(huì)通過(guò) advanceTimers 方法來(lái)檢測(cè)第一個(gè)任務(wù)是否過(guò)期, 如果過(guò)期了, 就放到 taskQueue 中.

          相較于單個(gè)任務(wù)的執(zhí)行(馬上會(huì)說(shuō)到), 任務(wù)隊(duì)列的管理屬于宏觀層面的范疇. 從 react-reconciler 計(jì)算的 Lane, 會(huì)被轉(zhuǎn)化成 Scheduler 可識(shí)別的任務(wù)優(yōu)先級(jí), 然后通過(guò)它去管理任務(wù)隊(duì)列中的任務(wù)順序. 總之來(lái)講, 就是越緊急的任務(wù), 它就需要被優(yōu)先處理.

          單個(gè)任務(wù)的中斷及恢復(fù)

          在循環(huán) taskQueue 執(zhí)行每一個(gè)任務(wù)時(shí), 如果某個(gè)任務(wù)執(zhí)行時(shí)間過(guò)長(zhǎng), 達(dá)到了時(shí)間片限制的時(shí)間, 那么該任務(wù)必須中斷, 以便于讓位給更重要的事情(如瀏覽器繪制), 等高優(yōu)過(guò)期任務(wù)完成了, 再恢復(fù)執(zhí)行該任務(wù). Scheduler 要實(shí)現(xiàn)這樣的調(diào)度效果需要兩個(gè)角色: 任務(wù)的調(diào)度者任務(wù)的執(zhí)行者. 調(diào)度者調(diào)度一個(gè)執(zhí)行者, 執(zhí)行者去循環(huán) taskQueue, 逐個(gè)執(zhí)行任務(wù). 當(dāng)某個(gè)任務(wù)的執(zhí)行時(shí)間比較長(zhǎng), 執(zhí)行者會(huì)根據(jù)時(shí)間片中斷任務(wù)執(zhí)行, 然后告訴調(diào)度者: 我現(xiàn)在正執(zhí)行的這個(gè)任務(wù)被中斷了, 還有一部分沒(méi)完成, 但現(xiàn)在必須讓位給更重要的事情, 你再調(diào)度一個(gè)執(zhí)行者吧, 好讓這個(gè)任務(wù)能在之后被繼續(xù)執(zhí)行完(任務(wù)的恢復(fù)). 于是, 調(diào)度者知道了任務(wù)還沒(méi)完成, 需要繼續(xù)做, 它會(huì)再調(diào)度一個(gè)執(zhí)行者去繼續(xù)完成這個(gè)任務(wù). 通過(guò)執(zhí)行者和調(diào)度者的配合, 可以實(shí)現(xiàn)任務(wù)的中斷和恢復(fù). 其實(shí)將任務(wù)掛起與恢復(fù)并不是一個(gè)新潮的概念, 它有一個(gè)名詞叫做協(xié)程[6], ES6 之后的生成器, 就可以用 yield 關(guān)鍵字來(lái)模擬協(xié)程的概念.

          源碼解析

          以上就是 Scheduler 的核心原理, talk is cheap, 想要真正搞懂, 還是得深入源碼才行. 我切了個(gè)分支專門來(lái)讀React 源碼[7], 看完下面的內(nèi)容可以再去 GayHub 上整體復(fù)習(xí)下.

          React 和 Scheduler 優(yōu)先級(jí)的轉(zhuǎn)換

          我們知道 React 的優(yōu)先級(jí)采用的是 Lane 模型, 而 Scheduler 是一個(gè)獨(dú)立的包, 有自己的一套優(yōu)先級(jí)機(jī)制, 因此需要做一個(gè)轉(zhuǎn)換. 這里摘錄 react-reconciler/src/ReactFiberWorkLoop.old(new).js 中的一部分.

          let newCallbackNode;
          // 同步
          if (newCallbackPriority === SyncLane) {
          // 執(zhí)行 scheduleSyncCallback 方法
          // 只不過(guò)要區(qū)分下 legacy 模式還是 concurrent 模式
          // scheduleSyncCallback 自己有個(gè) syncQueue, 用來(lái)承載同步任務(wù)
          // 并交由 flushSyncCallbacks 處理這些同步任務(wù)后, 再交由下面 scheduleCallback
          // 以最高優(yōu)先級(jí)讓 Scheduler 調(diào)度
          if (root.tag === LegacyRoot) {
          scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
          } else {
          scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
          }

          // 這里我們只談 scheduleCallback, 即以最高優(yōu)先級(jí)
          // ImmediateSchedulerPriority 來(lái)執(zhí)行同步任務(wù)
          if (supportsMicrotasks) {
          scheduleMicrotask(flushSyncCallbacks);
          } else {
          scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
          }
          newCallbackNode = null;
          } else {
          // 異步
          let schedulerPriorityLevel;
          // 需要將 lane 轉(zhuǎn)換為 Scheduler 可識(shí)別的優(yōu)先級(jí)
          switch (lanesToEventPriority(nextLanes)) {
          case DiscreteEventPriority:
          schedulerPriorityLevel = ImmediateSchedulerPriority;
          break;
          case ContinuousEventPriority:
          schedulerPriorityLevel = UserBlockingSchedulerPriority;
          break;
          case DefaultEventPriority:
          schedulerPriorityLevel = NormalSchedulerPriority;
          break;
          case IdleEventPriority:
          schedulerPriorityLevel = IdleSchedulerPriority;
          break;
          default:
          schedulerPriorityLevel = NormalSchedulerPriority;
          break;
          }
          // 通過(guò) scheduleCallback 將任務(wù)及其優(yōu)先級(jí)傳入到 Scheduler 中
          newCallbackNode = scheduleCallback(
          schedulerPriorityLevel,
          performConcurrentWorkOnRoot.bind(null, root)
          );
          }

          Scheduler 中的優(yōu)先級(jí)

          Scheduler 自身維護(hù) 6 種優(yōu)先級(jí), 不過(guò)翻了一遍源碼 NoPriority 沒(méi)被用過(guò). 它們是計(jì)算 expirationTime 的重要依據(jù), 而我們知道 expirationTime 事關(guān) taskQueue 的排序. 該文件位于 scheduler/src/SchedulerPriorities.js.

          export const NoPriority = 0; // 沒(méi)有任何優(yōu)先級(jí)
          export const ImmediatePriority = 1; // 立即執(zhí)行的優(yōu)先級(jí), 級(jí)別最高
          export const UserBlockingPriority = 2; // 用戶阻塞級(jí)別的優(yōu)先級(jí), 比如用戶輸入, 拖拽這些
          export const NormalPriority = 3; // 正常的優(yōu)先級(jí)
          export const LowPriority = 4; // 低優(yōu)先級(jí)
          export const IdlePriority = 5; // 最低階的優(yōu)先級(jí), 可以被閑置的那種

          scheduleCallback

          通過(guò)上面的介紹, 我們知道 Scheduler 的主入口是 scheduleCallback, 它負(fù)責(zé)生成調(diào)度任務(wù), 根據(jù)任務(wù)是否過(guò)期將任務(wù)放入 timerQueue 或 taskQueue, 然后觸發(fā)調(diào)度行為, 讓任務(wù)進(jìn)入調(diào)度. 注意: enableProfiling 用來(lái)做一些審計(jì)和 debugger, 本文不去涉及.

          1.首先計(jì)算 startTime, 它被用作 timerQueue 排序的依據(jù), getCurrentTime() 用來(lái)獲取當(dāng)前時(shí)間, 下面會(huì)講到.2.接著計(jì)算 expirationTime, 它被用作 taskQueue 排序的依據(jù), 過(guò)期時(shí)間通過(guò)傳入的優(yōu)先級(jí)確定.3.newTask 是 Scheduler 中任務(wù)單元的數(shù)據(jù)結(jié)構(gòu), 注釋寫(xiě)的很清楚, 其中 sortIndex 是優(yōu)先隊(duì)列(小頂堆)中排序的依據(jù).4.根據(jù)上面三步的鋪墊, 這一步就是根據(jù) startTime 和 currentTime 的關(guān)系將任務(wù)放到 timerQueue 或 taskQueue 之中, 然后觸發(fā)調(diào)度行為.

          function unstable_scheduleCallback(priorityLevel, callback, options) {
          /*
          * (1
          */
          var currentTime = getCurrentTime();

          // timerQueue 根據(jù) startTime 排序
          // 任務(wù)進(jìn)來(lái)的時(shí)候, 開(kāi)始時(shí)間默認(rèn)是當(dāng)前時(shí)間, 如果進(jìn)入調(diào)度的時(shí)候傳了延遲時(shí)間
          // 開(kāi)始時(shí)間則是當(dāng)前時(shí)間與延遲時(shí)間的和
          var startTime;
          if (typeof options === "object" && options !== null) {
          var delay = options.delay;
          if (typeof delay === "number" && delay > 0) {
          startTime = currentTime + delay;
          } else {
          startTime = currentTime;
          }
          } else {
          startTime = currentTime;
          }

          /*
          * (2
          */
          // taskQueue 根據(jù) expirationTime 排序
          var timeout;
          switch (priorityLevel) {
          case ImmediatePriority:
          timeout = IMMEDIATE_PRIORITY_TIMEOUT; // -1
          break;
          case UserBlockingPriority:
          timeout = USER_BLOCKING_PRIORITY_TIMEOUT; // 250
          break;
          case IdlePriority:
          timeout = IDLE_PRIORITY_TIMEOUT; // 1073741823 (2^30 - 1)
          break;
          case LowPriority:
          timeout = LOW_PRIORITY_TIMEOUT; // 10000
          break;
          case NormalPriority:
          default:
          timeout = NORMAL_PRIORITY_TIMEOUT; // 5000
          break;
          }

          // 計(jì)算任務(wù)的過(guò)期時(shí)間, 任務(wù)開(kāi)始時(shí)間 + timeout
          // 若是立即執(zhí)行的優(yōu)先級(jí)(IMMEDIATE_PRIORITY_TIMEOUT(-1))
          // 它的過(guò)期時(shí)間是 startTime - 1, 意味著立刻就過(guò)期
          var expirationTime = startTime + timeout;

          /*
          * (3
          */
          // 創(chuàng)建調(diào)度任務(wù)
          var newTask = {
          id: taskIdCounter++,
          callback, // 調(diào)度的任務(wù)
          priorityLevel, // 任務(wù)優(yōu)先級(jí)
          startTime, // 任務(wù)開(kāi)始的時(shí)間, 表示任務(wù)何時(shí)才能執(zhí)行
          expirationTime, // 任務(wù)的過(guò)期時(shí)間
          sortIndex: -1, // 在小頂堆隊(duì)列中排序的依據(jù)
          };

          if (enableProfiling) {
          newTask.isQueued = false;
          }

          /*
          * (4
          */
          // startTime > currentTime 說(shuō)明任務(wù)無(wú)需立刻執(zhí)行
          // 故放到 timerQueue 中
          if (startTime > currentTime) {
          // timerQueue 是通過(guò) startTime 判斷優(yōu)先級(jí)的,
          // 故將 startTime 設(shè)為 sortIndex 作為優(yōu)先級(jí)依據(jù)
          newTask.sortIndex = startTime;
          push(timerQueue, newTask);

          // 如果 taskQueue 是空的, 并且當(dāng)前任務(wù)優(yōu)先級(jí)最高
          // 那么這個(gè)任務(wù)就應(yīng)該優(yōu)先被設(shè)為 isHostTimeoutScheduled
          if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
          // 如果超時(shí)調(diào)度已經(jīng)在執(zhí)行了, 就取消掉
          // 因?yàn)楫?dāng)前這個(gè)任務(wù)是最高優(yōu)的, 需要先處理當(dāng)前這個(gè)任務(wù)
          if (isHostTimeoutScheduled) {
          cancelHostTimeout();
          } else {
          isHostTimeoutScheduled = true;
          }
          // Schedule a timeout.
          requestHostTimeout(handleTimeout, startTime - currentTime);
          }
          } else {
          // startTime <= currentTime 說(shuō)明任務(wù)已過(guò)期
          // 需將任務(wù)放到 taskQueue
          newTask.sortIndex = expirationTime;
          push(taskQueue, newTask);

          if (enableProfiling) {
          markTaskStart(newTask, currentTime);
          newTask.isQueued = true;
          }

          // 如果目前正在對(duì)某個(gè)過(guò)期任務(wù)進(jìn)行調(diào)度,
          // 當(dāng)前任務(wù)需要等待下次時(shí)間片讓出時(shí)才能執(zhí)行
          if (!isHostCallbackScheduled && !isPerformingWork) {
          isHostCallbackScheduled = true;
          requestHostCallback(flushWork);
          }
          }

          return newTask;
          }

          getCurrentTime

          顧名思義, getCurrentTime 用來(lái)獲取當(dāng)前時(shí)間, 它優(yōu)先使用 performance.now()[8], 否則使用 Date.now(). 提起 performance 我們并不陌生, 它主要被用來(lái)收集性能指標(biāo). performance.now() 返回一個(gè)精確到毫秒的 DOMHighResTimeStamp(emmm, 一看到 HighRes 就想起大法).

          let getCurrentTime;
          const hasPerformanceNow =
          typeof performance === "object" && typeof performance.now === "function";

          if (hasPerformanceNow) {
          const localPerformance = performance;
          getCurrentTime = () => localPerformance.now();
          } else {
          const localDate = Date;
          const initialTime = localDate.now();
          getCurrentTime = () => localDate.now() - initialTime;
          }

          稍微看了下 chromium 源碼[9](反正也看不懂啦), 大抵就是說(shuō) performance.now() 是個(gè)單調(diào)遞增的時(shí)間(monotonic_time), 這保證了兩個(gè)調(diào)用之間的差永遠(yuǎn)不會(huì)是負(fù)的; 此外還看到通過(guò) time_lower_digits 和 time_upper_digits 來(lái)做了一些降噪處理, 保證計(jì)算結(jié)果不會(huì)太突兀. 此外還有什么粗化時(shí)間算法(coarsen time algorithm)[10]就更尼瑪看不懂了.

          DOMHighResTimeStamp Performance::now() const {
          return MonotonicTimeToDOMHighResTimeStamp(tick_clock_->NowTicks());
          }

          requestHostTimeout 和 cancelHostTimeout

          顯然這是一對(duì)相愛(ài)相殺的好基友. 為了讓一個(gè)未過(guò)期的任務(wù)能夠到達(dá)恰好過(guò)期的狀態(tài), 那么需要延遲 startTime - currentTime 毫秒就可以了(其實(shí)它倆的差就是 XXX_PRIORITY_TIMEOUT), requestHostTimeout 就是來(lái)做這件事的, 而 cancelHostTimeout 就是用來(lái)取消這個(gè)超時(shí)函數(shù)的.

          function requestHostTimeout(callback, ms) {
          taskTimeoutID = setTimeout(() => {
          callback(getCurrentTime());
          }, ms);
          }

          function cancelHostTimeout() {
          clearTimeout(taskTimeoutID);
          taskTimeoutID = -1;
          }

          handleTimeout

          requestHostTimeout 的第一個(gè)參數(shù)是 handleTimeout, 讓我們來(lái)看看它是來(lái)做什么的. 首先調(diào)用了 advanceTimers 方法, 這個(gè)方法下面具體說(shuō), 它主要是用來(lái)更新 timerQueue 和 taskQueue 兩個(gè)序列, 如果發(fā)現(xiàn) timerQueue 有過(guò)期的, 就放到 taskQueue 中. 接下來(lái)如果沒(méi)有正在調(diào)度任務(wù), 就看看 taskQueue 中是否存在任務(wù), 如果有的話就先 flush 掉; 否則就遞歸執(zhí)行 requestHostTimeout(handleTimeout, ...). 總之來(lái)講, 這個(gè)方法就是要把 timerQueue 中的任務(wù)轉(zhuǎn)移到 taskQueue 中.

          function handleTimeout(currentTime) {
          isHostTimeoutScheduled = false;
          // 更新 timerQueue 和 taskQueue 兩個(gè)序列
          // 如果發(fā)現(xiàn) timerQueue 有過(guò)期的, 就放到 taskQueue 中
          advanceTimers(currentTime);

          // 檢查是否已經(jīng)開(kāi)始調(diào)度
          // 如果正在調(diào)度, 就什么都不做
          if (!isHostCallbackScheduled) {
          // 如果 taskQueue 中有任務(wù), 那就先去執(zhí)行已過(guò)期的任務(wù)
          if (peek(taskQueue) !== null) {
          isHostCallbackScheduled = true;
          requestHostCallback(flushWork);
          } else {
          // 如果沒(méi)有過(guò)期任務(wù), 那就接著對(duì)最高優(yōu)的第一個(gè)未過(guò)期的任務(wù)
          // 繼續(xù)重復(fù)這個(gè)過(guò)程, 直到它可以被放置到 taskQueue
          const firstTimer = peek(timerQueue);
          if (firstTimer !== null) {
          requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
          }
          }
          }
          }

          advanceTimers

          這個(gè)方法就是用來(lái)檢查 timerQueue 中的過(guò)期任務(wù), 放到 taskQueue. 主要是對(duì)小頂堆的各種操作, 直接看注釋即可.

          function advanceTimers(currentTime) {
          let timer = peek(timerQueue);
          while (timer !== null) {
          if (timer.callback === null) {
          // Timer was cancelled.
          pop(timerQueue);

          // 開(kāi)始時(shí)間小于等于當(dāng)前時(shí)間, 說(shuō)明已過(guò)期,
          // 從 taskQueue 移走, 放到 taskQueue
          } else if (timer.startTime <= currentTime) {
          pop(timerQueue);
          // taskQueue 是通過(guò) expirationTime 判斷優(yōu)先級(jí)的,
          // expirationTime 越小, 說(shuō)明越緊急, 它就應(yīng)該放在 taskQueue 的最前面
          timer.sortIndex = timer.expirationTime;
          push(taskQueue, timer);

          if (enableProfiling) {
          markTaskStart(timer, currentTime);
          timer.isQueued = true;
          }
          } else {
          // 開(kāi)始時(shí)間大于當(dāng)前時(shí)間, 說(shuō)明未過(guò)期, 任務(wù)仍然保留在 timerQueue
          // 任務(wù)進(jìn)來(lái)的時(shí)候, 開(kāi)始時(shí)間默認(rèn)是當(dāng)前時(shí)間, 如果進(jìn)入調(diào)度的時(shí)候傳了延遲時(shí)間, 開(kāi)始時(shí)間則是當(dāng)前時(shí)間與延遲時(shí)間的和
          // 開(kāi)始時(shí)間越早, 說(shuō)明會(huì)越早開(kāi)始, 排在最小堆的前面
          // Remaining timers are pending.
          return;
          }
          timer = peek(timerQueue);
          }
          }

          requestHostCallback

          不管你接沒(méi)接觸過(guò) React 源碼, 想必也聽(tīng)到過(guò)時(shí)間切片, 任務(wù)中斷可恢復(fù)這些概念. requestHostCallback 這個(gè)方法就是用來(lái)調(diào)度任務(wù)的. 既然是"調(diào)度", 那勢(shì)必得有指揮的和干活的.

          舊的 React 版通過(guò) requestAnimationFrame 和 requestIdleCallback 進(jìn)行任務(wù)調(diào)度與幀對(duì)齊, 但在 [scheduler] Yield many times per frame, no rAF #16214[11] 這個(gè) pr 中, 這種方式被廢棄了. 如果你看過(guò)我以前的一篇文章 剖析 requestAnimationFrame[12], 就會(huì)發(fā)現(xiàn) rAF 是會(huì)受到用戶行為的干擾的, 比如切換選項(xiàng)卡, 滾動(dòng)頁(yè)面等. 看下面這張圖, 前面一部分的斜率大抵就是 16.7, 也就是 1 / 60, 但我切換了選項(xiàng)卡之后, 幀刷新率立馬不穩(wěn)定了.

          此外, rAF 畢竟仰仗顯示器的刷新頻率, 而市面上的刷新頻率層次不齊, 有 60Hz 的, 像蘋(píng)果的 ProMotion 就到了 120Hz, 再加上好的顯卡都被拿去挖礦了, 兼容起來(lái)實(shí)在麻煩. 簡(jiǎn)言之, 這種方式會(huì)受到外界因素影響, 無(wú)法使 Scheduler 做到百分百掌控.

          requestIdleCallback 就不詳細(xì)說(shuō)了, 它可用在瀏覽器空閑階段去執(zhí)行一些低優(yōu)先級(jí)任務(wù), 而不會(huì)影響延遲關(guān)鍵事件, 如動(dòng)畫(huà)和輸入響應(yīng). 具體使用方法可自行去看 MDN[13] 上的介紹.

          目前最新的代碼中, Scheduler 通過(guò) MessageChannel 來(lái)人為的控制調(diào)度頻率, 默認(rèn)的時(shí)間切片是 5ms, 可見(jiàn)這個(gè)粒度比 ProMotion 還要高. 如果你以前沒(méi)聽(tīng)說(shuō)過(guò) MessageChannel, 但一定得聽(tīng)說(shuō)過(guò) postMessage 這家伙, 它經(jīng)常被用做宿主跟 iframe 之間的通信. 此外它兼容性上也是好到?jīng)]朋友.

          鋪墊的都說(shuō)完了, 直接看源碼. 它做了一波兼容, 如果是 Node.js 或者低端 IE, 就使用 setImmediate, 這塊不展開(kāi)說(shuō). 在正經(jīng)的瀏覽器環(huán)境下(IE: 你直接念我身份證好了), 我們通過(guò) MessageChannel 創(chuàng)建一個(gè)實(shí)例 channel, 該實(shí)例有兩個(gè) port, 用來(lái)互相通信. Scheduler 通過(guò) port2 發(fā)送消息(port.postMessage), 通過(guò) port1 來(lái)接收消息(port1.onmessage). 因此, port2 就是那個(gè)調(diào)度者, port1 是那個(gè)收到調(diào)度信號(hào)真正干活的.

          let schedulePerformWorkUntilDeadline;

          if (typeof setImmediate === "function") {
          schedulePerformWorkUntilDeadline = () => {
          setImmediate(performWorkUntilDeadline);
          };
          } else {
          const channel = new MessageChannel();
          const port = channel.port2;

          // port1 接收調(diào)度信號(hào), 來(lái)執(zhí)行 performWorkUntilDeadline(受)
          channel.port1.onmessage = performWorkUntilDeadline;

          // port 是調(diào)度者(攻)
          schedulePerformWorkUntilDeadline = () => {
          port.postMessage(null);
          };
          }

          requestHostCallback 將傳進(jìn)來(lái)的 callback 賦值給全局變量 scheduledHostCallback, 如果當(dāng)前 isMessageLoopRunning 是 false, 即沒(méi)有任務(wù)調(diào)度, 就把它開(kāi)啟, 然后發(fā)送調(diào)度信號(hào)給 port1 進(jìn)行調(diào)度.

          function requestHostCallback(callback) {
          scheduledHostCallback = callback;
          if (!isMessageLoopRunning) {
          isMessageLoopRunning = true;

          // postMessage, 告訴 port1 來(lái)執(zhí)行 performWorkUntilDeadline 方法
          schedulePerformWorkUntilDeadline();
          }
          }

          performWorkUntilDeadline

          performWorkUntilDeadline 是任務(wù)的執(zhí)行者, 也就是 port1 接收到信號(hào)后需要執(zhí)行的函數(shù), 它用來(lái)在時(shí)間片內(nèi)執(zhí)行任務(wù), 如果沒(méi)執(zhí)行完, 用一個(gè)新的調(diào)度者繼續(xù)調(diào)度. 首先判斷是否有 scheduledHostCallback, 如果存在說(shuō)明存在需要被調(diào)度的任務(wù). 計(jì)算 deadline 為當(dāng)前時(shí)間加上 yieldInterval(也就是那 5ms). 看到這里相必你就恍然大悟了, deadline 其實(shí)就來(lái)做時(shí)間切片! 接下來(lái)設(shè)置了一個(gè)常量 hasTimeRemaining 為 true, 看到這倆名字你是不是想起了 requestIdleCallback 的用法了呢. 至于為什么 hasTimeRemaining 為 true, 因?yàn)椴还苣愕恼麄€(gè)任務(wù)是否執(zhí)行完, 給你的時(shí)間就是 5ms, 要么超時(shí)就中斷, 要么不超時(shí)就恰好執(zhí)行完了, 總之時(shí)間切片內(nèi)一定是有剩余時(shí)間的.

          后面的邏輯直接看代碼注釋即可, 總結(jié)來(lái)講就是任務(wù)在時(shí)間切片內(nèi)沒(méi)有被執(zhí)行完, 就需要讓調(diào)度者再次調(diào)度一個(gè)執(zhí)行者繼續(xù)執(zhí)行任務(wù), 否則這個(gè)任務(wù)就算執(zhí)行完了. 判斷一個(gè)任務(wù)執(zhí)行完成的標(biāo)記是 hasMoreWork 字段, 下面 workLoop 會(huì)講到.

          //
          const performWorkUntilDeadline = () => {
          if (scheduledHostCallback !== null) {
          const currentTime = getCurrentTime();
          // 時(shí)間分片
          deadline = currentTime + yieldInterval;
          const hasTimeRemaining = true;

          let hasMoreWork = true;
          try {
          // scheduledHostCallback 去執(zhí)行真正的任務(wù)
          // 如果返回 true, 說(shuō)明當(dāng)前任務(wù)被中斷了
          // 會(huì)再讓調(diào)度者調(diào)度一個(gè)執(zhí)行者繼續(xù)執(zhí)行任務(wù)
          // 下面講 workLoop 方法時(shí)會(huì)說(shuō)到中斷恢復(fù)的邏輯, 先留個(gè)坑
          hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
          } finally {
          if (hasMoreWork) {
          // 如果任務(wù)中斷了(沒(méi)執(zhí)行完), 就說(shuō)明 hasMoreWork 為 true
          // 這塊類似于遞歸, 就再申請(qǐng)一個(gè)調(diào)度者來(lái)繼續(xù)執(zhí)行該任務(wù)
          schedulePerformWorkUntilDeadline();
          } else {
          // 否則當(dāng)前任務(wù)就執(zhí)行完了
          // 關(guān)閉 isMessageLoopRunning
          // 并將 scheduledHostCallback 置為 null
          isMessageLoopRunning = false;
          scheduledHostCallback = null;
          }
          }
          } else {
          isMessageLoopRunning = false;
          }
          // Yielding to the browser will give it a chance to paint, so we can
          // reset this.
          needsPaint = false;
          };

          flushWork

          我們?cè)缭?nbsp;requestHostCallback 就將 flushWork 作為參數(shù)賦值給了全局變量 scheduledHostCallback, 在上面 performWorkUntilDeadline 也調(diào)用了該方法, 讓我們看看 flushWork 用來(lái)做什么. 顧名思義, flushWork 就是把任務(wù)"沖刷"掉, 就好比 taskQueue 是馬桶, 里面的任務(wù)是那啥, flushWork 就是沖水那套機(jī)制. 當(dāng)然剖絲抽繭, 該方法的核心就是 return 了 workLoop.

          function flushWork(hasTimeRemaining, initialTime) {
          if (enableProfiling) {
          markSchedulerUnsuspended(initialTime);
          }

          // 由于 requestHostCallback 并不一定立即執(zhí)行傳入的回調(diào)函數(shù)
          // 所以 isHostCallbackScheduled 狀態(tài)可能會(huì)維持一段時(shí)間
          // 等到 flushWork 開(kāi)始處理任務(wù)時(shí), 則需要釋放該狀態(tài)以支持其他的任務(wù)被 schedule 進(jìn)來(lái)
          isHostCallbackScheduled = false;

          // 因?yàn)橐呀?jīng)在執(zhí)行 taskQueue 的任務(wù)了
          // 所以不需要等 timerQueue 中的任務(wù)過(guò)期了
          if (isHostTimeoutScheduled) {
          isHostTimeoutScheduled = false;
          cancelHostTimeout();
          }

          isPerformingWork = true;
          const previousPriorityLevel = currentPriorityLevel;
          try {
          if (enableProfiling) {
          try {
          return workLoop(hasTimeRemaining, initialTime);
          } catch (error) {
          if (currentTask !== null) {
          const currentTime = getCurrentTime();
          markTaskErrored(currentTask, currentTime);
          currentTask.isQueued = false;
          }
          throw error;
          }
          } else {
          // No catch in prod code path.
          return workLoop(hasTimeRemaining, initialTime);
          }
          } finally {
          // 執(zhí)行完任務(wù)后還原這些全局狀態(tài)
          currentTask = null;
          currentPriorityLevel = previousPriorityLevel;
          isPerformingWork = false;
          if (enableProfiling) {
          const currentTime = getCurrentTime();
          markSchedulerSuspended(currentTime);
          }
          }
          }

          任務(wù)中斷與恢復(fù) —— workLoop

          終于到了尾聲, workLoop 可謂是集大成者, 承載了任務(wù)中斷, 任務(wù)恢復(fù), 判斷任務(wù)完成等功能.

          ?循環(huán) taskQueue 執(zhí)行任務(wù)?任務(wù)狀態(tài)的判斷

          ?如果 taskQueue 執(zhí)行完成了, 就返回 false, 并從 timerQueue 中拿出最高優(yōu)的來(lái)做超時(shí)調(diào)度?如果未執(zhí)行完, 說(shuō)明當(dāng)前調(diào)度發(fā)生了中斷, 就返回 true, 下次接著調(diào)度(這個(gè) Boolean 類型的返回值, 其實(shí)就對(duì)應(yīng)著 performWorkUntilDeadline 中的 hasMoreWork)


          function workLoop(hasTimeRemaining, initialTime) {
          let currentTime = initialTime;

          // 因?yàn)槭莻€(gè)異步的, 需要再次調(diào)整一下 timerQueue 跟 taskQueue
          advanceTimers(currentTime);

          // 最緊急的過(guò)期任務(wù)
          currentTask = peek(taskQueue);
          while (
          currentTask !== null &&
          !(enableSchedulerDebugging && isSchedulerPaused) // 用于 debugger, 不管
          ) {
          // 任務(wù)中斷!!!
          // 時(shí)間片到了, 但 currentTask 未過(guò)期, 跳出循環(huán)
          // 當(dāng)前任務(wù)就被中斷了, 需要放到下次 workLoop 中執(zhí)行
          if (
          currentTask.expirationTime > currentTime &&
          (!hasTimeRemaining || shouldYieldToHost())
          ) {
          // This currentTask hasn't expired, and we've reached the deadline.
          break;
          }

          const callback = currentTask.callback;
          if (typeof callback === "function") {
          // 清除掉 currentTask.callback
          // 如果下次迭代 callback 為空, 說(shuō)明任務(wù)執(zhí)行完了
          currentTask.callback = null;

          currentPriorityLevel = currentTask.priorityLevel;

          // 已過(guò)期
          const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;

          if (enableProfiling) {
          markTaskRun(currentTask, currentTime);
          }

          // 執(zhí)行任務(wù)
          const continuationCallback = callback(didUserCallbackTimeout);
          currentTime = getCurrentTime();

          // 如果產(chǎn)生了連續(xù)回調(diào), 說(shuō)明出現(xiàn)了中斷
          // 故將新的 continuationCallback 賦值 currentTask.callback
          // 這樣下次恢復(fù)任務(wù)時(shí), callback 就接上趟了
          if (typeof continuationCallback === "function") {
          currentTask.callback = continuationCallback;

          if (enableProfiling) {
          markTaskYield(currentTask, currentTime);
          }
          } else {
          if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
          }
          // 如果 continuationCallback 不是 Function 類型, 說(shuō)明任務(wù)完成!!!
          // 否則, 說(shuō)明這個(gè)任務(wù)執(zhí)行完了, 可以被彈出了
          if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
          }
          }

          // 上面執(zhí)行任務(wù)會(huì)消耗一些時(shí)間, 再次重新更新兩個(gè)隊(duì)列
          advanceTimers(currentTime);
          } else {
          // 上面的 if 清空了 currentTask.callback, 所以
          // 如果 callback 為空, 說(shuō)明這個(gè)任務(wù)就執(zhí)行完了, 可以被彈出了
          pop(taskQueue);
          }

          // 如果當(dāng)前任務(wù)執(zhí)行完了, 那么就把下一個(gè)最高優(yōu)的任務(wù)拿出來(lái)執(zhí)行, 直到清空了 taskQueue
          // 如果當(dāng)前任務(wù)沒(méi)執(zhí)行完, currentTask 實(shí)際還是當(dāng)前的任務(wù), 只不過(guò) callback 變成了 continuationCallback
          currentTask = peek(taskQueue);
          }

          // 任務(wù)恢復(fù)!!!
          // 上面說(shuō)到 ddl 到了, 但 taskQueue 還沒(méi)執(zhí)行完(也就是任務(wù)被中斷了)
          // 就返回 true, 這就是恢復(fù)任務(wù)的標(biāo)志
          if (currentTask !== null) {
          return true;
          } else {
          // 若任務(wù)完成!!!, 去 timerQueue 中找需要最早開(kāi)始執(zhí)行的那個(gè)任務(wù)
          // 進(jìn)行 requestHostTimeout 調(diào)度那一套
          const firstTimer = peek(timerQueue);
          if (firstTimer !== null) {
          requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
          }
          return false;
          }
          }

          shouldYieldToHost

          這個(gè)方法沒(méi)啥可說(shuō)的, 就是判斷是否要讓出主線程. 不過(guò)它引申出一個(gè)比較新潮的 API 即 navigator.scheduling.isInputPending, 它用來(lái)再不讓出主線程的情況下提高響應(yīng)能力, 不過(guò) Chrome 90 還沒(méi)有該 API, 想必這是個(gè)面向未來(lái)的. Better JS scheduling with isInputPending()[14] 講得不錯(cuò), 可以看看.

          function shouldYieldToHost() {
          if (
          enableIsInputPending &&
          navigator !== undefined &&
          navigator.scheduling !== undefined &&
          navigator.scheduling.isInputPending !== undefined
          ) {
          const scheduling = navigator.scheduling;
          const currentTime = getCurrentTime();
          if (currentTime >= deadline) {
          // There's no time left. We may want to yield control of the main
          // thread, so the browser can perform high priority tasks. The main ones
          // are painting and user input. If there's a pending paint or a pending
          // input, then we should yield. But if there's neither, then we can
          // yield less often while remaining responsive. We'll eventually yield
          // regardless, since there could be a pending paint that wasn't
          // accompanied by a call to `requestPaint`, or other main thread tasks
          // like network events.
          // 需要繪制或者有高優(yōu)先級(jí)的 I/O, 必須得讓出主線程
          if (needsPaint || scheduling.isInputPending()) {
          // There is either a pending paint or a pending input.
          return true;
          }
          // There's no pending input. Only yield if we've reached the max
          // yield interval.
          return currentTime >= maxYieldInterval;
          } else {
          // There's still time left in the frame.
          return false;
          }
          } else {
          // `isInputPending` is not available. Since we have no way of knowing if
          // there's pending input, always yield at the end of the frame.

          // task 執(zhí)行超過(guò)了 ddl 就應(yīng)該讓出主進(jìn)程了
          return getCurrentTime() >= deadline;
          }
          }

          取消調(diào)度

          在 workLoop 的代碼中有一段是 currentTask.callback = null;, 也就是 Scheduler 以 callback 是否為 null 來(lái)判斷任務(wù)被取消(或者完成了).

          function unstable_cancelCallback(task) {
          if (enableProfiling) {
          if (task.isQueued) {
          const currentTime = getCurrentTime();
          markTaskCanceled(task, currentTime);
          task.isQueued = false;
          }
          }

          // Null out the callback to indicate the task has been canceled. (Can't
          // remove from the queue because you can't remove arbitrary nodes from an
          // array based heap, only the first one.)
          task.callback = null;
          }

          自定義的時(shí)間切片頻率

          為了后續(xù) Scheduler 獨(dú)立成包, 它開(kāi)放了設(shè)置時(shí)間切片的大小, 默認(rèn)為 5ms, 你可以根據(jù)實(shí)際情況調(diào)整到 0 ~ 125 之間. 不過(guò)怎么把握這個(gè)度, 咱也不知道咱也不敢問(wèn).

          function forceFrameRate(fps) {
          if (fps < 0 || fps > 125) {
          // Using console['error'] to evade Babel and ESLint
          console["error"](
          "forceFrameRate takes a positive int between 0 and 125, " +
          "forcing frame rates higher than 125 fps is not supported"
          );
          return;
          }
          if (fps > 0) {
          yieldInterval = Math.floor(1000 / fps);
          } else {
          // reset the framerate
          yieldInterval = 5;
          }
          }

          最后

          以上全部就是 Scheduler 的源碼解析了, 洋洋灑灑兩萬(wàn)余字, 一大半都是代碼... 除此之外源碼中還有一些通用邏輯的封裝, 以及一些面向未來(lái)的特性文中沒(méi)有涉及, 有興趣可以去 GayHub 上翻源碼看看. 本文基于 v17.0.2, 未來(lái)誰(shuí)也沒(méi)法保證它的代碼會(huì)變成啥樣, 先看先享受, 且行且珍惜. 后面如有大的更新, 我會(huì)盡力更新文章, 以保證和 master 對(duì)齊. 讀源碼這事兒, 不是一朝一夕的事兒, 也不能只一家之言, 歡迎大家拍磚提意見(jiàn). 實(shí)在是畫(huà)圖苦手, 盜用 shockw4ver 大佬的一張流程圖收尾.

          參考

          ?React 中的優(yōu)先級(jí)管理[15]?React 調(diào)度原理(scheduler)[16]?探索 React 的內(nèi)在 —— postMessage & Scheduler[17]?一篇長(zhǎng)文幫你徹底搞懂 React 的調(diào)度機(jī)制原理[18]?這可能是最通俗的 React Fiber(時(shí)間分片) 打開(kāi)方式[19]

          References

          [1] Scheduler: https://github.com/facebook/react/tree/master/packages/scheduler
          [2] 小頂堆https://algorithm.yanceyleo.com/data-structure/tree/binary-heap
          [3] 優(yōu)先隊(duì)列: https://algorithm.yanceyleo.com/data-structure/queue/priority-queue
          [4] topK: https://algorithm.yanceyleo.com/leetcode/lcof/40-get-least-numbers
          [5] 數(shù)據(jù)結(jié)構(gòu)與算法: https://algorithm.yanceyleo.com
          [6] 協(xié)程https://en.wikipedia.org/wiki/Coroutine
          [7] React 源碼: https://github.com/learn-frame/react/blob/feature/learn-react/packages/scheduler/src/forks/SchedulerDOM.js
          [8] performance.now()https://developer.mozilla.org/en-US/docs/Web/API/Performance/now
          [9] chromium 源碼: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/timing/performance.cc;l=1107;drc=28684b63b1e1ece5396dc0e4c03118855710a75f;bpv=1;bpt=1
          [10] 粗化時(shí)間算法(coarsen time algorithm): https://w3c.github.io/hr-time/#dfn-coarsen-time
          [11] [scheduler] Yield many times per frame, no rAF #16214: https://github.com/facebook/react/pull/16214/commits
          [12] 剖析 requestAnimationFrame: https://www.yanceyleo.com/post/20506b75-0a04-450d-aeec-6ea08ef25116
          [13] MDN: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
          [14] Better JS scheduling with isInputPending(): https://web.dev/isinputpending/
          [15] React 中的優(yōu)先級(jí)管理: https://github.com/7kms/react-illustration-series/blob/master/docs/main/priority.md
          [16] React 調(diào)度原理(scheduler): https://github.com/7kms/react-illustration-series/blob/master/docs/main/scheduler.md
          [17] 探索 React 的內(nèi)在 —— postMessage & Scheduler: https://segmentfault.com/a/1190000022942008
          [18] 一篇長(zhǎng)文幫你徹底搞懂 React 的調(diào)度機(jī)制原理: https://segmentfault.com/a/1190000039101758
          [19] 這可能是最通俗的 React Fiber(時(shí)間分片) 打開(kāi)方式: https://juejin.cn/post/6844903975112671239


          瀏覽 60
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  北条麻妃中文字幕在线视频 | 九色一区 | 美女操B视频 | 精品人妻一区二区三区香蕉 | 日本特一级免费 |