React為什么需要Scheduler調(diào)度器原理詳解
正文
最近在重學(xué)React,由于近兩年沒(méi)使用React突然重學(xué)發(fā)現(xiàn)一些很有意思的概念,首先便是React的Scheduler(調(diào)度器) 由于我對(duì)React的概念還停留在React 15之前(就是那個(gè)沒(méi)有hooks的年代),所以接觸Scheduler(調(diào)度器) 讓我感覺(jué)很有意思;
在我印象中React的架構(gòu)分為兩層(React 16 之前)
- Reconciler(協(xié)調(diào)器)—— 負(fù)責(zé)找出變化的組件
- Renderer(渲染器)—— 負(fù)責(zé)將變化的組件渲染到頁(yè)面上
如今增加了Scheduler(調(diào)度器) ,那么調(diào)度器有什么用?調(diào)度器的作用是調(diào)度任務(wù)的優(yōu)先級(jí),高優(yōu)任務(wù)優(yōu)先進(jìn)入Reconciler
我們?yōu)槭裁葱枰猄cheduler(調(diào)度器)
要了解為什么需要Scheduler(調(diào)度器) 我們需要知道以下幾個(gè)痛點(diǎn);
- React在何時(shí)進(jìn)行更新;
- 16之前的React怎樣進(jìn)行更新;
- 16之前的React帶來(lái)的痛點(diǎn);
首先我們講講React何時(shí)進(jìn)行更新,眾所周知主流的瀏覽器的刷新頻率是60HZ,也就是說(shuō)主流的瀏覽器完成一次刷新需要1000/60 ms約等于16.666ms
然后我們需要知道瀏覽器在你開啟一個(gè)頁(yè)面的時(shí)候做了什么;總結(jié)下來(lái)就是一張圖
CSSOM樹的構(gòu)建時(shí)機(jī)與JS的執(zhí)行時(shí)機(jī)是依據(jù)你解析的link標(biāo)簽與script標(biāo)簽來(lái)確認(rèn)的;因?yàn)楫?dāng)React開始更新時(shí)已完成部分工作(開始回流與重繪),所以經(jīng)過(guò)精簡(jiǎn),可以歸為以下幾個(gè)步驟
而以上的整個(gè)過(guò)程稱之為一幀,通俗點(diǎn)講就是在16.6ms之內(nèi)(主流瀏覽器)js的事件循環(huán)進(jìn)行完成之后會(huì)對(duì)頁(yè)面進(jìn)行渲染;那么React在何時(shí)對(duì)頁(yè)面進(jìn)行更新呢?react會(huì)在執(zhí)行完以上整個(gè)過(guò)程之后的空閑時(shí)間進(jìn)行更新,所以如果執(zhí)行以上流程用了10ms則react會(huì)在余下的6.6ms內(nèi)進(jìn)行更新(一般5ms左右);
在React16之前組件的mount階段會(huì)調(diào)用mountComponent,update階段會(huì)調(diào)用updateComponent,我們知道react的更新是從外向內(nèi)進(jìn)行更新,所以當(dāng)時(shí)的做法是使用遞歸逐步更新子組件,而這個(gè)過(guò)程是不可中斷的,所以當(dāng)子組件嵌套層級(jí)過(guò)深則會(huì)出現(xiàn)卡頓,因?yàn)檫@個(gè)過(guò)程是同步不可中斷的,所以react16之前采用的是同步更新策略,這顯然不符合React的快速響應(yīng)理念;
為了解決以上同步更新所帶來(lái)的痛點(diǎn),React16采用了異步可中斷更新來(lái)替代它,所以在React16當(dāng)中引入了Scheduler(調(diào)度器)
Scheduler如何進(jìn)行工作
Scheduler主要包含兩個(gè)作用
- 時(shí)間切片
- 優(yōu)先級(jí)調(diào)度
關(guān)于時(shí)間切片很好理解,我們已經(jīng)提到了Readt的更新會(huì)在重繪呈現(xiàn)之后的空閑時(shí)間執(zhí)行;所以在本質(zhì)上與requestIdleCallback 這個(gè)方法很相似;
requestIdleCallback(fn,timeout)
這個(gè)方法常用于處理一些優(yōu)先級(jí)比較低的任務(wù),任務(wù)會(huì)在瀏覽器空閑的時(shí)候執(zhí)行而它有兩個(gè)致命缺陷
- 不是所有瀏覽器適用(兼容性)
- 觸發(fā)不穩(wěn)定,在瀏覽器FPS為20左右的時(shí)候會(huì)比較流暢(違背React快速響應(yīng))
因此React放棄了requestIdleCallback 而實(shí)現(xiàn)了功能更加強(qiáng)大的requestIdleCallback polyfill 也就是 Scheduler
首先我們看下JS在瀏覽器中的執(zhí)行流程與requestIdleCallback的執(zhí)行時(shí)機(jī)
而Scheduler的時(shí)間切片將以回調(diào)函數(shù)的方式在異步宏任務(wù)當(dāng)中執(zhí)行;請(qǐng)看源碼
var schedulePerformWorkUntilDeadline; //node與舊版IE中執(zhí)行 if (typeof localSetImmediate === 'function') { // Node.js and old IE. // There's a few reasons for why we prefer setImmediate. // // Unlike MessageChannel, it doesn't prevent a Node.js process from exiting. // (Even though this is a DOM fork of the Scheduler, you could get here // with a mix of Node.js 15+, which has a MessageChannel, and jsdom.) // https://github.com/facebook/react/issues/20756 // // But also, it runs earlier which is the semantic we want. // If other browsers ever implement it, it's better to use it. // Although both of these would be inferior to native scheduling. schedulePerformWorkUntilDeadline = function () { localSetImmediate(performWorkUntilDeadline); }; } else if (typeof MessageChannel !== 'undefined') { //判斷瀏覽器能否執(zhí)行MessageChannel對(duì)象,同屬異步宏任務(wù),優(yōu)先級(jí)高于setTimeout // DOM and Worker environments. // We prefer MessageChannel because of the 4ms setTimeout clamping. var channel = new MessageChannel(); var port = channel.port2; channel.port1.onmessage = performWorkUntilDeadline; schedulePerformWorkUntilDeadline = function () { port.postMessage(null); }; } else { //如果當(dāng)前非舊IE與node環(huán)境并且不具備MessageChannel則使用setTimeout執(zhí)行回調(diào)函數(shù) // We should only fallback here in non-browser environments. schedulePerformWorkUntilDeadline = function () { localSetTimeout(performWorkUntilDeadline, 0); }; }
可以看到Scheduler在使用了三種異步宏任務(wù)方式,在舊版IE與node環(huán)境中使用setImmediate,在一般情況下使用MessageChannel如果當(dāng)前環(huán)境不支持MessageChannel則改用setTimeout
那么講完時(shí)間切片,我們來(lái)講講調(diào)度優(yōu)先級(jí);首先我們要知道對(duì)應(yīng)的五種優(yōu)先級(jí)
// Times out immediately var IMMEDIATE_PRIORITY_TIMEOUT = -1;//已經(jīng)過(guò)期 // Eventually times out var USER_BLOCKING_PRIORITY_TIMEOUT = 250;//將要過(guò)期 var NORMAL_PRIORITY_TIMEOUT = 5000;//一般優(yōu)先級(jí)任務(wù) var LOW_PRIORITY_TIMEOUT = 10000;//低優(yōu)先級(jí)任務(wù) // Never times out var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;//最低優(yōu)先級(jí)
可以看到過(guò)期時(shí)長(zhǎng)越低的任務(wù)優(yōu)先級(jí)越高,Scheduler是根據(jù)任務(wù)優(yōu)先級(jí)情況來(lái)調(diào)度的,它會(huì)優(yōu)先調(diào)度優(yōu)先級(jí)高的任務(wù),再調(diào)度優(yōu)先級(jí)低的任務(wù),如果在調(diào)度低優(yōu)先級(jí)任務(wù)時(shí)突然插入一個(gè)高優(yōu)先級(jí)任務(wù)則會(huì)中斷并保存該任務(wù)讓高優(yōu)先級(jí)任務(wù)插隊(duì),在之后有空閑時(shí)間片再?gòu)年?duì)列中取出執(zhí)行;我們來(lái)看主入口函數(shù)unstable_scheduleCallback
function unstable_scheduleCallback(priorityLevel, callback, options) { var currentTime = exports.unstable_now(); var startTime; //獲取任務(wù)延遲 if (typeof options === 'object' && options !== null) { var delay = options.delay; if (typeof delay === 'number' && delay > 0) { //延遲任務(wù) startTime = currentTime + delay; } else { startTime = currentTime; } } else { startTime = currentTime; } var timeout; //根據(jù)不同優(yōu)先級(jí)對(duì)應(yīng)時(shí)間給timeout賦值(過(guò)期時(shí)間) 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; } //計(jì)算任務(wù)延遲時(shí)間(執(zhí)行) var expirationTime = startTime + timeout; //新任務(wù)初始化 var newTask = { id: taskIdCounter++, callback: callback, priorityLevel: priorityLevel, startTime: startTime, expirationTime: expirationTime, sortIndex: -1 }; //如果startTime大于currentTime則說(shuō)明優(yōu)先級(jí)低,為延遲任務(wù) if (startTime > currentTime) { // This is a delayed task. //將startTime存入新任務(wù),用于任務(wù)排序(執(zhí)行順序) newTask.sortIndex = startTime; //采用小頂堆,將新任務(wù)插入延遲任務(wù)隊(duì)列進(jìn)行排序 //當(dāng)前startTime > currentTime所以當(dāng)前任務(wù)為延遲任務(wù)插入延遲任務(wù)隊(duì)列 push(timerQueue, newTask); //若可執(zhí)行任務(wù)隊(duì)列為空或者新任務(wù)為延遲任務(wù)的第一個(gè) if (peek(taskQueue) === null && newTask === peek(timerQueue)) { // All tasks are delayed, and this is the task with the earliest delay. if (isHostTimeoutScheduled) { // Cancel an existing timeout. //取消延時(shí)調(diào)度 cancelHostTimeout(); } else { isHostTimeoutScheduled = true; } // Schedule a timeout. requestHostTimeout(handleTimeout, startTime - currentTime); } } else { newTask.sortIndex = expirationTime; //推入可執(zhí)行隊(duì)列 push(taskQueue, newTask); // wait until the next time we yield. //當(dāng)前可調(diào)度無(wú)插隊(duì)任務(wù) if (!isHostCallbackScheduled && !isPerformingWork) { isHostCallbackScheduled = true; requestHostCallback(flushWork);//執(zhí)行 } } return newTask; }
從代碼中可以看到Scheduler中的任務(wù)以隊(duì)列的形式進(jìn)行保存分別是 可執(zhí)行隊(duì)列taskQueue與延遲隊(duì)列timerQueue 當(dāng)新任務(wù)進(jìn)入方法unstable_scheduleCallback會(huì)將任放到延遲隊(duì)列timerQueue中進(jìn)行排序(優(yōu)先級(jí)依照任務(wù)的sortIndex),如果延遲隊(duì)列timerQueue中有任務(wù)變成可執(zhí)行狀態(tài)(currentTmie>startTime)則我們會(huì)將任務(wù)放入我們會(huì)將任務(wù)取出并放入可執(zhí)行隊(duì)列taskQueue并取出最快到期的任務(wù)執(zhí)行
總結(jié)
React是以異步可中斷的更新來(lái)替代原有的同步更新,而實(shí)現(xiàn)異步可中斷更新的關(guān)鍵是Scheduler,Scheduler主要的功能是時(shí)間切片與優(yōu)先級(jí)調(diào)度,實(shí)現(xiàn)時(shí)間切片的關(guān)鍵是requestIdleCallback polyfill,調(diào)度任務(wù)為異步宏任務(wù)。而實(shí)現(xiàn)優(yōu)先級(jí)調(diào)度的關(guān)鍵是當(dāng)前任務(wù)到期時(shí)間,到期時(shí)間短的優(yōu)先級(jí)更高,根據(jù)任務(wù)的優(yōu)先級(jí)分別保存在可執(zhí)行隊(duì)列與延時(shí)隊(duì)列;
以上就是React為什么需要Scheduler調(diào)度器原理詳解的詳細(xì)內(nèi)容,更多關(guān)于React Scheduler調(diào)度器原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解在React項(xiàng)目中如何集成和使用web worker
在復(fù)雜的React應(yīng)用中,某些計(jì)算密集型或耗時(shí)操作可能會(huì)阻塞主線程,導(dǎo)致用戶界面出現(xiàn)卡頓或響應(yīng)慢的現(xiàn)象,為了優(yōu)化用戶體驗(yàn),可以采用Web Worker來(lái)在后臺(tái)線程中執(zhí)行這些操作,本文將詳細(xì)介紹在React項(xiàng)目中如何集成和使用Web Worker來(lái)改善應(yīng)用性能,需要的朋友可以參考下2023-12-12阿里低代碼框架lowcode-engine自定義設(shè)置器詳解
這篇文章主要為大家介紹了阿里低代碼框架lowcode-engine自定義設(shè)置器示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02React immer與Redux Toolkit使用教程詳解
這篇文章主要介紹了React中immer與Redux Toolkit的使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2022-10-10JavaScript React如何修改默認(rèn)端口號(hào)方法詳解
這篇文章主要介紹了JavaScript React如何修改默認(rèn)端口號(hào)方法詳解,文中通過(guò)步驟圖片解析介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07使用react-native-doc-viewer實(shí)現(xiàn)文檔預(yù)覽
這篇文章主要介紹了使用react-native-doc-viewer實(shí)現(xiàn)文檔預(yù)覽,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09關(guān)于React狀態(tài)管理的三個(gè)規(guī)則總結(jié)
隨著 JavaScript 單頁(yè)應(yīng)用開發(fā)日趨復(fù)雜,JavaScript 需要管理比任何時(shí)候都要多的 state (狀態(tài)),這篇文章主要給大家介紹了關(guān)于React狀態(tài)管理的三個(gè)規(guī)則,需要的朋友可以參考下2021-07-07React類組件和函數(shù)組件對(duì)比-Hooks的簡(jiǎn)介
Hook?是?React?16.8?的新增特性,它可以讓我們?cè)诓痪帉慶lass的情況下,?使用state以及其他的React特性(比如生命周期,這篇文章主要介紹了React類組件和函數(shù)組件對(duì)比-Hooks的介紹及初體驗(yàn),需要的朋友可以參考下2022-11-11基于React實(shí)現(xiàn)一個(gè)todo打勾效果
這篇文章主要為大家詳細(xì)介紹了如何基于React實(shí)現(xiàn)一個(gè)todo打勾效果,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-03-03