日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
一篇長(zhǎng)文幫你徹底搞懂React的調(diào)度機(jī)制原理

 點(diǎn)擊進(jìn)入React源碼調(diào)試倉庫。

成都創(chuàng)新互聯(lián)公司專業(yè)為企業(yè)提供槐蔭網(wǎng)站建設(shè)、槐蔭做網(wǎng)站、槐蔭網(wǎng)站設(shè)計(jì)、槐蔭網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁設(shè)計(jì)與制作、槐蔭企業(yè)網(wǎng)站模板建站服務(wù),十年槐蔭做網(wǎng)站經(jīng)驗(yàn),不只是建網(wǎng)站,更提供有價(jià)值的思路和整體網(wǎng)絡(luò)服務(wù)。

Scheduler作為一個(gè)獨(dú)立的包,可以獨(dú)自承擔(dān)起任務(wù)調(diào)度的職責(zé),你只需要將任務(wù)和任務(wù)的優(yōu)先級(jí)交給它,它就可以幫你管理任務(wù),安排任務(wù)的執(zhí)行。這就是React和Scheduler配合工作的模式。

對(duì)于多個(gè)任務(wù),它會(huì)先執(zhí)行優(yōu)先級(jí)高的。聚焦到單個(gè)任務(wù)的執(zhí)行上,會(huì)被Scheduler有節(jié)制地去執(zhí)行。換句話說,線程只有一個(gè),它不會(huì)一直占用著線程去執(zhí)行任務(wù)。而是執(zhí)行一會(huì),中斷一下,如此往復(fù)。用這樣的模式,來避免一直占用有限的資源執(zhí)行耗時(shí)較長(zhǎng)的任務(wù),解決用戶操作時(shí)頁面卡頓的問題,實(shí)現(xiàn)更快的響應(yīng)。

我們可以從中梳理出Scheduler中兩個(gè)重要的行為:多個(gè)任務(wù)的管理、單個(gè)任務(wù)的執(zhí)行控制。

基本概念

為了實(shí)現(xiàn)上述的兩個(gè)行為,它引入兩個(gè)概念:任務(wù)優(yōu)先級(jí) 、 時(shí)間片。

任務(wù)優(yōu)先級(jí)讓任務(wù)按照自身的緊急程度排序,這樣可以讓優(yōu)先級(jí)最高的任務(wù)最先被執(zhí)行到。

時(shí)間片規(guī)定的是單個(gè)任務(wù)在這一幀內(nèi)最大的執(zhí)行時(shí)間,任務(wù)一旦執(zhí)行時(shí)間超過時(shí)間片,則會(huì)被打斷,有節(jié)制地執(zhí)行任務(wù)。這樣可以保證頁面不會(huì)因?yàn)槿蝿?wù)連續(xù)執(zhí)行的時(shí)間過長(zhǎng)而產(chǎn)生卡頓。

原理概述

基于任務(wù)優(yōu)先級(jí)和時(shí)間片的概念,Scheduler圍繞著它的核心目標(biāo) - 任務(wù)調(diào)度,衍生出了兩大核心功能:任務(wù)隊(duì)列管理 和 時(shí)間片下任務(wù)的中斷和恢復(fù)。

任務(wù)隊(duì)列管理

任務(wù)隊(duì)列管理對(duì)應(yīng)了Scheduler的多任務(wù)管理這一行為。在Scheduler內(nèi)部,把任務(wù)分成了兩種:未過期的和已過期的,分別用兩個(gè)隊(duì)列存儲(chǔ),前者存到timerQueue中,后者存到taskQueue中。

如何區(qū)分任務(wù)是否過期?

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

不同隊(duì)列中的任務(wù)如何排序?

當(dāng)任務(wù)一個(gè)個(gè)入隊(duì)的時(shí)候,自然要對(duì)它們進(jìn)行排序,保證緊急的任務(wù)排在前面,所以排序的依據(jù)就是任務(wù)的緊急程度。而taskQueue和timerQueue中任務(wù)緊急程度的判定標(biāo)準(zhǔn)是有區(qū)別的。

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

任務(wù)入隊(duì)兩個(gè)隊(duì)列,之后呢?

如果放到了taskQueue,那么立即調(diào)度一個(gè)函數(shù)去循環(huán)taskQueue,挨個(gè)執(zhí)行里面的任務(wù)。

如果放到了timerQueue,那么說明它里面的任務(wù)都不會(huì)立即執(zhí)行,那就等到了timerQueue里面排在第一個(gè)任務(wù)的開始時(shí)間,看這個(gè)任務(wù)是否過期,如果是,則把任務(wù)從timerQueue中拿出來放入taskQueue,調(diào)度一個(gè)函數(shù)去循環(huán)它,執(zhí)行掉里面的任務(wù);否則過一會(huì)繼續(xù)檢查這第一個(gè)任務(wù)是否過期。

任務(wù)隊(duì)列管理相對(duì)于單個(gè)任務(wù)的執(zhí)行,是宏觀層面的概念,它利用任務(wù)的優(yōu)先級(jí)去管理任務(wù)隊(duì)列中的任務(wù)順序,始終讓最緊急的任務(wù)被優(yōu)先處理。

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

單個(gè)任務(wù)的中斷以及恢復(fù)對(duì)應(yīng)了Scheduler的單個(gè)任務(wù)執(zhí)行控制這一行為。在循環(huán)taskQueue執(zhí)行每一個(gè)任務(wù)時(shí),如果某個(gè)任務(wù)執(zhí)行時(shí)間過長(zhǎng),達(dá)到了時(shí)間片限制的時(shí)間,那么該任務(wù)必須中斷,以便于讓位給更重要的事情(如瀏覽器繪制),等事情完成,再恢復(fù)執(zhí)行任務(wù)。

例如這個(gè)例子,點(diǎn)擊按鈕渲染140000個(gè)DOM節(jié)點(diǎn),為的是讓React通過scheduler調(diào)度一個(gè)耗時(shí)較長(zhǎng)的更新任務(wù)。同時(shí)拖動(dòng)方塊,這是為了模擬用戶交互。更新任務(wù)會(huì)占用線程去執(zhí)行任務(wù),用戶交互要也要占用線程去響應(yīng)頁面,這就決定了它們兩個(gè)是互斥的關(guān)系。在React的concurrent模式下,通過Scheduler調(diào)度的更新任務(wù)遇到用戶交互之后,會(huì)是下面動(dòng)圖里的效果。

執(zhí)行React任務(wù)和頁面響應(yīng)交互這兩件事情是互斥的,但因?yàn)镾cheduler可以利用時(shí)間片中斷React任務(wù),然后讓出線程給瀏覽器去繪制,所以一開始在fiber樹的構(gòu)建階段,拖動(dòng)方塊會(huì)得到及時(shí)的反饋。但是后面卡了一下,這是因?yàn)閒iber樹構(gòu)建完成,進(jìn)入了同步的commit階段,導(dǎo)致交互卡頓。分析頁面的渲染過程可以非常直觀地看到通過時(shí)間片的控制。主線程被讓出去進(jìn)行頁面的繪制(Painting和Rendering,綠色和紫色的部分)。

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ù)被中斷了,還有一部分沒完成,但現(xiàn)在必須讓位給更重要的事情,你再調(diào)度一個(gè)執(zhí)行者吧,好讓這個(gè)任務(wù)能在之后被繼續(xù)執(zhí)行完(任務(wù)的恢復(fù))。于是,調(diào)度者知道了任務(wù)還沒完成,需要繼續(xù)做,它會(huì)再調(diào)度一個(gè)執(zhí)行者去繼續(xù)完成這個(gè)任務(wù)。

通過執(zhí)行者和調(diào)度者的配合,可以實(shí)現(xiàn)任務(wù)的中斷和恢復(fù)。

原理小結(jié)

Scheduler管理著taskQueue和timerQueue兩個(gè)隊(duì)列,它會(huì)定期將timerQueue中的過期任務(wù)放到taskQueue中,然后讓調(diào)度者通知執(zhí)行者循環(huán)taskQueue執(zhí)行掉每一個(gè)任務(wù)。執(zhí)行者控制著每個(gè)任務(wù)的執(zhí)行,一旦某個(gè)任務(wù)的執(zhí)行時(shí)間超出時(shí)間片的限制。就會(huì)被中斷,然后當(dāng)前的執(zhí)行者退場(chǎng),退場(chǎng)之前會(huì)通知調(diào)度者再去調(diào)度一個(gè)新的執(zhí)行者繼續(xù)完成這個(gè)任務(wù),新的執(zhí)行者在執(zhí)行任務(wù)時(shí)依舊會(huì)根據(jù)時(shí)間片中斷任務(wù),然后退場(chǎng),重復(fù)這一過程,直到當(dāng)前這個(gè)任務(wù)徹底完成后,將任務(wù)從taskQueue出隊(duì)。taskQueue中每一個(gè)任務(wù)都被這樣處理,最終完成所有任務(wù),這就是Scheduler的完整工作流程。

這里面有一個(gè)關(guān)鍵點(diǎn),就是執(zhí)行者如何知道這個(gè)任務(wù)到底完成沒完成呢?這是另一個(gè)話題了,也就是判斷任務(wù)的完成狀態(tài)。在講解執(zhí)行者執(zhí)行任務(wù)的細(xì)節(jié)時(shí)會(huì)重點(diǎn)突出。

以上是Scheduler原理的概述,下面開始是對(duì)React和Scheduler聯(lián)合工作機(jī)制的詳細(xì)解讀。涉及React與Scheduler的連接、調(diào)度入口、任務(wù)優(yōu)先級(jí)、任務(wù)過期時(shí)間、任務(wù)中斷和恢復(fù)、判斷任務(wù)的完成狀態(tài)等內(nèi)容。

詳細(xì)流程

在開始之前,我們先看一下React和Scheduler它們二者構(gòu)成的一個(gè)系統(tǒng)的示意圖。

整個(gè)系統(tǒng)分為三部分:

  •  產(chǎn)生任務(wù)的地方:React
  •  React和Scheduler交流的翻譯者:SchedulerWithReactIntegration
  •  任務(wù)的調(diào)度者:Scheduler

React中通過下面的代碼,讓fiber樹的構(gòu)建任務(wù)進(jìn)入調(diào)度流程: 

 
 
 
 
  1. scheduleCallback(  
  2.   schedulerPriorityLevel,  
  3.   performConcurrentWorkOnRoot.bind(null, root),  
  4. ); 

任務(wù)通過翻譯者交給Scheduler,Scheduler進(jìn)行真正的任務(wù)調(diào)度,那么為什么需要一個(gè)翻譯者的角色呢?

React與Scheduler的連接

Scheduler幫助React調(diào)度各種任務(wù),但是本質(zhì)上它們是兩個(gè)完全不耦合的東西,二者各自都有自己的優(yōu)先級(jí)機(jī)制,那么這時(shí)就需要有一個(gè)中間角色將它們連接起來。

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

在執(zhí)行React任務(wù)的重要文件ReactFiberWorkLoop.js中,關(guān)于Scheduler的內(nèi)容都是從SchedulerWithReactIntegration.old(new).js導(dǎo)入的。它可以理解成是React和Scheduler之間的橋梁。 

 
 
 
 
  1. // ReactFiberWorkLoop.js  
  2. import {  
  3.   scheduleCallback,  
  4.   cancelCallback,  
  5.   getCurrentPriorityLevel,  
  6.   runWithPriority,  
  7.   shouldYield,  
  8.   requestPaint,  
  9.   now,  
  10.   NoPriority as NoSchedulerPriority,  
  11.   ImmediatePriority as ImmediateSchedulerPriority,  
  12.   UserBlockingPriority as UserBlockingSchedulerPriority,  
  13.   NormalPriority as NormalSchedulerPriority,  
  14.   flushSyncCallbackQueue,  
  15.   scheduleSyncCallback,  
  16. } from './SchedulerWithReactIntegration.old'; 

SchedulerWithReactIntegration.old(new).js通過封裝Scheduler的內(nèi)容,對(duì)React提供兩種調(diào)度入口函數(shù):scheduleCallback 和 scheduleSyncCallback。任務(wù)通過調(diào)度入口函數(shù)進(jìn)入調(diào)度流程。

例如,fiber樹的構(gòu)建任務(wù)在concurrent模式下通過scheduleCallback完成調(diào)度,在同步渲染模式下由scheduleSyncCallback完成。 

 
 
 
 
  1. // concurrentMode  
  2. // 將本次更新任務(wù)的優(yōu)先級(jí)轉(zhuǎn)化為調(diào)度優(yōu)先級(jí)  
  3. // schedulerPriorityLevel為調(diào)度優(yōu)先級(jí)  
  4. const schedulerPriorityLevel = lanePriorityToSchedulerPriority(  
  5.   newCallbackPriority,  
  6. );  
  7. // concurrent模式  
  8. scheduleCallback(  
  9.   schedulerPriorityLevel,  
  10.   performConcurrentWorkOnRoot.bind(null, root),  
  11. );  
  12. // 同步渲染模式  
  13. scheduleSyncCallback(  
  14.   performSyncWorkOnRoot.bind(null, root),  

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

 
 
 
 
  1. function scheduleCallback(  
  2.   reactPriorityLevel: ReactPriorityLevel,  
  3.   callback: SchedulerCallback,  
  4.   options: SchedulerCallbackOptions | void | null,  
  5. ) {  
  6.   // 將react的優(yōu)先級(jí)翻譯成Scheduler的優(yōu)先級(jí)  
  7.   const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);  
  8.   // 調(diào)用Scheduler的scheduleCallback,傳入優(yōu)先級(jí)進(jìn)行調(diào)度  
  9.   return Scheduler_scheduleCallback(priorityLevel, callback, options);  
  10. function scheduleSyncCallback(callback: SchedulerCallback) {  
  11.   if (syncQueue === null) {  
  12.     syncQueue = [callback];  
  13.     // 以最高優(yōu)先級(jí)去調(diào)度刷新syncQueue的函數(shù)  
  14.     immediateQueueCallbackNode = Scheduler_scheduleCallback(  
  15.       Scheduler_ImmediatePriority,  
  16.       flushSyncCallbackQueueImpl,  
  17.     );  
  18.   } else {  
  19.     syncQueue.push(callback);  
  20.   }  
  21.   return fakeCallbackNode;  

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

說到優(yōu)先級(jí),我們來看一下Scheduler自己的優(yōu)先級(jí)級(jí)別,它為任務(wù)定義了以下幾種級(jí)別的優(yōu)先級(jí): 

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

任務(wù)優(yōu)先級(jí)的作用已經(jīng)提到過,它是計(jì)算任務(wù)過期時(shí)間的重要依據(jù),事關(guān)過期任務(wù)在taskQueue中的排序。 

 
 
 
 
  1. // 不同優(yōu)先級(jí)對(duì)應(yīng)的不同的任務(wù)過期時(shí)間間隔  
  2. var IMMEDIATE_PRIORITY_TIMEOUT = -1;  
  3. var USER_BLOCKING_PRIORITY_TIMEOUT = 250;  
  4. var NORMAL_PRIORITY_TIMEOUT = 5000;  
  5. var LOW_PRIORITY_TIMEOUT = 10000;  
  6. // Never times out  
  7. var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; 
  8. ...   
  9. // 計(jì)算過期時(shí)間(scheduleCallback函數(shù)中的內(nèi)容)  
  10. var timeout;  
  11. switch (priorityLevel) {  
  12. case ImmediatePriority:  
  13.   timeout = IMMEDIATE_PRIORITY_TIMEOUT;  
  14.   break;  
  15. case UserBlockingPriority:  
  16.   timeout = USER_BLOCKING_PRIORITY_TIMEOUT;  
  17.   break;  
  18. case IdlePriority:  
  19.   timeout = IDLE_PRIORITY_TIMEOUT;  
  20.   break;  
  21. case LowPriority:  
  22.   timeout = LOW_PRIORITY_TIMEOUT;  
  23.   break;  
  24. case NormalPriority:  
  25. default:  
  26.   timeout = NORMAL_PRIORITY_TIMEOUT;  
  27.   break;  
  28. }  
  29. // startTime可暫且認(rèn)為是當(dāng)前時(shí)間  
  30. var expirationTime = startTime + timeout; 

可見,過期時(shí)間是任務(wù)開始時(shí)間加上timeout,而這個(gè)timeout則是通過任務(wù)優(yōu)先級(jí)計(jì)算得出。

React中更全面的優(yōu)先級(jí)講解在我寫的這一篇文章中:React中的優(yōu)先級(jí)

調(diào)度入口 - scheduleCallback

通過上面的梳理,我們知道Scheduler中的scheduleCallback是調(diào)度流程開始的關(guān)鍵點(diǎn)。在進(jìn)入這個(gè)調(diào)度入口之前,我們先來認(rèn)識(shí)一下Scheduler中的任務(wù)是什么形式: 

 
 
 
 
  1. var newTask = {  
  2.     id: taskIdCounter++,  
  3.     // 任務(wù)函數(shù)  
  4.     callback,  
  5.     // 任務(wù)優(yōu)先級(jí)  
  6.     priorityLevel,  
  7.     // 任務(wù)開始的時(shí)間  
  8.     startTime,  
  9.     // 任務(wù)的過期時(shí)間  
  10.     expirationTime,  
  11.     // 在小頂堆隊(duì)列中排序的依據(jù)  
  12.     sortIndex: -1,  
  13.   }; 
  •  callback:真正的任務(wù)函數(shù),重點(diǎn),也就是外部傳入的任務(wù)函數(shù),例如構(gòu)建fiber樹的任務(wù)函數(shù):performConcurrentWorkOnRoot
  •  priorityLevel:任務(wù)優(yōu)先級(jí),參與計(jì)算任務(wù)過期時(shí)間
  •  startTime:表示任務(wù)開始的時(shí)間,影響它在timerQueue中的排序
  •  expirationTime:表示任務(wù)何時(shí)過期,影響它在taskQueue中的排序
  •  sortIndex:在小頂堆隊(duì)列中排序的依據(jù),在區(qū)分好任務(wù)是過期或非過期之后,sortIndex會(huì)被賦值為expirationTime或startTime,為兩個(gè)小頂堆的隊(duì)列(taskQueue,timerQueue)提供排序依據(jù)

真正的重點(diǎn)是callback,作為任務(wù)函數(shù),它的執(zhí)行結(jié)果會(huì)影響到任務(wù)完成狀態(tài)的判斷,后面我們會(huì)講到,暫時(shí)先無需關(guān)注。現(xiàn)在我們先來看看scheduleCallback做的事情:它負(fù)責(zé)生成調(diào)度任務(wù)、根據(jù)任務(wù)是否過期將任務(wù)放入timerQueue或taskQueue,然后觸發(fā)調(diào)度行為,讓任務(wù)進(jìn)入調(diào)度。完整代碼如下: 

 
 
 
 
  1. function unstable_scheduleCallback(priorityLevel, callback, options) {  
  2.   // 獲取當(dāng)前時(shí)間,它是計(jì)算任務(wù)開始時(shí)間、過期時(shí)間和判斷任務(wù)是否過期的依據(jù)  
  3.   var currentTime = getCurrentTime();  
  4.   // 確定任務(wù)開始時(shí)間  
  5.   var startTime;  
  6.   // 從options中嘗試獲取delay,也就是推遲時(shí)間  
  7.   if (typeof options === 'object' && options !== null) {  
  8.     var delay = options.delay;  
  9.     if (typeof delay === 'number' && delay > 0) {  
  10.       // 如果有delay,那么任務(wù)開始時(shí)間就是當(dāng)前時(shí)間加上delay  
  11.       startTime = currentTime + delay;  
  12.     } else {  
  13.       // 沒有delay,任務(wù)開始時(shí)間就是當(dāng)前時(shí)間,也就是任務(wù)需要立刻開始  
  14.       startTime = currentTime;  
  15.     }  
  16.   } else {  
  17.     startTime = currentTime;  
  18.   }  
  19.   // 計(jì)算timeout  
  20.   var timeout;  
  21.   switch (priorityLevel) {  
  22.     case ImmediatePriority:  
  23.       timeout = IMMEDIATE_PRIORITY_TIMEOUT; // -1  
  24.       break;  
  25.     case UserBlockingPriority:  
  26.       timeout = USER_BLOCKING_PRIORITY_TIMEOUT; // 250  
  27.       break;  
  28.     case IdlePriority:  
  29.       timeout = IDLE_PRIORITY_TIMEOUT; // 1073741823 ms  
  30.       break;  
  31.     case LowPriority:  
  32.       timeout = LOW_PRIORITY_TIMEOUT; // 10000  
  33.       break;  
  34.     case NormalPriority:  
  35.     default:  
  36.       timeout = NORMAL_PRIORITY_TIMEOUT; // 5000  
  37.       break;  
  38.   }  
  39.   // 計(jì)算任務(wù)的過期時(shí)間,任務(wù)開始時(shí)間 + timeout  
  40.   // 若是立即執(zhí)行的優(yōu)先級(jí)(ImmediatePriority),  
  41.   // 它的過期時(shí)間是startTime - 1,意味著立刻就過期  
  42.   var expirationTime = startTime + timeout;  
  43.   // 創(chuàng)建調(diào)度任務(wù)  
  44.   var newTask = {  
  45.     id: taskIdCounter++,  
  46.     // 真正的任務(wù)函數(shù),重點(diǎn)  
  47.     callback,  
  48.     // 任務(wù)優(yōu)先級(jí)  
  49.     priorityLevel,  
  50.     // 任務(wù)開始的時(shí)間,表示任務(wù)何時(shí)才能執(zhí)行  
  51.     startTime,  
  52.     // 任務(wù)的過期時(shí)間  
  53.     expirationTime,  
  54.     // 在小頂堆隊(duì)列中排序的依據(jù)  
  55.     sortIndex: -1,  
  56.   };  
  57.   // 下面的if...else判斷各自分支的含義是:  
  58.   // 如果任務(wù)未過期,則將 newTask 放入timerQueue, 調(diào)用requestHostTimeout,  
  59.   // 目的是在timerQueue中排在最前面的任務(wù)的開始時(shí)間的時(shí)間點(diǎn)檢查任務(wù)是否過期,  
  60.   // 過期則立刻將任務(wù)加入taskQueue,開始調(diào)度 
  61.   // 如果任務(wù)已過期,則將 newTask 放入taskQueue,調(diào)用requestHostCallback,  
  62.   // 開始調(diào)度執(zhí)行taskQueue中的任務(wù)  
  63.   if (startTime > currentTime) {  
  64.     // 任務(wù)未過期,以開始時(shí)間作為timerQueue排序的依據(jù)  
  65.     newTask.sortIndex = startTime;  
  66.     push(timerQueue, newTask);  
  67.     if (peek(taskQueue) === null && newTask === peek(timerQueue)) { 
  68.       // 如果現(xiàn)在taskQueue中沒有任務(wù),并且當(dāng)前的任務(wù)是timerQueue中排名最靠前的那一個(gè)  
  69.       // 那么需要檢查timerQueue中有沒有需要放到taskQueue中的任務(wù),這一步通過調(diào)用  
  70.       // requestHostTimeout實(shí)現(xiàn)  
  71.       if (isHostTimeoutScheduled) {  
  72.         // 因?yàn)榧磳⒄{(diào)度一個(gè)requestHostTimeout,所以如果之前已經(jīng)調(diào)度了,那么取消掉  
  73.         cancelHostTimeout();  
  74.       } else {  
  75.         isHostTimeoutScheduled = true;  
  76.       }  
  77.       // 調(diào)用requestHostTimeout實(shí)現(xiàn)任務(wù)的轉(zhuǎn)移,開啟調(diào)度  
  78.       requestHostTimeout(handleTimeout, startTime - currentTime);  
  79.     }  
  80.   } else {  
  81.     // 任務(wù)已經(jīng)過期,以過期時(shí)間作為taskQueue排序的依據(jù)  
  82.     newTask.sortIndex = expirationTime;  
  83.     push(taskQueue, newTask);  
  84.     // 開始執(zhí)行任務(wù),使用flushWork去執(zhí)行taskQueue  
  85.     if (!isHostCallbackScheduled && !isPerformingWork) {  
  86.       isHostCallbackScheduled = true;  
  87.       requestHostCallback(flushWork);  
  88.     }  
  89.   }  
  90.   return newTask;  

這個(gè)過程中的重點(diǎn)是任務(wù)過期與否的處理。

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

handleTimeout的職責(zé)是:

  •  調(diào)用advanceTimers,檢查timerQueue隊(duì)列中過期的任務(wù),放到taskQueue中。
  •  檢查是否已經(jīng)開始調(diào)度,如尚未調(diào)度,檢查taskQueue中是否已經(jīng)有任務(wù):
    •   如果有,而且現(xiàn)在是空閑的,說明之前的advanceTimers已經(jīng)將過期任務(wù)放到了taskQueue,那么現(xiàn)在立即開始調(diào)度,執(zhí)行任務(wù)
    •   如果沒有,而且現(xiàn)在是空閑的,說明之前的advanceTimers并沒有檢查到timerQueue中有過期任務(wù),那么再次調(diào)用requestHostTimeout重復(fù)這一過程。

總之,要把timerQueue中的任務(wù)全部都轉(zhuǎn)移到taskQueue中執(zhí)行掉才行。

針對(duì)已過期任務(wù),在將它放入taskQueue之后,調(diào)用requestHostCallback,讓調(diào)度者調(diào)度一個(gè)執(zhí)行者去執(zhí)行任務(wù),也就意味著調(diào)度流程開始。

開始調(diào)度-找出調(diào)度者和執(zhí)行者

Scheduler通過調(diào)用requestHostCallback讓任務(wù)進(jìn)入調(diào)度流程,回顧上面scheduleCallback最終調(diào)用requestHostCallback執(zhí)行任務(wù)的地方: 

 
 
 
 
  1. if (!isHostCallbackScheduled && !isPerformingWork) {  
  2.   isHostCallbackScheduled = true;  
  3.   // 開始進(jìn)行調(diào)度  
  4.   requestHostCallback(flushWork);  

它既然把flushWork作為入?yún)ⅲ敲慈蝿?wù)的執(zhí)行者本質(zhì)上調(diào)用的就是flushWork,我們先不管執(zhí)行者是如何執(zhí)行任務(wù)的,先關(guān)注它是如何被調(diào)度的,需要先找出調(diào)度者,這需要看一下requestHostCallback的實(shí)現(xiàn):

Scheduler區(qū)分了瀏覽器環(huán)境和非瀏覽器環(huán)境,為requestHostCallback做了兩套不同的實(shí)現(xiàn)。在非瀏覽器環(huán)境下,使用setTimeout實(shí)現(xiàn). 

 
 
 
 
  1. requestHostCallback = function(cb) {  
  2.    if (_callback !== null) {  
  3.      setTimeout(requestHostCallback, 0, cb);  
  4.    } else {  
  5.      _callback = cb;  
  6.      setTimeout(_flushCallback, 0);  
  7.    }  
  8.  }; 

在瀏覽器環(huán)境,用MessageChannel實(shí)現(xiàn),關(guān)于MessageChannel的介紹就不再贅述。 

 
 
 
 
  1. const channel = new MessageChannel();  
  2.   const port = channel.port2;  
  3.   channel.port1.onmessage = performWorkUntilDeadline;  
  4.   requestHostCallback = function(callback) {  
  5.     scheduledHostCallback = callback;  
  6.     if (!isMessageLoopRunning) {  
  7.       isMessageLoopRunning = true;  
  8.       port.postMessage(null);  
  9.     }  
  10.   }; 

之所以有兩種實(shí)現(xiàn),是因?yàn)榉菫g覽器環(huán)境不存在屏幕刷新率,沒有幀的概念,也就不會(huì)有時(shí)間片,這與在瀏覽器環(huán)境下執(zhí)行任務(wù)有本質(zhì)區(qū)別,因?yàn)榉菫g覽器環(huán)境基本不胡有用戶交互,所以該場(chǎng)景下不判斷任務(wù)執(zhí)行時(shí)間是否超出了時(shí)間片限制,而瀏覽器環(huán)境任務(wù)的執(zhí)行會(huì)有時(shí)間片的限制。除了這一點(diǎn)之外,雖然兩種環(huán)境下實(shí)現(xiàn)方式不一樣,但是做的事情大致相同。

先看非瀏覽器環(huán)境,它將入?yún)ⅲ▓?zhí)行任務(wù)的函數(shù))存儲(chǔ)到內(nèi)部的變量_callback上,然后調(diào)度_flushCallback去執(zhí)行這個(gè)此變量_callback,taskQueue被清空。

再看瀏覽器環(huán)境,它將入?yún)ⅲ▓?zhí)行任務(wù)的函數(shù))存到內(nèi)部的變量scheduledHostCallback上,然后通過MessageChannel的port去發(fā)送一個(gè)消息,讓channel.port1的監(jiān)聽函數(shù)performWorkUntilDeadline得以執(zhí)行。performWorkUntilDeadline內(nèi)部會(huì)執(zhí)行掉scheduledHostCallback,最后taskQueue被清空。

通過上面的描述,可以很清楚得找出調(diào)度者:非瀏覽器環(huán)境是setTimeout,瀏覽器環(huán)境是port.postMessage。而兩個(gè)環(huán)境的執(zhí)行者也顯而易見,前者是_flushCallback,后者是performWorkUntilDeadline,執(zhí)行者做的事情都是去調(diào)用實(shí)際的任務(wù)執(zhí)行函數(shù)。

因?yàn)楸疚膰@Scheduler的時(shí)間片調(diào)度行為展開,所以主要探討瀏覽器環(huán)境下的調(diào)度行為,performWorkUntilDeadline涉及到調(diào)用任務(wù)執(zhí)行函數(shù)去執(zhí)行任務(wù),這個(gè)過程中會(huì)涉及任務(wù)的中斷和恢復(fù)、任務(wù)完成狀態(tài)的判斷,接下來的內(nèi)容將重點(diǎn)對(duì)這兩點(diǎn)進(jìn)行講解。

任務(wù)執(zhí)行 - 從performWorkUntilDeadline說起

在文章開頭的原理概述中提到過performWorkUntilDeadline作為執(zhí)行者,它的作用是按照時(shí)間片的限制去中斷任務(wù),并通知調(diào)度者再次調(diào)度一個(gè)新的執(zhí)行者去繼續(xù)任務(wù)。按照這種認(rèn)知去看它的實(shí)現(xiàn),會(huì)很清晰。 

 
 
 
 
  1. const performWorkUntilDeadline = () => {  
  2.     if (scheduledHostCallback !== null) {  
  3.       // 獲取當(dāng)前時(shí)間  
  4.       const currentTime = getCurrentTime();  
  5.       // 計(jì)算deadline,deadline會(huì)參與到  
  6.       // shouldYieldToHost(根據(jù)時(shí)間片去限制任務(wù)執(zhí)行)的計(jì)算中  
  7.       deadline = currentTime + yieldInterval;  
  8.       // hasTimeRemaining表示任務(wù)是否還有剩余時(shí)間,  
  9.       // 它和時(shí)間片一起限制任務(wù)的執(zhí)行。如果沒有時(shí)間,  
  10.       // 或者任務(wù)的執(zhí)行時(shí)間超出時(shí)間片限制了,那么中斷任務(wù)。  
  11.       // 它的默認(rèn)為true,表示一直有剩余時(shí)間  
  12.       // 因?yàn)镸essageChannel的port在postMessage,  
  13.       // 是比setTimeout還靠前執(zhí)行的宏任務(wù),這意味著  
  14.       // 在這一幀開始時(shí),總是會(huì)有剩余時(shí)間  
  15.       // 所以現(xiàn)在中斷任務(wù)只看時(shí)間片的了  
  16.       const hasTimeRemaining = true;  
  17.       try {  
  18.         // scheduledHostCallback去執(zhí)行任務(wù)的函數(shù),  
  19.         // 當(dāng)任務(wù)因?yàn)闀r(shí)間片被打斷時(shí),它會(huì)返回true,表示  
  20.         // 還有任務(wù),所以會(huì)再讓調(diào)度者調(diào)度一個(gè)執(zhí)行者  
  21.         // 繼續(xù)執(zhí)行任務(wù)  
  22.         const hasMoreWork = scheduledHostCallback(  
  23.           hasTimeRemaining,  
  24.           currentTime,  
  25.         );  
  26.         if (!hasMoreWork) {  
  27.           // 如果沒有任務(wù)了,停止調(diào)度  
  28.           isMessageLoopRunning = false;  
  29.           scheduledHostCallback = null;  
  30.         } else {  
  31.           // 如果還有任務(wù),繼續(xù)讓調(diào)度者調(diào)度執(zhí)行者,便于繼續(xù)  
  32.           // 完成任務(wù)  
  33.           port.postMessage(null);  
  34.         }  
  35.       } catch (error) {  
  36.         port.postMessage(null);  
  37.         throw error;  
  38.       }  
  39.     } else {  
  40.       isMessageLoopRunning = false;  
  41.     }  
  42.     needsPaint = false;  
  43.   }; 

performWorkUntilDeadline內(nèi)部調(diào)用的是scheduledHostCallback,它早在開始調(diào)度的時(shí)候就被requestHostCallback賦值為了flushWork,具體可以翻到上面回顧一下requestHostCallback的實(shí)現(xiàn)。

flushWork作為真正去執(zhí)行任務(wù)的函數(shù),它會(huì)循環(huán)taskQueue,逐一調(diào)用里面的任務(wù)函數(shù)。我們看一下flushWork具體做了什么。 

 
 
 
 
  1. function flushWork(hasTimeRemaining, initialTime) {  
  2.   ...  
  3.   return workLoop(hasTimeRemaining, initialTime); 
  4.   ...  

它調(diào)用了workLoop,并將其調(diào)用的結(jié)果return了出去。那么現(xiàn)在任務(wù)執(zhí)行的核心內(nèi)容看來就在workLoop中了。workLoop的調(diào)用使得任務(wù)最終被執(zhí)行。

任務(wù)中斷和恢復(fù)

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

所以時(shí)間片下的任務(wù)執(zhí)行具備下面的重要特點(diǎn):會(huì)被中斷,也會(huì)被恢復(fù)。

不難推測(cè)出,workLoop作為實(shí)際執(zhí)行任務(wù)的函數(shù),它做的事情肯定與任務(wù)的中斷恢復(fù)有關(guān)。我們先看一下它的結(jié)構(gòu): 

 
 
 
 
  1. function workLoop(hasTimeRemaining, initialTime) {  
  2.   // 獲取taskQueue中排在最前面的任務(wù)  
  3.   currentTask = peek(taskQueue);  
  4.   while (currentTask !== null) {  
  5.     if (currentTask.expirationTime > currentTime &&  
  6.      (!hasTimeRemaining || shouldYieldToHost())) {  
  7.        // break掉while循環(huán)  
  8.        break 
  9.     }  
  10.     ...  
  11.     // 執(zhí)行任務(wù)  
  12.     ...  
  13.     // 任務(wù)執(zhí)行完畢,從隊(duì)列中刪除  
  14.     pop(taskQueue);  
  15.     // 獲取下一個(gè)任務(wù),繼續(xù)循環(huán)  
  16.     currentTask = peek(taskQueue);  
  17.   }  
  18.   if (currentTask !== null) {  
  19.     // 如果currentTask不為空,說明是時(shí)間片的限制導(dǎo)致了任務(wù)中斷  
  20.     // return 一個(gè) true告訴外部,此時(shí)任務(wù)還未執(zhí)行完,還有任務(wù),  
  21.     // 翻譯成英文就是hasMoreWork  
  22.     return true;  
  23.   } else {  
  24.     // 如果currentTask為空,說明taskQueue隊(duì)列中的任務(wù)已經(jīng)都  
  25.     // 執(zhí)行完了,然后從timerQueue中找任務(wù),調(diào)用requestHostTimeout  
  26.     // 去把task放到taskQueue中,到時(shí)會(huì)再次發(fā)起調(diào)度,但是這次,  
  27.     // 會(huì)先return false,告訴外部當(dāng)前的taskQueue已經(jīng)清空,  
  28.     // 先停止執(zhí)行任務(wù),也就是終止任務(wù)調(diào)度 
  29.     const firstTimer = peek(timerQueue);  
  30.     if (firstTimer !== null) {  
  31.       requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);  
  32.     }    
  33.     return false;  
  34.   }  

workLoop中可以分為兩大部分:循環(huán)taskQueue執(zhí)行任務(wù) 和 任務(wù)狀態(tài)的判斷。

循環(huán)taskQueue執(zhí)行任務(wù)

暫且不管任務(wù)如何執(zhí)行,只關(guān)注任務(wù)如何被時(shí)間片限制,workLoop中: 

 
 
 
 
  1. if (currentTask.expirationTime > currentTime &&  
  2.      (!hasTimeRemaining || shouldYieldToHost())) {  
  3.    // break掉while循環(huán)  
  4.    break  

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

由于它只是被中止了,所以currentTask不可能是null,那么會(huì)return一個(gè)true告訴外部還沒完事呢(此處是恢復(fù)任務(wù)的關(guān)鍵),否則說明全部的任務(wù)都已經(jīng)執(zhí)行完了,taskQueue已經(jīng)被清空了,return一個(gè)false好讓外部終止本次調(diào)度。而workLoop的執(zhí)行結(jié)果會(huì)被flushWork return出去,flushWork實(shí)際上是scheduledHostCallback,當(dāng)performWorkUntilDeadline檢測(cè)到scheduledHostCallback的返回值(hasMoreWork)為false時(shí),就會(huì)停止調(diào)度。

回顧performWorkUntilDeadline中的行為,可以很清晰地將任務(wù)中斷恢復(fù)的機(jī)制串聯(lián)起來: 

 
 
 
 
  1. const performWorkUntilDeadline = () => {  
  2.    ...  
  3.    const hasTimeRemaining = true;  
  4.    // scheduledHostCallback去執(zhí)行任務(wù)的函數(shù),  
  5.    // 當(dāng)任務(wù)因?yàn)闀r(shí)間片被打斷時(shí),它會(huì)返回true,表示  
  6.    // 還有任務(wù),所以會(huì)再讓調(diào)度者調(diào)度一個(gè)執(zhí)行者  
  7.    // 繼續(xù)執(zhí)行任務(wù)  
  8.    const hasMoreWork = scheduledHostCallback(  
  9.      hasTimeRemaining,  
  10.      currentTime,  
  11.    );  
  12.    if (!hasMoreWork) {  
  13.      // 如果沒有任務(wù)了,停止調(diào)度  
  14.      isMessageLoopRunning = false;  
  15.      scheduledHostCallback = null;  
  16.    } else { 
  17.       // 如果還有任務(wù),繼續(xù)讓調(diào)度者調(diào)度執(zhí)行者,便于繼續(xù)  
  18.      // 完成任務(wù)  
  19.      port.postMessage(null);  
  20.    }  
  21.  }; 

當(dāng)任務(wù)被打斷之后,performWorkUntilDeadline會(huì)再讓調(diào)度者調(diào)用一個(gè)執(zhí)行者,繼續(xù)執(zhí)行這個(gè)任務(wù),直到任務(wù)完成。但是這里有一個(gè)重點(diǎn)是如何判斷該任務(wù)是否完成呢?這就需要研究workLoop中執(zhí)行任務(wù)的那部分邏輯。

判斷單個(gè)任務(wù)的完成狀態(tài)

任務(wù)的中斷恢復(fù)是一個(gè)重復(fù)的過程,該過程會(huì)一直重復(fù)到任務(wù)完成。所以判斷任務(wù)是否完成非常重要,而任務(wù)未完成則會(huì)重復(fù)執(zhí)行任務(wù)函數(shù)。

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

在真正講解workLoop中的執(zhí)行任務(wù)的邏輯之前,我們用一個(gè)例子來理解一下判斷任務(wù)完成狀態(tài)的核心。

有一個(gè)任務(wù)calculate,負(fù)責(zé)把currentResult每次加1,一直到3為止。當(dāng)沒到3的時(shí)候,calculate不是去調(diào)用它自身,而是將自身return出去,一旦到了3,return的是null。這樣外部才可以知道calculate是否已經(jīng)完成了任務(wù)。 

 
 
 
 
  1. const result = 3  
  2. let currentResult = 0  
  3. function calculate() {  
  4.     currentResult++  
  5.     if (currentResult < result) {  
  6.         return calculate  
  7.     }  
  8.     return null  

上面是任務(wù),接下來我們模擬一下調(diào)度,去執(zhí)行calculate。但執(zhí)行應(yīng)該是基于時(shí)間片的,為了觀察效果,只用setInterval去模擬因?yàn)闀r(shí)間片中止恢復(fù)任務(wù)的機(jī)制(相當(dāng)粗糙的模擬,只需明白這是時(shí)間片的模擬即可,重點(diǎn)關(guān)注任務(wù)完成狀態(tài)的判斷),1秒執(zhí)行它一次,即一次只完成全部任務(wù)的三分之一。

另外Scheduler中有兩個(gè)隊(duì)列去管理任務(wù),我們暫且只用一個(gè)隊(duì)列(taskQueue)存儲(chǔ)任務(wù)。除此之外還需要三個(gè)角色:把任務(wù)加入調(diào)度的函數(shù)(調(diào)度入口scheduleCallback)、開始調(diào)度的函數(shù)(requestHostCallback)、執(zhí)行任務(wù)的函數(shù)(workLoop,關(guān)鍵邏輯所在)。 

 
 
 
 
  1. const result = 3  
  2. let currentResult = 0  
  3. function calculate() {  
  4.     currentResult++  
  5.     if (currentResult < result) {  
  6.         return calculate  
  7.     }  新聞標(biāo)題:一篇長(zhǎng)文幫你徹底搞懂React的調(diào)度機(jī)制原理
    新聞來源:http://m.5511xx.com/article/dhshjjh.html