網站首頁 編程語言 正文
正文
最近在重學React,由于近兩年沒使用React突然重學發現一些很有意思的概念,首先便是React的Scheduler(調度器) 由于我對React的概念還停留在React 15之前(就是那個沒有hooks的年代),所以接觸Scheduler(調度器) 讓我感覺很有意思;
在我印象中React的架構分為兩層(React 16 之前)
- Reconciler(協調器)—— 負責找出變化的組件
- Renderer(渲染器)—— 負責將變化的組件渲染到頁面上
如今增加了Scheduler(調度器) ,那么調度器有什么用?調度器的作用是調度任務的優先級,高優任務優先進入Reconciler
我們為什么需要Scheduler(調度器)
要了解為什么需要Scheduler(調度器) 我們需要知道以下幾個痛點;
- React在何時進行更新;
- 16之前的React怎樣進行更新;
- 16之前的React帶來的痛點;
首先我們講講React何時進行更新,眾所周知主流的瀏覽器的刷新頻率是60HZ,也就是說主流的瀏覽器完成一次刷新需要1000/60 ms約等于16.666ms
然后我們需要知道瀏覽器在你開啟一個頁面的時候做了什么;總結下來就是一張圖
CSSOM樹的構建時機與JS的執行時機是依據你解析的link標簽與script標簽來確認的;因為當React開始更新時已完成部分工作(開始回流與重繪),所以經過精簡,可以歸為以下幾個步驟
而以上的整個過程稱之為一幀,通俗點講就是在16.6ms之內(主流瀏覽器)js的事件循環進行完成之后會對頁面進行渲染;那么React在何時對頁面進行更新呢?react會在執行完以上整個過程之后的空閑時間進行更新,所以如果執行以上流程用了10ms則react會在余下的6.6ms內進行更新(一般5ms左右);
在React16之前組件的mount階段會調用mountComponent,update階段會調用updateComponent,我們知道react的更新是從外向內進行更新,所以當時的做法是使用遞歸逐步更新子組件,而這個過程是不可中斷的,所以當子組件嵌套層級過深則會出現卡頓,因為這個過程是同步不可中斷的,所以react16之前采用的是同步更新策略,這顯然不符合React的快速響應理念;
為了解決以上同步更新所帶來的痛點,React16采用了異步可中斷更新來替代它,所以在React16當中引入了Scheduler(調度器)
Scheduler如何進行工作
Scheduler主要包含兩個作用
- 時間切片
- 優先級調度
關于時間切片很好理解,我們已經提到了Readt的更新會在重繪呈現之后的空閑時間執行;所以在本質上與requestIdleCallback 這個方法很相似;
requestIdleCallback(fn,timeout)
這個方法常用于處理一些優先級比較低的任務,任務會在瀏覽器空閑的時候執行而它有兩個致命缺陷
- 不是所有瀏覽器適用(兼容性)
- 觸發不穩定,在瀏覽器FPS為20左右的時候會比較流暢(違背React快速響應)
因此React放棄了requestIdleCallback 而實現了功能更加強大的requestIdleCallback polyfill 也就是 Scheduler
首先我們看下JS在瀏覽器中的執行流程與requestIdleCallback的執行時機
而Scheduler的時間切片將以回調函數的方式在異步宏任務當中執行;請看源碼
var schedulePerformWorkUntilDeadline;
//node與舊版IE中執行
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') {
//判斷瀏覽器能否執行MessageChannel對象,同屬異步宏任務,優先級高于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 {
//如果當前非舊IE與node環境并且不具備MessageChannel則使用setTimeout執行回調函數
// We should only fallback here in non-browser environments.
schedulePerformWorkUntilDeadline = function () {
localSetTimeout(performWorkUntilDeadline, 0);
};
}
可以看到Scheduler在使用了三種異步宏任務方式,在舊版IE與node環境中使用setImmediate,在一般情況下使用MessageChannel如果當前環境不支持MessageChannel則改用setTimeout
那么講完時間切片,我們來講講調度優先級;首先我們要知道對應的五種優先級
// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;//已經過期
// Eventually times out
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;//將要過期
var NORMAL_PRIORITY_TIMEOUT = 5000;//一般優先級任務
var LOW_PRIORITY_TIMEOUT = 10000;//低優先級任務
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;//最低優先級
可以看到過期時長越低的任務優先級越高,Scheduler是根據任務優先級情況來調度的,它會優先調度優先級高的任務,再調度優先級低的任務,如果在調度低優先級任務時突然插入一個高優先級任務則會中斷并保存該任務讓高優先級任務插隊,在之后有空閑時間片再從隊列中取出執行;我們來看主入口函數unstable_scheduleCallback
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = exports.unstable_now();
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;
}
var timeout;
//根據不同優先級對應時間給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;
}
//計算任務延遲時間(執行)
var expirationTime = startTime + timeout;
//新任務初始化
var newTask = {
id: taskIdCounter++,
callback: callback,
priorityLevel: priorityLevel,
startTime: startTime,
expirationTime: expirationTime,
sortIndex: -1
};
//如果startTime大于currentTime則說明優先級低,為延遲任務
if (startTime > currentTime) {
// This is a delayed task.
//將startTime存入新任務,用于任務排序(執行順序)
newTask.sortIndex = startTime;
//采用小頂堆,將新任務插入延遲任務隊列進行排序
//當前startTime > currentTime所以當前任務為延遲任務插入延遲任務隊列
push(timerQueue, newTask);
//若可執行任務隊列為空或者新任務為延遲任務的第一個
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.
//取消延時調度
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
} // Schedule a timeout.
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
//推入可執行隊列
push(taskQueue, newTask);
// wait until the next time we yield.
//當前可調度無插隊任務
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);//執行
}
}
return newTask;
}
從代碼中可以看到Scheduler中的任務以隊列的形式進行保存分別是 可執行隊列taskQueue與延遲隊列timerQueue 當新任務進入方法unstable_scheduleCallback會將任放到延遲隊列timerQueue中進行排序(優先級依照任務的sortIndex),如果延遲隊列timerQueue中有任務變成可執行狀態(currentTmie>startTime)則我們會將任務放入我們會將任務取出并放入可執行隊列taskQueue并取出最快到期的任務執行
總結
React是以異步可中斷的更新來替代原有的同步更新,而實現異步可中斷更新的關鍵是Scheduler,Scheduler主要的功能是時間切片與優先級調度,實現時間切片的關鍵是requestIdleCallback polyfill,調度任務為異步宏任務。而實現優先級調度的關鍵是當前任務到期時間,到期時間短的優先級更高,根據任務的優先級分別保存在可執行隊列與延時隊列;
原文鏈接:https://juejin.cn/post/7143148651936743438
相關推薦
- 2022-11-08 PostgreSQL查看帶有綁定變量SQL的通用方法詳解_PostgreSQL
- 2022-01-12 解決element-ui 日期選擇器提交后臺數據不準確問題
- 2021-12-07 詳解C語言編程之thread多線程_C 語言
- 2022-04-10 Windows Terminal添加到右鍵菜單中
- 2023-05-21 詳解在Anaconda環境下Python安裝pydot與graphviz的方法_python
- 2022-07-25 Oracle中的序列SEQUENCE詳解_oracle
- 2022-11-02 Python嵌套函數與nonlocal使用詳細介紹_python
- 2022-06-06 MybatisPlus二級緩存體系探究分析_相關技巧
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支