自從react 16
出來之後,react fiber
相關的文章層出不窮,但大多都是講解fiber
的數據結構,以及組件樹的diff
是如何由遞歸改成循環遍歷的。對於time slicing
的描述通常都說利用了requestIdleCallback
這個api來作調度,但對於任務如何調度卻很難找到詳細的描述。node
所以,本篇文章就是來幹這個事情的,從源碼角度來一步步闡述React Scheduler
是怎麼實現任務調度的。react
雖說標題是React Scheduler
,但本文的內容跟react
是不相關的,由於任務調度器其實跟react
是沒有關係的,它只是描述怎麼在合適的時機去執行一些任務,也就是說你即便沒有react
基礎也能夠進行本文的閱讀,若是你是框架做者,也能夠借鑑這個scheduler
的實現,在本身的框架裏來進行任務調度。git
react v16.7.0
版本的源碼,請注意時效性。Scheduler.js
接下來先來了解一下閱讀本文須要知道的一些基礎知識。github
window.performance.now
這個是瀏覽器內置的時鐘,從頁面加載開始計時,返回到當前的總時間,單位ms
。意味着你在打開頁面第10分鐘在控制檯調用這個方法,返回的數字大概是 600000(誤)。api
window.requestAnimationFrame
這個方法應該很常見了,它讓咱們能夠在下一幀開始時調用指定的函數。它的執行是是跟隨系統的刷新頻率的。requestAnimationFrame
方法接收一個參數,即要執行的回調函數。這個回調函數會默認地傳入一個參數,即從打開頁面到回調函數被觸發時的時間長度,單位爲毫秒。瀏覽器
能夠理解爲系統在調用回調前立馬執行了一下performance.now()
傳給了回調當參數。這樣咱們就能夠在執行回調的時候知道當前的執行時間了。bash
requestAnimationFrame(function F(t) {
console.log(t, '===='); //會不斷打印執行回調的時間,若是刷新頻率爲60Hz,則相鄰的t間隔時間大約爲1000/60 = 16.7ms
requestAnimationFrame(F)
})
複製代碼
requestAnimationFrame
有個特色,就是當頁面處理未激活的狀態下,requestAnimationFrame
會中止執行;當頁面後面再轉爲激活時,requestAnimationFrame
又會接着上次的地方繼續執行。數據結構
window.MessageChannel
這個接口容許咱們建立一個新的消息通道,並經過它的兩個MessagePort(port1,port2)
屬性發送數據。 示例代碼以下框架
var channel = new MessageChannel();
var port1 = channel.port1;
var port2 = channel.port2;
port1.onmessage = function(event){
console.log(event.data) // someData
}
port2.postMessage('someData')
複製代碼
這裏有一點須要注意,onmessage
的回調函數的調用時機是在一幀的paint完成以後。據觀察vue
的nextTick
也是用MessageChannel
來作fallback
的(優先用setImmediate
)。
react scheduler
內部正是利用了這一點來在一幀渲染結束後的剩餘時間來執行任務的
先默認你們對鏈表有個基本的認識。沒有的話本身去補一下知識。
這裏要介紹的是雙向循環鏈表
previous
和next
兩個屬性來分別指向先後兩個節點。previous
指向最後一個節點,造成一個環形的人體蜈蚣
。//person的類型定義
interface Person {
name : string //姓名
age : number //年齡,依賴這個屬性排序
next : Person //緊跟在後面的人,默認是null
previous : Person //前面相鄰的那我的,默認是null
}
var firstNode = null; //一開始鏈表裏沒有節點
//插入的邏輯
function insertByAge(newPerson:Person){
if(firstNode = null){
//若是 firstNode爲空,說明newPerson是第一我的,
//把它賦值給firstNode,並把next和previous屬性指向自身,自成一個環。
firstNode = newPerson.next = newPerson.previous = newPerson;
} else { //隊伍裏有人了,新來的人要找準本身的位置
var next = null; //記錄newPerson插入到哪一個人前邊
var person = firstNode; // person 在下邊的循環中會從第一我的開始日後找
do {
if (person.age > newPerson.age) {
//若是person的年齡比新來的人大,說明新來的人找到位置了,他剛好要排在person的前邊,結束
next = person;
break;
}
//繼續找後面的人
node = node.next;
} while (node !== firstNode); //這裏的while是爲了防止無限循環,畢竟是環形的結構
if(next === null){ //找了一圈發現 沒有person的age比newPerson大,說明newPerson應該放到隊伍的最後,也就是說newPerson的後面應該是firstNode。
next = firstNode;
}else if(next === firstNode){ //找第一個的時候就找到next了,說明newPerson要放到firstNode前面,這時候firstNode就要更新爲newPerson
firstNode = newPerson
}
//下面是newPerson的插入操做,給next及previous兩我的的先後連接都關聯到newPerson
var previous = next.previous;
previous.next = next.previous = newPerson;
newPerson.next = next;
newPerson.previous = previous;
}
//插入成功
}
//刪除第一個節點
function deleteFirstPerson(){
if(firstNode === null) return; //隊伍裏沒有人,返回
var next = firstNode.next; //第二我的
if(firstNode === next) {
//這時候只有一我的
firstNode = null;
next = null;
} else {
var lastPerson = firstNode.previous; //找到最後一我的
firstNode = lastPerson.next = next; //更新新的第一人
next.previout = lastPerson; //並在新的第一人和最後一人之間創建鏈接
}
}
複製代碼
因爲react16
內大量利用了鏈表來記錄數據,尤爲react scheduler
內對任務的操做使用了雙向循環鏈表結構。因此理解了上述的代碼,對於理解react
對任務的調度就會比較容易了。
注:爲了梳理總體的運行流程,下面的示例代碼有可能會在源碼基礎上有少許刪減
```
getCurrentTime = function() {
return performance.now();
//若是不支持performance,利用 Date.now()作fallback
}
```
複製代碼
react內對任務定義的優先級分爲5種,數字越小優先級越高
var ImmediatePriority = 1; //最高優先級
var UserBlockingPriority = 2; //用戶阻塞型優先級
var NormalPriority = 3; //普通優先級
var LowPriority = 4; // 低優先級
var IdlePriority = 5; // 空閒優先級
複製代碼
這5種優先級依次對應5個過時時間
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
var maxSigned31BitInt = 1073741823;
// 立馬過時 ==> ImmediatePriority
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// 250ms之後過時
var USER_BLOCKING_PRIORITY = 250;
//
var NORMAL_PRIORITY_TIMEOUT = 5000;
//
var LOW_PRIORITY_TIMEOUT = 10000;
// 永不過時
var IDLE_PRIORITY = maxSigned31BitInt;
複製代碼
每一個任務在添加到鏈表裏的時候,都會經過 performance.now() + timeout
來得出這個任務的過時時間,隨着時間的推移,當前時間會愈來愈接近這個過時時間,因此過時時間越小的表明優先級越高。若是過時時間已經比當前時間小了,說明這個任務已通過期了還沒執行,須要立馬去執行(asap
)。
上面的maxSigned31BitInt
,經過註釋能夠知道這是32
位系統V8
引擎裏最大的整數。react
用它來作IdlePriority
的過時時間。
據粗略計算這個時間大概是12.427
天。也就是說極端狀況下你的網頁tab
若是能一直開着到12天半,任務纔有可能過時。
function scheduleCallback()
unstable_scheduleCallback
,意思是當前仍是不穩定的,這裏就以scheduleCallback
做名字。下面上代碼
function scheduleCallback(callback, options? : {timeout:number} ) {
//to be coutinued
}
複製代碼
這個方法有兩個入參,第一個是要執行的callback
,暫時能夠理解爲一個任務。第二個參數是可選的,能夠傳入一個超時時間來標識這個任務過多久超時。若是不傳的話就會根據上述的任務優先級肯定過時時間。
//這是一個全局變量,表明當前任務的優先級,默認爲普通
var currentPriorityLevel = NormalPriority
function scheduleCallback(callback, options? : {timeout:number} ) {
var startTime = getCurrentTime()
if (
typeof options === 'object' &&
options !== null &&
typeof options.timeout === 'number'
){
//若是傳了options, 就用入參的過時時間
expirationTime = startTime + options.timeout;
} else {
//判斷當前的優先級
switch (currentPriorityLevel) {
case ImmediatePriority:
expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
expirationTime = startTime + USER_BLOCKING_PRIORITY;
break;
case IdlePriority:
expirationTime = startTime + IDLE_PRIORITY;
break;
case LowPriority:
expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
}
}
//上面肯定了當前任務的截止時間,下面建立一個任務節點,
var newNode = {
callback, //任務的具體內容
priorityLevel: currentPriorityLevel, //任務優先級
expirationTime, //任務的過時時間
next: null, //下一個節點
previous: null, //上一個節點
};
//to be coutinued
}
複製代碼
上面的代碼根據入參或者當前的優先級來肯定當前callback
的過時時間,並生成一個真正的任務節點。接下來就要把這個節點按照expirationTime
排序插入到任務的鏈表裏邊去。
// 表明任務鏈表的第一個節點
var firstCallbackNode = null;
function scheduleCallback(callback, options? : {timeout:number} ) {
...
var newNode = {
callback, //任務的具體內容
priorityLevel: currentPriorityLevel, //任務優先級
expirationTime, //任務的過時時間
next: null, //下一個節點
previous: null, //上一個節點
};
// 下面是按照 expirationTime 把 newNode 加入到任務隊列裏。參考基礎知識裏的person排隊的例子
if (firstCallbackNode === null) {
firstCallbackNode = newNode.next = newNode.previous = newNode;
ensureHostCallbackIsScheduled(); //這個方法先忽略,後面講
} else {
var next = null;
var node = firstCallbackNode;
do {
if (node.expirationTime > expirationTime) {
next = node;
break;
}
node = node.next;
} while (node !== firstCallbackNode);
if (next === null) {
next = firstCallbackNode;
} else if (next === firstCallbackNode) {
firstCallbackNode = newNode;
ensureHostCallbackIsScheduled(); //這個方法先忽略,後面講
}
var previous = next.previous;
previous.next = next.previous = newNode;
newNode.next = next;
newNode.previous = previous;
}
return newNode;
}
複製代碼
ensureHostCallbackIsScheduled
就是前面講的雙向循環鏈表的插入邏輯。ensureHostCallbackIsScheduled
方法執行的兩個分支。因此咱們如今應該知道,ensureHostCallbackIsScheduled
是用來在合適的時機去啓動任務執行的。接下來就須要實現這麼一個功能,如何在合適的時機去執行一個function。
requestIdleCallback pollyfill
如今請暫時忘掉上面那段任務隊列相關的事情,來思考如何在瀏覽器每一幀繪製完的空閒時間來作一些事情。
答案能夠是requestIdleCallback
,但因爲某些緣由,react團隊放棄了這個api,轉而利用requestAnimationFrame
和MessageChannel
pollyfill
了一個requestIdleCallback
function requestAnimationFrameWithTimeout()
首先介紹一個超強的函數,代碼以下
var requestAnimationFrameWithTimeout = function(callback) {
rAFID = requestAnimationFrame(function(timestamp) {
clearTimeout(rAFTimeoutID);
callback(timestamp);
});
rAFTimeoutID = setTimeout(function() {
cancelAnimationFrame(rAFID);
callback(getCurrentTime());
}, 100);
}
複製代碼
這段代碼什麼意思呢?
當咱們調用requestAnimationFrameWithTimeout
並傳入一個callback
的時候,會啓動一個requestAnimationFrame
和一個setTimeout
,二者都會去執行callback
。但因爲requestAnimationFrame
執行優先級相對較高,它內部會調用clearTimeout
取消下面定時器的操做。因此在頁面active
狀況下的表現跟requestAnimationFrame
是一致的。
到這裏你們應該明白了,一開始的基礎知識裏說了,requestAnimationFrame
在頁面切換到未激活的時候是不工做的,這時requestAnimationFrameWithTimeout
就至關於啓動了一個100ms
的定時器,接管任務的執行工做。這個執行頻率不高也不低,既能不影響cpu能耗,又能保證任務能有必定效率的執行。
下面咱們暫時先認爲requestAnimationFrameWithTimeout
等價於 requestAnimationFrame
(不知不覺篇幅已經這麼長了,今天先寫到這裏吧,下次有機會再更)