新聞中心
點擊進入React源碼調試倉庫。

成都創(chuàng)新互聯(lián)公司專業(yè)為企業(yè)提供槐蔭網站建設、槐蔭做網站、槐蔭網站設計、槐蔭網站制作等企業(yè)網站建設、網頁設計與制作、槐蔭企業(yè)網站模板建站服務,十年槐蔭做網站經驗,不只是建網站,更提供有價值的思路和整體網絡服務。
Scheduler作為一個獨立的包,可以獨自承擔起任務調度的職責,你只需要將任務和任務的優(yōu)先級交給它,它就可以幫你管理任務,安排任務的執(zhí)行。這就是React和Scheduler配合工作的模式。
對于多個任務,它會先執(zhí)行優(yōu)先級高的。聚焦到單個任務的執(zhí)行上,會被Scheduler有節(jié)制地去執(zhí)行。換句話說,線程只有一個,它不會一直占用著線程去執(zhí)行任務。而是執(zhí)行一會,中斷一下,如此往復。用這樣的模式,來避免一直占用有限的資源執(zhí)行耗時較長的任務,解決用戶操作時頁面卡頓的問題,實現(xiàn)更快的響應。
我們可以從中梳理出Scheduler中兩個重要的行為:多個任務的管理、單個任務的執(zhí)行控制。
基本概念
為了實現(xiàn)上述的兩個行為,它引入兩個概念:任務優(yōu)先級 、 時間片。
任務優(yōu)先級讓任務按照自身的緊急程度排序,這樣可以讓優(yōu)先級最高的任務最先被執(zhí)行到。
時間片規(guī)定的是單個任務在這一幀內最大的執(zhí)行時間,任務一旦執(zhí)行時間超過時間片,則會被打斷,有節(jié)制地執(zhí)行任務。這樣可以保證頁面不會因為任務連續(xù)執(zhí)行的時間過長而產生卡頓。
原理概述
基于任務優(yōu)先級和時間片的概念,Scheduler圍繞著它的核心目標 - 任務調度,衍生出了兩大核心功能:任務隊列管理 和 時間片下任務的中斷和恢復。
任務隊列管理
任務隊列管理對應了Scheduler的多任務管理這一行為。在Scheduler內部,把任務分成了兩種:未過期的和已過期的,分別用兩個隊列存儲,前者存到timerQueue中,后者存到taskQueue中。
如何區(qū)分任務是否過期?
用任務的開始時間(startTime)和當前時間(currentTime)作比較。開始時間大于當前時間,說明未過期,放到timerQueue;開始時間小于等于當前時間,說明已過期,放到taskQueue。
不同隊列中的任務如何排序?
當任務一個個入隊的時候,自然要對它們進行排序,保證緊急的任務排在前面,所以排序的依據就是任務的緊急程度。而taskQueue和timerQueue中任務緊急程度的判定標準是有區(qū)別的。
- taskQueue中,依據任務的過期時間(expirationTime)排序,過期時間越早,說明越緊急,過期時間小的排在前面。過期時間根據任務優(yōu)先級計算得出,優(yōu)先級越高,過期時間越早。
- timerQueue中,依據任務的開始時間(startTime)排序,開始時間越早,說明會越早開始,開始時間小的排在前面。任務進來的時候,開始時間默認是當前時間,如果進入調度的時候傳了延遲時間,開始時間則是當前時間與延遲時間的和。
任務入隊兩個隊列,之后呢?
如果放到了taskQueue,那么立即調度一個函數去循環(huán)taskQueue,挨個執(zhí)行里面的任務。
如果放到了timerQueue,那么說明它里面的任務都不會立即執(zhí)行,那就等到了timerQueue里面排在第一個任務的開始時間,看這個任務是否過期,如果是,則把任務從timerQueue中拿出來放入taskQueue,調度一個函數去循環(huán)它,執(zhí)行掉里面的任務;否則過一會繼續(xù)檢查這第一個任務是否過期。
任務隊列管理相對于單個任務的執(zhí)行,是宏觀層面的概念,它利用任務的優(yōu)先級去管理任務隊列中的任務順序,始終讓最緊急的任務被優(yōu)先處理。
單個任務的中斷以及恢復
單個任務的中斷以及恢復對應了Scheduler的單個任務執(zhí)行控制這一行為。在循環(huán)taskQueue執(zhí)行每一個任務時,如果某個任務執(zhí)行時間過長,達到了時間片限制的時間,那么該任務必須中斷,以便于讓位給更重要的事情(如瀏覽器繪制),等事情完成,再恢復執(zhí)行任務。
例如這個例子,點擊按鈕渲染140000個DOM節(jié)點,為的是讓React通過scheduler調度一個耗時較長的更新任務。同時拖動方塊,這是為了模擬用戶交互。更新任務會占用線程去執(zhí)行任務,用戶交互要也要占用線程去響應頁面,這就決定了它們兩個是互斥的關系。在React的concurrent模式下,通過Scheduler調度的更新任務遇到用戶交互之后,會是下面動圖里的效果。
執(zhí)行React任務和頁面響應交互這兩件事情是互斥的,但因為Scheduler可以利用時間片中斷React任務,然后讓出線程給瀏覽器去繪制,所以一開始在fiber樹的構建階段,拖動方塊會得到及時的反饋。但是后面卡了一下,這是因為fiber樹構建完成,進入了同步的commit階段,導致交互卡頓。分析頁面的渲染過程可以非常直觀地看到通過時間片的控制。主線程被讓出去進行頁面的繪制(Painting和Rendering,綠色和紫色的部分)。
Scheduler要實現(xiàn)這樣的調度效果需要兩個角色:任務的調度者、任務的執(zhí)行者。調度者調度一個執(zhí)行者,執(zhí)行者去循環(huán)taskQueue,逐個執(zhí)行任務。當某個任務的執(zhí)行時間比較長,執(zhí)行者會根據時間片中斷任務執(zhí)行,然后告訴調度者:我現(xiàn)在正執(zhí)行的這個任務被中斷了,還有一部分沒完成,但現(xiàn)在必須讓位給更重要的事情,你再調度一個執(zhí)行者吧,好讓這個任務能在之后被繼續(xù)執(zhí)行完(任務的恢復)。于是,調度者知道了任務還沒完成,需要繼續(xù)做,它會再調度一個執(zhí)行者去繼續(xù)完成這個任務。
通過執(zhí)行者和調度者的配合,可以實現(xiàn)任務的中斷和恢復。
原理小結
Scheduler管理著taskQueue和timerQueue兩個隊列,它會定期將timerQueue中的過期任務放到taskQueue中,然后讓調度者通知執(zhí)行者循環(huán)taskQueue執(zhí)行掉每一個任務。執(zhí)行者控制著每個任務的執(zhí)行,一旦某個任務的執(zhí)行時間超出時間片的限制。就會被中斷,然后當前的執(zhí)行者退場,退場之前會通知調度者再去調度一個新的執(zhí)行者繼續(xù)完成這個任務,新的執(zhí)行者在執(zhí)行任務時依舊會根據時間片中斷任務,然后退場,重復這一過程,直到當前這個任務徹底完成后,將任務從taskQueue出隊。taskQueue中每一個任務都被這樣處理,最終完成所有任務,這就是Scheduler的完整工作流程。
這里面有一個關鍵點,就是執(zhí)行者如何知道這個任務到底完成沒完成呢?這是另一個話題了,也就是判斷任務的完成狀態(tài)。在講解執(zhí)行者執(zhí)行任務的細節(jié)時會重點突出。
以上是Scheduler原理的概述,下面開始是對React和Scheduler聯(lián)合工作機制的詳細解讀。涉及React與Scheduler的連接、調度入口、任務優(yōu)先級、任務過期時間、任務中斷和恢復、判斷任務的完成狀態(tài)等內容。
詳細流程
在開始之前,我們先看一下React和Scheduler它們二者構成的一個系統(tǒng)的示意圖。
整個系統(tǒng)分為三部分:
- 產生任務的地方:React
- React和Scheduler交流的翻譯者:SchedulerWithReactIntegration
- 任務的調度者:Scheduler
React中通過下面的代碼,讓fiber樹的構建任務進入調度流程:
- scheduleCallback(
- schedulerPriorityLevel,
- performConcurrentWorkOnRoot.bind(null, root),
- );
任務通過翻譯者交給Scheduler,Scheduler進行真正的任務調度,那么為什么需要一個翻譯者的角色呢?
React與Scheduler的連接
Scheduler幫助React調度各種任務,但是本質上它們是兩個完全不耦合的東西,二者各自都有自己的優(yōu)先級機制,那么這時就需要有一個中間角色將它們連接起來。
實際上,在react-reconciler中提供了這樣一個文件專門去做這樣的工作,它就是SchedulerWithReactIntegration.old(new).js。它將二者的優(yōu)先級翻譯了一下,讓React和Scheduler能讀懂對方。另外,封裝了一些Scheduler中的函數供React使用。
在執(zhí)行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
- // 將本次更新任務的優(yōu)先級轉化為調度優(yōu)先級
- // schedulerPriorityLevel為調度優(yōu)先級
- const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
- newCallbackPriority,
- );
- // concurrent模式
- scheduleCallback(
- schedulerPriorityLevel,
- performConcurrentWorkOnRoot.bind(null, root),
- );
- // 同步渲染模式
- scheduleSyncCallback(
- performSyncWorkOnRoot.bind(null, root),
- )
它們兩個其實都是對Scheduler中scheduleCallback的封裝,只不過傳入的優(yōu)先級不同而已,前者是傳遞的是已經本次更新的lane計算得出的調度優(yōu)先級,后者傳遞的是最高級別的優(yōu)先級。另外的區(qū)別是,前者直接將任務交給Scheduler,而后者先將任務放到SchedulerWithReactIntegration.old(new).js自己的同步隊列中,再將執(zhí)行同步隊列的函數交給Scheduler,以最高優(yōu)先級進行調度,由于傳入了最高優(yōu)先級,意味著它將會是立即過期的任務,會立即執(zhí)行掉它,這樣能夠保證在下一次事件循環(huán)中執(zhí)行掉任務。
- function scheduleCallback(
- reactPriorityLevel: ReactPriorityLevel,
- callback: SchedulerCallback,
- options: SchedulerCallbackOptions | void | null,
- ) {
- // 將react的優(yōu)先級翻譯成Scheduler的優(yōu)先級
- const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
- // 調用Scheduler的scheduleCallback,傳入優(yōu)先級進行調度
- return Scheduler_scheduleCallback(priorityLevel, callback, options);
- }
- function scheduleSyncCallback(callback: SchedulerCallback) {
- if (syncQueue === null) {
- syncQueue = [callback];
- // 以最高優(yōu)先級去調度刷新syncQueue的函數
- immediateQueueCallbackNode = Scheduler_scheduleCallback(
- Scheduler_ImmediatePriority,
- flushSyncCallbackQueueImpl,
- );
- } else {
- syncQueue.push(callback);
- }
- return fakeCallbackNode;
- }
Scheduler中的優(yōu)先級
說到優(yōu)先級,我們來看一下Scheduler自己的優(yōu)先級級別,它為任務定義了以下幾種級別的優(yōu)先級:
- export const NoPriority = 0; // 沒有任何優(yōu)先級
- export const ImmediatePriority = 1; // 立即執(zhí)行的優(yōu)先級,級別最高
- export const UserBlockingPriority = 2; // 用戶阻塞級別的優(yōu)先級
- export const NormalPriority = 3; // 正常的優(yōu)先級
- export const LowPriority = 4; // 較低的優(yōu)先級
- export const IdlePriority = 5; // 優(yōu)先級最低,表示任務可以閑置
任務優(yōu)先級的作用已經提到過,它是計算任務過期時間的重要依據,事關過期任務在taskQueue中的排序。
- // 不同優(yōu)先級對應的不同的任務過期時間間隔
- 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則是通過任務優(yōu)先級計算得出。
React中更全面的優(yōu)先級講解在我寫的這一篇文章中:React中的優(yōu)先級
調度入口 - scheduleCallback
通過上面的梳理,我們知道Scheduler中的scheduleCallback是調度流程開始的關鍵點。在進入這個調度入口之前,我們先來認識一下Scheduler中的任務是什么形式:
- var newTask = {
- id: taskIdCounter++,
- // 任務函數
- callback,
- // 任務優(yōu)先級
- priorityLevel,
- // 任務開始的時間
- startTime,
- // 任務的過期時間
- expirationTime,
- // 在小頂堆隊列中排序的依據
- sortIndex: -1,
- };
- callback:真正的任務函數,重點,也就是外部傳入的任務函數,例如構建fiber樹的任務函數:performConcurrentWorkOnRoot
- priorityLevel:任務優(yōu)先級,參與計算任務過期時間
- startTime:表示任務開始的時間,影響它在timerQueue中的排序
- expirationTime:表示任務何時過期,影響它在taskQueue中的排序
- sortIndex:在小頂堆隊列中排序的依據,在區(qū)分好任務是過期或非過期之后,sortIndex會被賦值為expirationTime或startTime,為兩個小頂堆的隊列(taskQueue,timerQueue)提供排序依據
真正的重點是callback,作為任務函數,它的執(zhí)行結果會影響到任務完成狀態(tài)的判斷,后面我們會講到,暫時先無需關注?,F(xiàn)在我們先來看看scheduleCallback做的事情:它負責生成調度任務、根據任務是否過期將任務放入timerQueue或taskQueue,然后觸發(fā)調度行為,讓任務進入調度。完整代碼如下:
- 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
- // 若是立即執(zhí)行的優(yōu)先級(ImmediatePriority),
- // 它的過期時間是startTime - 1,意味著立刻就過期
- var expirationTime = startTime + timeout;
- // 創(chuàng)建調度任務
- var newTask = {
- id: taskIdCounter++,
- // 真正的任務函數,重點
- callback,
- // 任務優(yōu)先級
- priorityLevel,
- // 任務開始的時間,表示任務何時才能執(zhí)行
- startTime,
- // 任務的過期時間
- expirationTime,
- // 在小頂堆隊列中排序的依據
- sortIndex: -1,
- };
- // 下面的if...else判斷各自分支的含義是:
- // 如果任務未過期,則將 newTask 放入timerQueue, 調用requestHostTimeout,
- // 目的是在timerQueue中排在最前面的任務的開始時間的時間點檢查任務是否過期,
- // 過期則立刻將任務加入taskQueue,開始調度
- // 如果任務已過期,則將 newTask 放入taskQueue,調用requestHostCallback,
- // 開始調度執(zhí)行taskQueue中的任務
- if (startTime > currentTime) {
- // 任務未過期,以開始時間作為timerQueue排序的依據
- newTask.sortIndex = startTime;
- push(timerQueue, newTask);
- if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
- // 如果現(xiàn)在taskQueue中沒有任務,并且當前的任務是timerQueue中排名最靠前的那一個
- // 那么需要檢查timerQueue中有沒有需要放到taskQueue中的任務,這一步通過調用
- // requestHostTimeout實現(xiàn)
- if (isHostTimeoutScheduled) {
- // 因為即將調度一個requestHostTimeout,所以如果之前已經調度了,那么取消掉
- cancelHostTimeout();
- } else {
- isHostTimeoutScheduled = true;
- }
- // 調用requestHostTimeout實現(xiàn)任務的轉移,開啟調度
- requestHostTimeout(handleTimeout, startTime - currentTime);
- }
- } else {
- // 任務已經過期,以過期時間作為taskQueue排序的依據
- newTask.sortIndex = expirationTime;
- push(taskQueue, newTask);
- // 開始執(zhí)行任務,使用flushWork去執(zhí)行taskQueue
- if (!isHostCallbackScheduled && !isPerformingWork) {
- isHostCallbackScheduled = true;
- requestHostCallback(flushWork);
- }
- }
- return newTask;
- }
這個過程中的重點是任務過期與否的處理。
針對未過期任務,會放入timerQueue,并按照開始時間排列,然后調用requestHostTimeout,為的是等一會,等到了timerQueue中那個應該最早開始的任務(排在第一個的任務)的開始時間,再去檢查它是否過期,如果它過期則放到taskQueue中,這樣任務就可以被執(zhí)行了,否則繼續(xù)等。這個過程通過handleTimeout完成。
handleTimeout的職責是:
- 調用advanceTimers,檢查timerQueue隊列中過期的任務,放到taskQueue中。
- 檢查是否已經開始調度,如尚未調度,檢查taskQueue中是否已經有任務:
- 如果有,而且現(xiàn)在是空閑的,說明之前的advanceTimers已經將過期任務放到了taskQueue,那么現(xiàn)在立即開始調度,執(zhí)行任務
- 如果沒有,而且現(xiàn)在是空閑的,說明之前的advanceTimers并沒有檢查到timerQueue中有過期任務,那么再次調用requestHostTimeout重復這一過程。
總之,要把timerQueue中的任務全部都轉移到taskQueue中執(zhí)行掉才行。
針對已過期任務,在將它放入taskQueue之后,調用requestHostCallback,讓調度者調度一個執(zhí)行者去執(zhí)行任務,也就意味著調度流程開始。
開始調度-找出調度者和執(zhí)行者
Scheduler通過調用requestHostCallback讓任務進入調度流程,回顧上面scheduleCallback最終調用requestHostCallback執(zhí)行任務的地方:
- if (!isHostCallbackScheduled && !isPerformingWork) {
- isHostCallbackScheduled = true;
- // 開始進行調度
- requestHostCallback(flushWork);
- }
它既然把flushWork作為入參,那么任務的執(zhí)行者本質上調用的就是flushWork,我們先不管執(zhí)行者是如何執(zhí)行任務的,先關注它是如何被調度的,需要先找出調度者,這需要看一下requestHostCallback的實現(xiàn):
Scheduler區(qū)分了瀏覽器環(huán)境和非瀏覽器環(huán)境,為requestHostCallback做了兩套不同的實現(xiàn)。在非瀏覽器環(huán)境下,使用setTimeout實現(xiàn).
- requestHostCallback = function(cb) {
- if (_callback !== null) {
- setTimeout(requestHostCallback, 0, cb);
- } else {
- _callback = cb;
- setTimeout(_flushCallback, 0);
- }
- };
在瀏覽器環(huán)境,用MessageChannel實現(xiàn),關于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);
- }
- };
之所以有兩種實現(xiàn),是因為非瀏覽器環(huán)境不存在屏幕刷新率,沒有幀的概念,也就不會有時間片,這與在瀏覽器環(huán)境下執(zhí)行任務有本質區(qū)別,因為非瀏覽器環(huán)境基本不胡有用戶交互,所以該場景下不判斷任務執(zhí)行時間是否超出了時間片限制,而瀏覽器環(huán)境任務的執(zhí)行會有時間片的限制。除了這一點之外,雖然兩種環(huán)境下實現(xiàn)方式不一樣,但是做的事情大致相同。
先看非瀏覽器環(huán)境,它將入參(執(zhí)行任務的函數)存儲到內部的變量_callback上,然后調度_flushCallback去執(zhí)行這個此變量_callback,taskQueue被清空。
再看瀏覽器環(huán)境,它將入參(執(zhí)行任務的函數)存到內部的變量scheduledHostCallback上,然后通過MessageChannel的port去發(fā)送一個消息,讓channel.port1的監(jiān)聽函數performWorkUntilDeadline得以執(zhí)行。performWorkUntilDeadline內部會執(zhí)行掉scheduledHostCallback,最后taskQueue被清空。
通過上面的描述,可以很清楚得找出調度者:非瀏覽器環(huán)境是setTimeout,瀏覽器環(huán)境是port.postMessage。而兩個環(huán)境的執(zhí)行者也顯而易見,前者是_flushCallback,后者是performWorkUntilDeadline,執(zhí)行者做的事情都是去調用實際的任務執(zhí)行函數。
因為本文圍繞Scheduler的時間片調度行為展開,所以主要探討瀏覽器環(huán)境下的調度行為,performWorkUntilDeadline涉及到調用任務執(zhí)行函數去執(zhí)行任務,這個過程中會涉及任務的中斷和恢復、任務完成狀態(tài)的判斷,接下來的內容將重點對這兩點進行講解。
任務執(zhí)行 - 從performWorkUntilDeadline說起
在文章開頭的原理概述中提到過performWorkUntilDeadline作為執(zhí)行者,它的作用是按照時間片的限制去中斷任務,并通知調度者再次調度一個新的執(zhí)行者去繼續(xù)任務。按照這種認知去看它的實現(xiàn),會很清晰。
- const performWorkUntilDeadline = () => {
- if (scheduledHostCallback !== null) {
- // 獲取當前時間
- const currentTime = getCurrentTime();
- // 計算deadline,deadline會參與到
- // shouldYieldToHost(根據時間片去限制任務執(zhí)行)的計算中
- deadline = currentTime + yieldInterval;
- // hasTimeRemaining表示任務是否還有剩余時間,
- // 它和時間片一起限制任務的執(zhí)行。如果沒有時間,
- // 或者任務的執(zhí)行時間超出時間片限制了,那么中斷任務。
- // 它的默認為true,表示一直有剩余時間
- // 因為MessageChannel的port在postMessage,
- // 是比setTimeout還靠前執(zhí)行的宏任務,這意味著
- // 在這一幀開始時,總是會有剩余時間
- // 所以現(xiàn)在中斷任務只看時間片的了
- const hasTimeRemaining = true;
- try {
- // scheduledHostCallback去執(zhí)行任務的函數,
- // 當任務因為時間片被打斷時,它會返回true,表示
- // 還有任務,所以會再讓調度者調度一個執(zhí)行者
- // 繼續(xù)執(zhí)行任務
- const hasMoreWork = scheduledHostCallback(
- hasTimeRemaining,
- currentTime,
- );
- if (!hasMoreWork) {
- // 如果沒有任務了,停止調度
- isMessageLoopRunning = false;
- scheduledHostCallback = null;
- } else {
- // 如果還有任務,繼續(xù)讓調度者調度執(zhí)行者,便于繼續(xù)
- // 完成任務
- port.postMessage(null);
- }
- } catch (error) {
- port.postMessage(null);
- throw error;
- }
- } else {
- isMessageLoopRunning = false;
- }
- needsPaint = false;
- };
performWorkUntilDeadline內部調用的是scheduledHostCallback,它早在開始調度的時候就被requestHostCallback賦值為了flushWork,具體可以翻到上面回顧一下requestHostCallback的實現(xiàn)。
flushWork作為真正去執(zhí)行任務的函數,它會循環(huán)taskQueue,逐一調用里面的任務函數。我們看一下flushWork具體做了什么。
- function flushWork(hasTimeRemaining, initialTime) {
- ...
- return workLoop(hasTimeRemaining, initialTime);
- ...
- }
它調用了workLoop,并將其調用的結果return了出去。那么現(xiàn)在任務執(zhí)行的核心內容看來就在workLoop中了。workLoop的調用使得任務最終被執(zhí)行。
任務中斷和恢復
要理解workLoop,需要回顧Scheduler的功能之一:通過時間片限制任務的執(zhí)行時間。那么既然任務的執(zhí)行被限制了,它肯定有可能是尚未完成的,如果未完成被中斷,那么需要將它恢復。
所以時間片下的任務執(zhí)行具備下面的重要特點:會被中斷,也會被恢復。
不難推測出,workLoop作為實際執(zhí)行任務的函數,它做的事情肯定與任務的中斷恢復有關。我們先看一下它的結構:
- function workLoop(hasTimeRemaining, initialTime) {
- // 獲取taskQueue中排在最前面的任務
- currentTask = peek(taskQueue);
- while (currentTask !== null) {
- if (currentTask.expirationTime > currentTime &&
- (!hasTimeRemaining || shouldYieldToHost())) {
- // break掉while循環(huán)
- break
- }
- ...
- // 執(zhí)行任務
- ...
- // 任務執(zhí)行完畢,從隊列中刪除
- pop(taskQueue);
- // 獲取下一個任務,繼續(xù)循環(huán)
- currentTask = peek(taskQueue);
- }
- if (currentTask !== null) {
- // 如果currentTask不為空,說明是時間片的限制導致了任務中斷
- // return 一個 true告訴外部,此時任務還未執(zhí)行完,還有任務,
- // 翻譯成英文就是hasMoreWork
- return true;
- } else {
- // 如果currentTask為空,說明taskQueue隊列中的任務已經都
- // 執(zhí)行完了,然后從timerQueue中找任務,調用requestHostTimeout
- // 去把task放到taskQueue中,到時會再次發(fā)起調度,但是這次,
- // 會先return false,告訴外部當前的taskQueue已經清空,
- // 先停止執(zhí)行任務,也就是終止任務調度
- const firstTimer = peek(timerQueue);
- if (firstTimer !== null) {
- requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
- }
- return false;
- }
- }
workLoop中可以分為兩大部分:循環(huán)taskQueue執(zhí)行任務 和 任務狀態(tài)的判斷。
循環(huán)taskQueue執(zhí)行任務
暫且不管任務如何執(zhí)行,只關注任務如何被時間片限制,workLoop中:
- if (currentTask.expirationTime > currentTime &&
- (!hasTimeRemaining || shouldYieldToHost())) {
- // break掉while循環(huán)
- break
- }
currentTask就是當前正在執(zhí)行的任務,它中止的判斷條件是:任務并未過期,但已經沒有剩余時間了(由于hasTimeRemaining一直為true,這與MessageChannel作為宏任務的執(zhí)行時機有關,我們忽略這個判斷條件,只看時間片),或者應該讓出執(zhí)行權給主線程(時間片的限制),也就是說currentTask執(zhí)行得好好的,可是時間不允許,那只能先break掉本次while循環(huán),使得本次循環(huán)下面currentTask執(zhí)行的邏輯都不能被執(zhí)行到(此處是中斷任務的關鍵)。但是被break的只是while循環(huán),while下部還是會判斷currentTask的狀態(tài)。
由于它只是被中止了,所以currentTask不可能是null,那么會return一個true告訴外部還沒完事呢(此處是恢復任務的關鍵),否則說明全部的任務都已經執(zhí)行完了,taskQueue已經被清空了,return一個false好讓外部終止本次調度。而workLoop的執(zhí)行結果會被flushWork return出去,flushWork實際上是scheduledHostCallback,當performWorkUntilDeadline檢測到scheduledHostCallback的返回值(hasMoreWork)為false時,就會停止調度。
回顧performWorkUntilDeadline中的行為,可以很清晰地將任務中斷恢復的機制串聯(lián)起來:
- const performWorkUntilDeadline = () => {
- ...
- const hasTimeRemaining = true;
- // scheduledHostCallback去執(zhí)行任務的函數,
- // 當任務因為時間片被打斷時,它會返回true,表示
- // 還有任務,所以會再讓調度者調度一個執(zhí)行者
- // 繼續(xù)執(zhí)行任務
- const hasMoreWork = scheduledHostCallback(
- hasTimeRemaining,
- currentTime,
- );
- if (!hasMoreWork) {
- // 如果沒有任務了,停止調度
- isMessageLoopRunning = false;
- scheduledHostCallback = null;
- } else {
- // 如果還有任務,繼續(xù)讓調度者調度執(zhí)行者,便于繼續(xù)
- // 完成任務
- port.postMessage(null);
- }
- };
當任務被打斷之后,performWorkUntilDeadline會再讓調度者調用一個執(zhí)行者,繼續(xù)執(zhí)行這個任務,直到任務完成。但是這里有一個重點是如何判斷該任務是否完成呢?這就需要研究workLoop中執(zhí)行任務的那部分邏輯。
判斷單個任務的完成狀態(tài)
任務的中斷恢復是一個重復的過程,該過程會一直重復到任務完成。所以判斷任務是否完成非常重要,而任務未完成則會重復執(zhí)行任務函數。
我們可以用遞歸函數做類比,如果沒到遞歸邊界,就重復調用自己。這個遞歸邊界,就是任務完成的標志。因為遞歸函數所處理的任務就是它本身,可以很方便地把任務完成作為遞歸邊界去結束任務,但是Scheduler中的workLoop與遞歸不同的是,它只是一個執(zhí)行任務的,這個任務并不是它自己產生的,而是外部的(比如它去執(zhí)行React的工作循環(huán)渲染fiber樹),它可以做到重復執(zhí)行任務函數,但邊界(即任務是否完成)卻無法像遞歸那樣直接獲取,只能依賴任務函數的返回值去判斷。即:若任務函數返回值為函數,那么就說明當前任務尚未完成,需要繼續(xù)調用任務函數,否則任務完成。workLoop就是通過這樣的辦法判斷單個任務的完成狀態(tài)。
在真正講解workLoop中的執(zhí)行任務的邏輯之前,我們用一個例子來理解一下判斷任務完成狀態(tài)的核心。
有一個任務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
- }
上面是任務,接下來我們模擬一下調度,去執(zhí)行calculate。但執(zhí)行應該是基于時間片的,為了觀察效果,只用setInterval去模擬因為時間片中止恢復任務的機制(相當粗糙的模擬,只需明白這是時間片的模擬即可,重點關注任務完成狀態(tài)的判斷),1秒執(zhí)行它一次,即一次只完成全部任務的三分之一。
另外Scheduler中有兩個隊列去管理任務,我們暫且只用一個隊列(taskQueue)存儲任務。除此之外還需要三個角色:把任務加入調度的函數(調度入口scheduleCallback)、開始調度的函數(requestHostCallback)、執(zhí)行任務的函數(workLoop,關鍵邏輯所在)。
- const result = 3
- let currentResult = 0
- function calculate() {
- currentResult++
- if (currentResult < result) {
- return calculate
- }
文章名稱:一篇長文幫你徹底搞懂React的調度機制原理
轉載源于:http://m.fisionsoft.com.cn/article/dhshjjh.html


咨詢
建站咨詢
