中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

React的調度機制原理是什么

發布時間:2021-10-19 15:22:07 來源:億速云 閱讀:151 作者:iii 欄目:web開發

這篇文章主要介紹“React的調度機制原理是什么”,在日常操作中,相信很多人在React的調度機制原理是什么問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”React的調度機制原理是什么”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

 點擊進入React源碼調試倉庫。

Scheduler作為一個獨立的包,可以獨自承擔起任務調度的職責,你只需要將任務和任務的優先級交給它,它就可以幫你管理任務,安排任務的執行。這就是React和Scheduler配合工作的模式。

對于多個任務,它會先執行優先級高的。聚焦到單個任務的執行上,會被Scheduler有節制地去執行。換句話說,線程只有一個,它不會一直占用著線程去執行任務。而是執行一會,中斷一下,如此往復。用這樣的模式,來避免一直占用有限的資源執行耗時較長的任務,解決用戶操作時頁面卡頓的問題,實現更快的響應。

我們可以從中梳理出Scheduler中兩個重要的行為:多個任務的管理、單個任務的執行控制。

基本概念

為了實現上述的兩個行為,它引入兩個概念:任務優先級 、 時間片。

任務優先級讓任務按照自身的緊急程度排序,這樣可以讓優先級最高的任務最先被執行到。

時間片規定的是單個任務在這一幀內最大的執行時間,任務一旦執行時間超過時間片,則會被打斷,有節制地執行任務。這樣可以保證頁面不會因為任務連續執行的時間過長而產生卡頓。

原理概述

基于任務優先級和時間片的概念,Scheduler圍繞著它的核心目標 - 任務調度,衍生出了兩大核心功能:任務隊列管理 和 時間片下任務的中斷和恢復。

任務隊列管理

任務隊列管理對應了Scheduler的多任務管理這一行為。在Scheduler內部,把任務分成了兩種:未過期的和已過期的,分別用兩個隊列存儲,前者存到timerQueue中,后者存到taskQueue中。

如何區分任務是否過期?

用任務的開始時間(startTime)和當前時間(currentTime)作比較。開始時間大于當前時間,說明未過期,放到timerQueue;開始時間小于等于當前時間,說明已過期,放到taskQueue。

不同隊列中的任務如何排序?

當任務一個個入隊的時候,自然要對它們進行排序,保證緊急的任務排在前面,所以排序的依據就是任務的緊急程度。而taskQueue和timerQueue中任務緊急程度的判定標準是有區別的。

  •  taskQueue中,依據任務的過期時間(expirationTime)排序,過期時間越早,說明越緊急,過期時間小的排在前面。過期時間根據任務優先級計算得出,優先級越高,過期時間越早。

  •  timerQueue中,依據任務的開始時間(startTime)排序,開始時間越早,說明會越早開始,開始時間小的排在前面。任務進來的時候,開始時間默認是當前時間,如果進入調度的時候傳了延遲時間,開始時間則是當前時間與延遲時間的和。

任務入隊兩個隊列,之后呢?

如果放到了taskQueue,那么立即調度一個函數去循環taskQueue,挨個執行里面的任務。

如果放到了timerQueue,那么說明它里面的任務都不會立即執行,那就等到了timerQueue里面排在第一個任務的開始時間,看這個任務是否過期,如果是,則把任務從timerQueue中拿出來放入taskQueue,調度一個函數去循環它,執行掉里面的任務;否則過一會繼續檢查這第一個任務是否過期。

任務隊列管理相對于單個任務的執行,是宏觀層面的概念,它利用任務的優先級去管理任務隊列中的任務順序,始終讓最緊急的任務被優先處理。

單個任務的中斷以及恢復

單個任務的中斷以及恢復對應了Scheduler的單個任務執行控制這一行為。在循環taskQueue執行每一個任務時,如果某個任務執行時間過長,達到了時間片限制的時間,那么該任務必須中斷,以便于讓位給更重要的事情(如瀏覽器繪制),等事情完成,再恢復執行任務。

例如這個例子,點擊按鈕渲染140000個DOM節點,為的是讓React通過scheduler調度一個耗時較長的更新任務。同時拖動方塊,這是為了模擬用戶交互。更新任務會占用線程去執行任務,用戶交互要也要占用線程去響應頁面,這就決定了它們兩個是互斥的關系。在React的concurrent模式下,通過Scheduler調度的更新任務遇到用戶交互之后,會是下面動圖里的效果。

React的調度機制原理是什么

執行React任務和頁面響應交互這兩件事情是互斥的,但因為Scheduler可以利用時間片中斷React任務,然后讓出線程給瀏覽器去繪制,所以一開始在fiber樹的構建階段,拖動方塊會得到及時的反饋。但是后面卡了一下,這是因為fiber樹構建完成,進入了同步的commit階段,導致交互卡頓。分析頁面的渲染過程可以非常直觀地看到通過時間片的控制。主線程被讓出去進行頁面的繪制(Painting和Rendering,綠色和紫色的部分)。

React的調度機制原理是什么

Scheduler要實現這樣的調度效果需要兩個角色:任務的調度者、任務的執行者。調度者調度一個執行者,執行者去循環taskQueue,逐個執行任務。當某個任務的執行時間比較長,執行者會根據時間片中斷任務執行,然后告訴調度者:我現在正執行的這個任務被中斷了,還有一部分沒完成,但現在必須讓位給更重要的事情,你再調度一個執行者吧,好讓這個任務能在之后被繼續執行完(任務的恢復)。于是,調度者知道了任務還沒完成,需要繼續做,它會再調度一個執行者去繼續完成這個任務。

通過執行者和調度者的配合,可以實現任務的中斷和恢復。

原理小結

Scheduler管理著taskQueue和timerQueue兩個隊列,它會定期將timerQueue中的過期任務放到taskQueue中,然后讓調度者通知執行者循環taskQueue執行掉每一個任務。執行者控制著每個任務的執行,一旦某個任務的執行時間超出時間片的限制。就會被中斷,然后當前的執行者退場,退場之前會通知調度者再去調度一個新的執行者繼續完成這個任務,新的執行者在執行任務時依舊會根據時間片中斷任務,然后退場,重復這一過程,直到當前這個任務徹底完成后,將任務從taskQueue出隊。taskQueue中每一個任務都被這樣處理,最終完成所有任務,這就是Scheduler的完整工作流程。

這里面有一個關鍵點,就是執行者如何知道這個任務到底完成沒完成呢?這是另一個話題了,也就是判斷任務的完成狀態。在講解執行者執行任務的細節時會重點突出。

以上是Scheduler原理的概述,下面開始是對React和Scheduler聯合工作機制的詳細解讀。涉及React與Scheduler的連接、調度入口、任務優先級、任務過期時間、任務中斷和恢復、判斷任務的完成狀態等內容。

詳細流程

在開始之前,我們先看一下React和Scheduler它們二者構成的一個系統的示意圖。

React的調度機制原理是什么

整個系統分為三部分:

  •  產生任務的地方:React

  •  React和Scheduler交流的翻譯者:SchedulerWithReactIntegration

  •  任務的調度者:Scheduler

React中通過下面的代碼,讓fiber樹的構建任務進入調度流程:

scheduleCallback(    schedulerPriorityLevel,    performConcurrentWorkOnRoot.bind(null, root),  );

任務通過翻譯者交給Scheduler,Scheduler進行真正的任務調度,那么為什么需要一個翻譯者的角色呢?

React與Scheduler的連接

Scheduler幫助React調度各種任務,但是本質上它們是兩個完全不耦合的東西,二者各自都有自己的優先級機制,那么這時就需要有一個中間角色將它們連接起來。

實際上,在react-reconciler中提供了這樣一個文件專門去做這樣的工作,它就是SchedulerWithReactIntegration.old(new).js。它將二者的優先級翻譯了一下,讓React和Scheduler能讀懂對方。另外,封裝了一些Scheduler中的函數供React使用。

在執行React任務的重要文件ReactFiberWorkLoop.js中,關于Scheduler的內容都是從SchedulerWithReactIntegration.old(new).js導入的。它可以理解成是React和Scheduler之間的橋梁。

// ReactFiberWorkLoop.js  import {    scheduleCallback,    cancelCallback,    getCurrentPriorityLevel,    runWithPriority,    shouldYield,    requestPaint,    now,    NoPriority as NoSchedulerPriority,    ImmediatePriority as ImmediateSchedulerPriority,    UserBlockingPriority as UserBlockingSchedulerPriority,    NormalPriority as NormalSchedulerPriority,    flushSyncCallbackQueue,    scheduleSyncCallback,  } from './SchedulerWithReactIntegration.old';

SchedulerWithReactIntegration.old(new).js通過封裝Scheduler的內容,對React提供兩種調度入口函數:scheduleCallback 和 scheduleSyncCallback。任務通過調度入口函數進入調度流程。

例如,fiber樹的構建任務在concurrent模式下通過scheduleCallback完成調度,在同步渲染模式下由scheduleSyncCallback完成。

// concurrentMode  // 將本次更新任務的優先級轉化為調度優先級  // schedulerPriorityLevel為調度優先級  const schedulerPriorityLevel = lanePriorityToSchedulerPriority(    newCallbackPriority,  );  // concurrent模式  scheduleCallback(    schedulerPriorityLevel,    performConcurrentWorkOnRoot.bind(null, root),  );  // 同步渲染模式  scheduleSyncCallback(    performSyncWorkOnRoot.bind(null, root),  )

它們兩個其實都是對Scheduler中scheduleCallback的封裝,只不過傳入的優先級不同而已,前者是傳遞的是已經本次更新的lane計算得出的調度優先級,后者傳遞的是最高級別的優先級。另外的區別是,前者直接將任務交給Scheduler,而后者先將任務放到SchedulerWithReactIntegration.old(new).js自己的同步隊列中,再將執行同步隊列的函數交給Scheduler,以最高優先級進行調度,由于傳入了最高優先級,意味著它將會是立即過期的任務,會立即執行掉它,這樣能夠保證在下一次事件循環中執行掉任務。

function scheduleCallback(    reactPriorityLevel: ReactPriorityLevel,    callback: SchedulerCallback,    options: SchedulerCallbackOptions | void | null,  ) {    // 將react的優先級翻譯成Scheduler的優先級    const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);    // 調用Scheduler的scheduleCallback,傳入優先級進行調度    return Scheduler_scheduleCallback(priorityLevel, callback, options);  } function scheduleSyncCallback(callback: SchedulerCallback) {    if (syncQueue === null) {      syncQueue = [callback];      // 以最高優先級去調度刷新syncQueue的函數      immediateQueueCallbackNode = Scheduler_scheduleCallback(        Scheduler_ImmediatePriority,        flushSyncCallbackQueueImpl,      );    } else {      syncQueue.push(callback);    }    return fakeCallbackNode;  }

Scheduler中的優先級

說到優先級,我們來看一下Scheduler自己的優先級級別,它為任務定義了以下幾種級別的優先級:

export const NoPriority = 0; // 沒有任何優先級  export const ImmediatePriority = 1; // 立即執行的優先級,級別最高  export const UserBlockingPriority = 2; // 用戶阻塞級別的優先級  export const NormalPriority = 3; // 正常的優先級  export const LowPriority = 4; // 較低的優先級  export const IdlePriority = 5; // 優先級最低,表示任務可以閑置

任務優先級的作用已經提到過,它是計算任務過期時間的重要依據,事關過期任務在taskQueue中的排序。

// 不同優先級對應的不同的任務過期時間間隔  var IMMEDIATE_PRIORITY_TIMEOUT = -1;  var USER_BLOCKING_PRIORITY_TIMEOUT = 250;  var NORMAL_PRIORITY_TIMEOUT = 5000;  var LOW_PRIORITY_TIMEOUT = 10000;  // Never times out  var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; ...   // 計算過期時間(scheduleCallback函數中的內容)  var timeout;  switch (priorityLevel) {  case ImmediatePriority:    timeout = IMMEDIATE_PRIORITY_TIMEOUT;    break;  case UserBlockingPriority:    timeout = USER_BLOCKING_PRIORITY_TIMEOUT;    break;  case IdlePriority:    timeout = IDLE_PRIORITY_TIMEOUT;    break;  case LowPriority:    timeout = LOW_PRIORITY_TIMEOUT;    break;  case NormalPriority:  default:    timeout = NORMAL_PRIORITY_TIMEOUT;    break;  }  // startTime可暫且認為是當前時間  var expirationTime = startTime + timeout;

可見,過期時間是任務開始時間加上timeout,而這個timeout則是通過任務優先級計算得出。

React中更全面的優先級講解在我寫的這一篇文章中:React中的優先級

調度入口 - scheduleCallback

通過上面的梳理,我們知道Scheduler中的scheduleCallback是調度流程開始的關鍵點。在進入這個調度入口之前,我們先來認識一下Scheduler中的任務是什么形式:

var newTask = {      id: taskIdCounter++,      // 任務函數      callback,      // 任務優先級      priorityLevel,      // 任務開始的時間      startTime,      // 任務的過期時間      expirationTime,      // 在小頂堆隊列中排序的依據      sortIndex: -1,    };
  •  callback:真正的任務函數,重點,也就是外部傳入的任務函數,例如構建fiber樹的任務函數:performConcurrentWorkOnRoot

  •  priorityLevel:任務優先級,參與計算任務過期時間

  •  startTime:表示任務開始的時間,影響它在timerQueue中的排序

  •  expirationTime:表示任務何時過期,影響它在taskQueue中的排序

  •  sortIndex:在小頂堆隊列中排序的依據,在區分好任務是過期或非過期之后,sortIndex會被賦值為expirationTime或startTime,為兩個小頂堆的隊列(taskQueue,timerQueue)提供排序依據

真正的重點是callback,作為任務函數,它的執行結果會影響到任務完成狀態的判斷,后面我們會講到,暫時先無需關注。現在我們先來看看scheduleCallback做的事情:它負責生成調度任務、根據任務是否過期將任務放入timerQueue或taskQueue,然后觸發調度行為,讓任務進入調度。完整代碼如下:

function unstable_scheduleCallback(priorityLevel, callback, options) {    // 獲取當前時間,它是計算任務開始時間、過期時間和判斷任務是否過期的依據    var currentTime = getCurrentTime();    // 確定任務開始時間    var startTime;    // 從options中嘗試獲取delay,也就是推遲時間    if (typeof options === 'object' && options !== null) {      var delay = options.delay;      if (typeof delay === 'number' && delay > 0) {        // 如果有delay,那么任務開始時間就是當前時間加上delay        startTime = currentTime + delay;      } else {        // 沒有delay,任務開始時間就是當前時間,也就是任務需要立刻開始        startTime = currentTime;      }    } else {      startTime = currentTime;    }    // 計算timeout    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 ms        break;      case LowPriority:        timeout = LOW_PRIORITY_TIMEOUT; // 10000        break;      case NormalPriority:      default:        timeout = NORMAL_PRIORITY_TIMEOUT; // 5000        break;    }    // 計算任務的過期時間,任務開始時間 + timeout    // 若是立即執行的優先級(ImmediatePriority),    // 它的過期時間是startTime - 1,意味著立刻就過期    var expirationTime = startTime + timeout;    // 創建調度任務    var newTask = {      id: taskIdCounter++,      // 真正的任務函數,重點      callback,      // 任務優先級      priorityLevel,      // 任務開始的時間,表示任務何時才能執行      startTime,      // 任務的過期時間      expirationTime,      // 在小頂堆隊列中排序的依據      sortIndex: -1,    };    // 下面的if...else判斷各自分支的含義是:    // 如果任務未過期,則將 newTask 放入timerQueue, 調用requestHostTimeout,    // 目的是在timerQueue中排在最前面的任務的開始時間的時間點檢查任務是否過期,    // 過期則立刻將任務加入taskQueue,開始調度   // 如果任務已過期,則將 newTask 放入taskQueue,調用requestHostCallback,    // 開始調度執行taskQueue中的任務    if (startTime > currentTime) {      // 任務未過期,以開始時間作為timerQueue排序的依據      newTask.sortIndex = startTime;      push(timerQueue, newTask);      if (peek(taskQueue) === null && newTask === peek(timerQueue)) {       // 如果現在taskQueue中沒有任務,并且當前的任務是timerQueue中排名最靠前的那一個        // 那么需要檢查timerQueue中有沒有需要放到taskQueue中的任務,這一步通過調用        // requestHostTimeout實現        if (isHostTimeoutScheduled) {          // 因為即將調度一個requestHostTimeout,所以如果之前已經調度了,那么取消掉          cancelHostTimeout();        } else {          isHostTimeoutScheduled = true;        }        // 調用requestHostTimeout實現任務的轉移,開啟調度        requestHostTimeout(handleTimeout, startTime - currentTime);      }    } else {      // 任務已經過期,以過期時間作為taskQueue排序的依據      newTask.sortIndex = expirationTime;      push(taskQueue, newTask);      // 開始執行任務,使用flushWork去執行taskQueue      if (!isHostCallbackScheduled && !isPerformingWork) {        isHostCallbackScheduled = true;        requestHostCallback(flushWork);      }    }    return newTask;  }

這個過程中的重點是任務過期與否的處理。

針對未過期任務,會放入timerQueue,并按照開始時間排列,然后調用requestHostTimeout,為的是等一會,等到了timerQueue中那個應該最早開始的任務(排在第一個的任務)的開始時間,再去檢查它是否過期,如果它過期則放到taskQueue中,這樣任務就可以被執行了,否則繼續等。這個過程通過handleTimeout完成。

handleTimeout的職責是:

  •  調用advanceTimers,檢查timerQueue隊列中過期的任務,放到taskQueue中。

  •  檢查是否已經開始調度,如尚未調度,檢查taskQueue中是否已經有任務:

    •   如果有,而且現在是空閑的,說明之前的advanceTimers已經將過期任務放到了taskQueue,那么現在立即開始調度,執行任務

    •   如果沒有,而且現在是空閑的,說明之前的advanceTimers并沒有檢查到timerQueue中有過期任務,那么再次調用requestHostTimeout重復這一過程。

總之,要把timerQueue中的任務全部都轉移到taskQueue中執行掉才行。

針對已過期任務,在將它放入taskQueue之后,調用requestHostCallback,讓調度者調度一個執行者去執行任務,也就意味著調度流程開始。

開始調度-找出調度者和執行者

Scheduler通過調用requestHostCallback讓任務進入調度流程,回顧上面scheduleCallback最終調用requestHostCallback執行任務的地方:

if (!isHostCallbackScheduled && !isPerformingWork) {    isHostCallbackScheduled = true;    // 開始進行調度    requestHostCallback(flushWork);  }

它既然把flushWork作為入參,那么任務的執行者本質上調用的就是flushWork,我們先不管執行者是如何執行任務的,先關注它是如何被調度的,需要先找出調度者,這需要看一下requestHostCallback的實現:

Scheduler區分了瀏覽器環境和非瀏覽器環境,為requestHostCallback做了兩套不同的實現。在非瀏覽器環境下,使用setTimeout實現.

requestHostCallback = function(cb) {     if (_callback !== null) {       setTimeout(requestHostCallback, 0, cb);     } else {       _callback = cb;       setTimeout(_flushCallback, 0);     }   };

在瀏覽器環境,用MessageChannel實現,關于MessageChannel的介紹就不再贅述。

const channel = new MessageChannel();    const port = channel.port2;    channel.port1.onmessage = performWorkUntilDeadline;    requestHostCallback = function(callback) {      scheduledHostCallback = callback;      if (!isMessageLoopRunning) {        isMessageLoopRunning = true;        port.postMessage(null);      }    };

之所以有兩種實現,是因為非瀏覽器環境不存在屏幕刷新率,沒有幀的概念,也就不會有時間片,這與在瀏覽器環境下執行任務有本質區別,因為非瀏覽器環境基本不胡有用戶交互,所以該場景下不判斷任務執行時間是否超出了時間片限制,而瀏覽器環境任務的執行會有時間片的限制。除了這一點之外,雖然兩種環境下實現方式不一樣,但是做的事情大致相同。

先看非瀏覽器環境,它將入參(執行任務的函數)存儲到內部的變量_callback上,然后調度_flushCallback去執行這個此變量_callback,taskQueue被清空。

再看瀏覽器環境,它將入參(執行任務的函數)存到內部的變量scheduledHostCallback上,然后通過MessageChannel的port去發送一個消息,讓channel.port1的監聽函數performWorkUntilDeadline得以執行。performWorkUntilDeadline內部會執行掉scheduledHostCallback,最后taskQueue被清空。

通過上面的描述,可以很清楚得找出調度者:非瀏覽器環境是setTimeout,瀏覽器環境是port.postMessage。而兩個環境的執行者也顯而易見,前者是_flushCallback,后者是performWorkUntilDeadline,執行者做的事情都是去調用實際的任務執行函數。

因為本文圍繞Scheduler的時間片調度行為展開,所以主要探討瀏覽器環境下的調度行為,performWorkUntilDeadline涉及到調用任務執行函數去執行任務,這個過程中會涉及任務的中斷和恢復、任務完成狀態的判斷,接下來的內容將重點對這兩點進行講解。

任務執行 - 從performWorkUntilDeadline說起

在文章開頭的原理概述中提到過performWorkUntilDeadline作為執行者,它的作用是按照時間片的限制去中斷任務,并通知調度者再次調度一個新的執行者去繼續任務。按照這種認知去看它的實現,會很清晰。

const performWorkUntilDeadline = () => {      if (scheduledHostCallback !== null) {        // 獲取當前時間        const currentTime = getCurrentTime();        // 計算deadline,deadline會參與到        // shouldYieldToHost(根據時間片去限制任務執行)的計算中        deadline = currentTime + yieldInterval;        // hasTimeRemaining表示任務是否還有剩余時間,        // 它和時間片一起限制任務的執行。如果沒有時間,        // 或者任務的執行時間超出時間片限制了,那么中斷任務。        // 它的默認為true,表示一直有剩余時間        // 因為MessageChannel的port在postMessage,        // 是比setTimeout還靠前執行的宏任務,這意味著        // 在這一幀開始時,總是會有剩余時間        // 所以現在中斷任務只看時間片的了        const hasTimeRemaining = true;        try {          // scheduledHostCallback去執行任務的函數,          // 當任務因為時間片被打斷時,它會返回true,表示          // 還有任務,所以會再讓調度者調度一個執行者          // 繼續執行任務          const hasMoreWork = scheduledHostCallback(            hasTimeRemaining,            currentTime,          );          if (!hasMoreWork) {            // 如果沒有任務了,停止調度            isMessageLoopRunning = false;            scheduledHostCallback = null;          } else {            // 如果還有任務,繼續讓調度者調度執行者,便于繼續            // 完成任務            port.postMessage(null);          }        } catch (error) {          port.postMessage(null);          throw error;        }      } else {        isMessageLoopRunning = false;      }      needsPaint = false;    };

performWorkUntilDeadline內部調用的是scheduledHostCallback,它早在開始調度的時候就被requestHostCallback賦值為了flushWork,具體可以翻到上面回顧一下requestHostCallback的實現。

flushWork作為真正去執行任務的函數,它會循環taskQueue,逐一調用里面的任務函數。我們看一下flushWork具體做了什么。

function flushWork(hasTimeRemaining, initialTime) {    ...    return workLoop(hasTimeRemaining, initialTime);   ...  }

它調用了workLoop,并將其調用的結果return了出去。那么現在任務執行的核心內容看來就在workLoop中了。workLoop的調用使得任務最終被執行。

任務中斷和恢復

要理解workLoop,需要回顧Scheduler的功能之一:通過時間片限制任務的執行時間。那么既然任務的執行被限制了,它肯定有可能是尚未完成的,如果未完成被中斷,那么需要將它恢復。

所以時間片下的任務執行具備下面的重要特點:會被中斷,也會被恢復。

不難推測出,workLoop作為實際執行任務的函數,它做的事情肯定與任務的中斷恢復有關。我們先看一下它的結構:

function workLoop(hasTimeRemaining, initialTime) {    // 獲取taskQueue中排在最前面的任務    currentTask = peek(taskQueue);    while (currentTask !== null) {      if (currentTask.expirationTime > currentTime &&       (!hasTimeRemaining || shouldYieldToHost())) {         // break掉while循環         break     }      ...      // 執行任務      ...      // 任務執行完畢,從隊列中刪除      pop(taskQueue);      // 獲取下一個任務,繼續循環      currentTask = peek(taskQueue);    }    if (currentTask !== null) {      // 如果currentTask不為空,說明是時間片的限制導致了任務中斷      // return 一個 true告訴外部,此時任務還未執行完,還有任務,      // 翻譯成英文就是hasMoreWork      return true;    } else {      // 如果currentTask為空,說明taskQueue隊列中的任務已經都      // 執行完了,然后從timerQueue中找任務,調用requestHostTimeout      // 去把task放到taskQueue中,到時會再次發起調度,但是這次,      // 會先return false,告訴外部當前的taskQueue已經清空,      // 先停止執行任務,也就是終止任務調度     const firstTimer = peek(timerQueue);      if (firstTimer !== null) {        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);      }        return false;    }  }

workLoop中可以分為兩大部分:循環taskQueue執行任務 和 任務狀態的判斷。

循環taskQueue執行任務

暫且不管任務如何執行,只關注任務如何被時間片限制,workLoop中:

if (currentTask.expirationTime > currentTime &&       (!hasTimeRemaining || shouldYieldToHost())) {     // break掉while循環     break  }

currentTask就是當前正在執行的任務,它中止的判斷條件是:任務并未過期,但已經沒有剩余時間了(由于hasTimeRemaining一直為true,這與MessageChannel作為宏任務的執行時機有關,我們忽略這個判斷條件,只看時間片),或者應該讓出執行權給主線程(時間片的限制),也就是說currentTask執行得好好的,可是時間不允許,那只能先break掉本次while循環,使得本次循環下面currentTask執行的邏輯都不能被執行到(此處是中斷任務的關鍵)。但是被break的只是while循環,while下部還是會判斷currentTask的狀態。

由于它只是被中止了,所以currentTask不可能是null,那么會return一個true告訴外部還沒完事呢(此處是恢復任務的關鍵),否則說明全部的任務都已經執行完了,taskQueue已經被清空了,return一個false好讓外部終止本次調度。而workLoop的執行結果會被flushWork return出去,flushWork實際上是scheduledHostCallback,當performWorkUntilDeadline檢測到scheduledHostCallback的返回值(hasMoreWork)為false時,就會停止調度。

回顧performWorkUntilDeadline中的行為,可以很清晰地將任務中斷恢復的機制串聯起來:

const performWorkUntilDeadline = () => {     ...     const hasTimeRemaining = true;     // scheduledHostCallback去執行任務的函數,     // 當任務因為時間片被打斷時,它會返回true,表示     // 還有任務,所以會再讓調度者調度一個執行者     // 繼續執行任務     const hasMoreWork = scheduledHostCallback(       hasTimeRemaining,       currentTime,     );     if (!hasMoreWork) {       // 如果沒有任務了,停止調度       isMessageLoopRunning = false;       scheduledHostCallback = null;     } else {       // 如果還有任務,繼續讓調度者調度執行者,便于繼續       // 完成任務       port.postMessage(null);     }   };

當任務被打斷之后,performWorkUntilDeadline會再讓調度者調用一個執行者,繼續執行這個任務,直到任務完成。但是這里有一個重點是如何判斷該任務是否完成呢?這就需要研究workLoop中執行任務的那部分邏輯。

判斷單個任務的完成狀態

任務的中斷恢復是一個重復的過程,該過程會一直重復到任務完成。所以判斷任務是否完成非常重要,而任務未完成則會重復執行任務函數。

我們可以用遞歸函數做類比,如果沒到遞歸邊界,就重復調用自己。這個遞歸邊界,就是任務完成的標志。因為遞歸函數所處理的任務就是它本身,可以很方便地把任務完成作為遞歸邊界去結束任務,但是Scheduler中的workLoop與遞歸不同的是,它只是一個執行任務的,這個任務并不是它自己產生的,而是外部的(比如它去執行React的工作循環渲染fiber樹),它可以做到重復執行任務函數,但邊界(即任務是否完成)卻無法像遞歸那樣直接獲取,只能依賴任務函數的返回值去判斷。即:若任務函數返回值為函數,那么就說明當前任務尚未完成,需要繼續調用任務函數,否則任務完成。workLoop就是通過這樣的辦法判斷單個任務的完成狀態。

在真正講解workLoop中的執行任務的邏輯之前,我們用一個例子來理解一下判斷任務完成狀態的核心。

有一個任務calculate,負責把currentResult每次加1,一直到3為止。當沒到3的時候,calculate不是去調用它自身,而是將自身return出去,一旦到了3,return的是null。這樣外部才可以知道calculate是否已經完成了任務。

const result = 3  let currentResult = 0  function calculate() {      currentResult++      if (currentResult < result) {          return calculate      }      return null  }

上面是任務,接下來我們模擬一下調度,去執行calculate。但執行應該是基于時間片的,為了觀察效果,只用setInterval去模擬因為時間片中止恢復任務的機制(相當粗糙的模擬,只需明白這是時間片的模擬即可,重點關注任務完成狀態的判斷),1秒執行它一次,即一次只完成全部任務的三分之一。

另外Scheduler中有兩個隊列去管理任務,我們暫且只用一個隊列(taskQueue)存儲任務。除此之外還需要三個角色:把任務加入調度的函數(調度入口scheduleCallback)、開始調度的函數(requestHostCallback)、執行任務的函數(workLoop,關鍵邏輯所在)。

const result = 3  let currentResult = 0  function calculate() {      currentResult++      if (currentResult < result) {          return calculate      }      return null  }  // 存放任務的隊列  const taskQueue = []  // 存放模擬時間片的定時器  let interval  // 調度入口----------------------------------------  const scheduleCallback = (task, priority) => {      // 創建一個專屬于調度器的任務      const taskItem = {          callback: task,          priority      }      // 向隊列中添加任務      taskQueue.push(taskItem)      // 優先級影響到任務在隊列中的排序,將優先級最高的任務排在最前面      taskQueue.sort((a, b) => (a.priority - b.priority))      // 開始執行任務,調度開始      requestHostCallback(workLoop)  }  // 開始調度-----------------------------------------  const requestHostCallback = cb => {      interval = setInterval(cb, 1000)  }  // 執行任務-----------------------------------------  const workLoop = () => {      // 從隊列中取出任務      const currentTask = taskQueue[0]      // 獲取真正的任務函數,即calculate      const taskCallback = currentTask.callback      // 判斷任務函數否是函數,若是,執行它,將返回值更新到currentTask的callback中      // 所以,taskCallback是上一階段執行的返回值,若它是函數類型,則說明上一次執行返回了函數      // 類型,說明任務尚未完成,本次繼續執行這個函數,否則說明任務完成。      if (typeof taskCallback === 'function') {          currentTask.callback = taskCallback()          console.log('正在執行任務,當前的currentResult 是', currentResult);      } else {          // 任務完成。將當前的這個任務從taskQueue中移除,并清除定時器          console.log('任務完成,最終的 currentResult 是', currentResult);          taskQueue.shift()          clearInterval(interval)      }  }  // 把calculate加入調度,也就意味著調度開始  scheduleCallback(calculate, 1)

最終的執行結果如下:

正在執行任務,當前的currentResult 是 1  正在執行任務,當前的currentResult 是 2  正在執行任務,當前的currentResult 是 3  任務完成,最終的 currentResult 是 3

可見,如果沒有加到3,那么calculate會return它自己,workLoop若判斷返回值為function,說明任務還未完成,它就會繼續調用任務函數去完成任務。

這個例子只保留了workLoop中判斷任務完成狀態的邏輯,其余的地方并不完善,要以真正的的workLoop為準,現在讓我們貼出它的全部代碼,完整地看一下真正的實現:

function workLoop(hasTimeRemaining, initialTime) {    let currentTime = initialTime;    // 開始執行前檢查一下timerQueue中的過期任務,    // 放到taskQueue中    advanceTimers(currentTime);    // 獲取taskQueue中最緊急的任務    currentTask = peek(taskQueue);    // 循環taskQueue,執行任務    while (      currentTask !== null &&      !(enableSchedulerDebugging && isSchedulerPaused)    ) {      if (        currentTask.expirationTime > currentTime &&        (!hasTimeRemaining || shouldYieldToHost())      ) {        // 時間片的限制,中斷任務        break;      }      // 執行任務 ---------------------------------------------------      // 獲取任務的執行函數,這個callback就是React傳給Scheduler      // 的任務。例如:performConcurrentWorkOnRoot      const callback = currentTask.callback;      if (typeof callback === 'function') {        // 如果執行函數為function,說明還有任務可做,調用它        currentTask.callback = null;        // 獲取任務的優先級        currentPriorityLevel = currentTask.priorityLevel;        // 任務是否過期        const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;        // 獲取任務函數的執行結果        const continuationCallback = callback(didUserCallbackTimeout);        if (typeof continuationCallback === 'function') {          // 檢查callback的執行結果返回的是不是函數,如果返回的是函數,則將這個函數作為當前任務新的回調。          // concurrent模式下,callback是performConcurrentWorkOnRoot,其內部根據當前調度的任務          // 是否相同,來決定是否返回自身,如果相同,則說明還有任務沒做完,返回自身,其作為新的callback          // 被放到當前的task上。while循環完成一次之后,檢查shouldYieldToHost,如果需要讓出執行權,          // 則中斷循環,走到下方,判斷currentTask不為null,返回true,說明還有任務,回到performWorkUntilDeadline          // 中,判斷還有任務,繼續port.postMessage(null),調用監聽函數performWorkUntilDeadline(執行者),          // 繼續調用workLoop行任務          // 將返回值繼續賦值給currentTask.callback,為得是下一次能夠繼續執行callback,          // 獲取它的返回值,繼續判斷任務是否完成。          currentTask.callback = continuationCallback;        } else {          if (currentTask === peek(taskQueue)) {            pop(taskQueue);          }        }        advanceTimers(currentTime);      } else {        pop(taskQueue);      }      // 從taskQueue中繼續獲取任務,如果上一個任務未完成,那么它將不會      // 被從隊列剔除,所以獲取到的currentTask還是上一個任務,會繼續      // 去執行它      currentTask = peek(taskQueue);    }    // return 的結果會作為 performWorkUntilDeadline    // 中判斷是否還需要再次發起調度的依據    if (currentTask !== null) {      return true;    } else {      // 若任務完成,去timerQueue中找需要最早開始執行的那個任務      // 調度requestHostTimeout,目的是等到了它的開始事件時把它      // 放到taskQueue中,再次調度      const firstTimer = peek(timerQueue);      if (firstTimer !== null) {        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);      }      return false;    }  }

所以,workLoop是通過判斷任務函數的返回值去識別任務的完成狀態的。

總結一下判斷任務完成狀態與任務執行的整體關系:當開始調度后,調度者調度執行者去執行任務,實際上是執行任務上的callback(也就是任務函數)。如果執行者判斷callback返回值為一個function,說明未完成,那么會將返回的這個function再次賦值給任務的callback,由于任務還未完成,所以并不會被剔除出taskQueue,currentTask獲取到的還是它,while循環到下一次還是會繼續執行這個任務,直到任務完成出隊,才會繼續下一個。

另外有一個點需要提一下,就是構建fiber樹的任務函數:performConcurrentWorkOnRoot,它接受的參數是fiberRoot。

function performConcurrentWorkOnRoot(root) {    ...  }

在workLoop中它會被這樣調用(callback即為performConcurrentWorkOnRoot):

const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;  const continuationCallback = callback(didUserCallbackTimeout);

didUserCallbackTimeout明顯是boolean類型的值,并不是fiberRoot,但performConcurrentWorkOnRoot卻能正常調用。這是因為在開始調度,以及后續的return自身的時候,都在bind的時候將root傳進去了。

// 調度的時候  scheduleCallback(    schedulerPriorityLevel,    performConcurrentWorkOnRoot.bind(null, root),  );  // 其內部return自身的時候  function performConcurrentWorkOnRoot(root) {    ...    if (root.callbackNode === originalCallbackNode) {      return performConcurrentWorkOnRoot.bind(null, root);    }    return null;  }

這樣的話,再給它傳參數調用它,那這個參數只能作為后續的參數被接收到,performConcurrentWorkOnRoot中接收到的第一個參數還是bind時傳入的那個root,這個特點與bind的實現有關。可以跑一下下面的這個簡單例子:

function test(root, b) {      console.log(root, b)  }  function runTest() {     return test.bind(null, 'root')  }  runTest()(false)  // 結果:root false

以上,是Scheduler執行任務時的兩大核心邏輯:任務的中斷與恢復 & 任務完成狀態的判斷。它們協同合作,若任務未完成就中斷了任務,那么調度的新執行者會恢復執行該任務,直到它完成。到此,Scheduler的核心部分已經寫完了,下面是取消調度的邏輯。

取消調度

通過上面的內容我們知道,任務執行實際上是執行的任務的callback,當callback是function的時候去執行它,當它為null的時候會發生什么?當前的任務會被剔除出taskQueue,讓我們再來看一下workLoop函數:

function workLoop(hasTimeRemaining, initialTime) {    ...   // 獲取taskQueue中最緊急的任務    currentTask = peek(taskQueue);    while (currentTask !== null) {      ...      const callback = currentTask.callback;      if (typeof callback === 'function') {        // 執行任務      } else {        // 如果callback為null,將任務出隊        pop(taskQueue);      }      currentTask = peek(taskQueue);    }    ...  }

所以取消調度的關鍵就是將當前這個任務的callback設置為null。

function unstable_cancelCallback(task) {    ...    task.callback = null;  }

為什么設置callback為null就能取消任務調度呢?因為在workLoop中,如果callback是null會被移出taskQueue,所以當前的這個任務就不會再被執行了。它取消的是當前任務的執行,while循環還會繼續執行下一個任務。

取消任務在React的場景是什么呢?當一個更新任務正在進行的時候,突然有高優先級任務進來了,那么就要取消掉這個正在進行的任務,這只是眾多場景中的一種。

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {    ...    if (existingCallbackNode !== null) {      const existingCallbackPriority = root.callbackPriority;      if (existingCallbackPriority === newCallbackPriority) {        return;      }      // 取消掉原有的任務      cancelCallback(existingCallbackNode);    }    ...  }

總結

Scheduler用任務優先級去實現多任務的管理,優先解決高優任務,用任務的持續調度來解決時間片造成的單個任務中斷恢復問題。任務函數的執行結果為是否應該結束當前任務的調度提供參考,另外,在有限的時間片內完成任務的一部分,也為瀏覽器響應交互與完成任務提供了保障。

到此,關于“React的調度機制原理是什么”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

丰台区| 河西区| 唐河县| 深水埗区| 原平市| 宁强县| 新宾| 永德县| 崇州市| 墨竹工卡县| 安国市| 施秉县| 绍兴县| 玉树县| 凌源市| 申扎县| 洪洞县| 孟州市| 眉山市| 青河县| 嵊泗县| 汶上县| 兴文县| 云龙县| 合作市| 开江县| 牟定县| 隆回县| 沾化县| 颍上县| 江阴市| 齐齐哈尔市| 郧西县| 青浦区| 女性| 永平县| 阜南县| 伊宁市| 津市市| 沁水县| 岑巩县|