閱讀本文你將收穫:html
全面熟悉requestidlecallback用法和存在的價值。前端
明確requestidlecallback的使用場景。react
瞭解react requestidlecallback polyfill的實現。git
當前大多數的屏幕刷新率都是60hz,也就是每秒屏幕刷新60次,低於60hz人眼就會感知卡頓掉幀等狀況,一樣咱們前端瀏覽器所說的FPS(frame per second)
是瀏覽器每秒刷新的次數,理論上FPS
越高人眼以爲界面越流暢,在兩次屏幕硬件刷新之間,瀏覽器正好進行一次刷新(重繪),網頁也會很流暢,固然這種是理想模式, 若是兩次硬件刷新之間瀏覽器重繪屢次是沒意義的,只會消耗資源,若是瀏覽器重繪一次的時間是硬件屢次刷新的時間,那麼人眼將感知卡頓掉幀等, 因此瀏覽器對一次重繪的渲染工做須要在16ms(1000ms/60)以內完成,也就是說每一次重繪小於16ms纔不會卡頓掉幀。github
一次重繪瀏覽器須要作哪些事情?web
瀏覽器的一幀說的就是一次完整的重繪。瀏覽器
requestIdleCallback
如下demo源碼地址app
**window.requestIdleCallback()
**方法將在瀏覽器的空閒時段內調用的函數排隊。dom
API異步
var handle = window.requestIdleCallback(callback[, options])
callback: 一個在事件循環空閒時即將被調用的函數的引用。函數會接收到一個名爲 IdleDeadline 的參數,這個參數能夠獲取當前空閒時間以及回調是否在超時時間前已經執行的狀態。
其中 IdleDeadline 對象包含:
didTimeout,布爾值,表示任務是否超時,結合 timeRemaining 使用。
timeRemaining(),表示當前幀剩餘的時間,也可理解爲留給任務的時間還有多少。
options的參數
timeout: 表示超過這個時間後,若是任務還沒執行,則強制執行,沒必要等待空閒。還沒有經過超時毫秒數調用回調,那麼回調會在下一次空閒時期被強制執行。若是明確在某段時間內執行回調,能夠設置timeout值。在瀏覽器繁忙的時候,requestIdleCallback超時執行就和setTimeout效果同樣。
複製代碼
返回值:和setTimeout
、setInterval
返回值同樣,是一個標識符。能夠經過 cancelIdleCallback(handle)
清除取消。
空閒時段
何時瀏覽器出現空閒時段?
場景一
當瀏覽器一幀渲染所用時間小於屏幕刷新率(對於具備60Hz 的設備,一幀間隔應該小於16ms)時間,到下一幀渲染渲染開始時出現的空閒時間,如圖idle period
,
場景二
當瀏覽器沒有可渲染的任務,主線程一直處於空閒狀態,事件隊列爲空。爲了不在不可預測的任務(例如用戶輸入的處理)中引發用戶可察覺的延遲,這些空閒週期的長度應限制爲最大值50ms,也就是timeRemaining
最大不超過50(也就是20fps,這也是react polyfill的緣由之一),當空閒時段結束時,能夠調度另外一個空閒時段,若是它保持空閒,那麼空閒時段將更長,後臺任務能夠在更長時間段內發生。如圖:
注意:
timeRemaining
最大爲50毫秒,是根據研究[ RESPONSETIME ] 得出的,該研究代表,對用戶輸入的100毫秒之內的響應一般被認爲對人類是瞬時的,就是人類不會有察覺。將閒置截止期限設置爲50ms意味着即便在閒置任務開始後當即發生用戶輸入,用戶代理仍然有剩餘的50ms能夠在其中響應用戶輸入而不會產生用戶可察覺的滯後。
requestIdleCallback
用法先模擬一個可預測執行時間的佔用主線程的方法:
function sleep(date) {
let flag = true;
const now = Date.now();
while (flag) {
if (Date.now() - now > date) {
flag = false;
}
}
}
複製代碼
用requestIdleCallback
執行主線程空閒開始調用的方法:
function work() {
sleep(2000); // 模擬主線程任務執行時間
requestIdleCallback(() => {
console.log("空閒時間1");
sleep(1000);
console.log("空閒時間1回調任務執行完成");
});
requestIdleCallback(() => {
console.log("空閒時間2");
});
}
btn1.addEventListener("click", work);
複製代碼
執行結果:點擊button -> 等待2s -> 打印 空閒時間1 -> 等待 1s -> 打印 空閒時間1回調任務執行完成 -> 空閒時間2;當sleep
結束requestIdleCallback
獲取到主線程空閒,立馬執行cb(也是在主線程執行)繼續佔用主線程,直到sleep結束,第二個requestIdleCallback
獲取主線程空閒輸出空閒時間2。細看一下,此處requestIdleCallback
不就是setTimeout
嗎,這樣的功能用setTimeout
也能實現,固然他們是有區別的,的咱們sleep
模擬佔用主線程時間是可控的,但大多時候主線程work時間是不可預知的,setTimeout須要知道具體延遲時間,因此這是主要的卻別。
function renderElement(txt) {
const p = document.createElement("p");
p.innerText = txt;
return p;
}
let taskLen = 10;
let update = 0;
function work2() {
document.body.appendChild(renderElement(`任務還剩 ${taskLen}`));
console.log(`頁面更新${++update}次`);
taskLen--;
if (taskLen) {
requestAnimationFrame(work2);
}
}
btn1.addEventListener("click", () => {
requestAnimationFrame(work2);
window.requestIdleCallback(() => {
console.log("空閒了, requestIdleCallback生效了");
});
});
複製代碼
結果如圖:
通過performance
錄製分析如圖:
放大第一幀看:
requestIdleCallback
在第一幀事後就執行,緣由第一幀事後就出現了空閒時段。那麼若是每一幀沒有空閒時間,requestIdleCallback
會何時執行哪?
修改代碼:
...
function work2() {
document.body.appendChild(renderElement(`任務還剩 ${taskLen}`));
console.log(`頁面更新${++update}次`);
sleep(1000);
taskLen--;
if (taskLen) {
requestAnimationFrame(work2);
}
}
...
複製代碼
結果:會等到全部的渲染任務執行完畢纔會有空閒時間,因此requestIdleCallback
的cb
在最後執行。
若是不想讓空閒任務等待那麼久,那麼requestIdleCallback
的第二個參數就派上用場了, {timeout: 1000}
,更改demo
,以下:
...
btn1.addEventListener("click", () => {
requestAnimationFrame(work2);
window.requestIdleCallback(
() => {
console.log("空閒了, requestIdleCallback生效了");
},
{ timeout: 1200 } // 最遲能等待1.2s
);
});
...
複製代碼
運行的結果,console輸出順序:... -> 頁面更新3次 -> 空閒了, requestIdleCallback生效了-> ...
當用戶input
輸入時,可用requestIdleCallback
來避免不可見的行爲形成用戶行爲形成卡頓,譬如發送數據分析、處理界面不可見的業務邏輯等。
下面以發送數據分析
爲例:
// 記錄須要發送的數據隊列
const eventStack = [];
// requestIdleCallback是否已經調度
let isRequestIdleCallbackScheduled = false;
// 模擬發送數據
const sendData = (...arg) => {
console.log("發送數據", arg);
};
function onDivThemeRed() {
// 業務邏輯
render.classList.remove("border-blue");
render.classList.add("border-red");
eventStack.push({
category: "button",
action: "click",
label: "theme",
value: "red",
});
schedulePendingEvents();
}
function onDivThemeBlue() {
// 業務邏輯
render.classList.remove("border-red");
render.classList.add("border-blue");
eventStack.push({
category: "button",
action: "click",
label: "theme",
value: "blue",
});
schedulePendingEvents();
}
function schedulePendingEvents() {
if (isRequestIdleCallbackScheduled) return;
isRequestIdleCallbackScheduled = true;
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
}
function processPendingAnalyticsEvents(deadline) {
isRequestIdleCallbackScheduled = false;
while (deadline.timeRemaining() > 0 && eventStack.length > 0) {
const evt = eventStack.pop();
sendData(
"send",
"event",
evt.category,
evt.action,
evt.label,
evt.value
);
}
if (eventStack.length > 0) schedulePendingEvents();
}
btn2.addEventListener("click", onDivThemeRed);
btn3.addEventListener("click", onDivThemeBlue);
複製代碼
requestIdleCallback
會在每一幀結束後執行,去判斷瀏覽器是否空閒,若是瀏覽器一直處於佔用狀態,則沒有空閒時間,且若是requestIdleCallback
沒有設置timeout
時間,那麼callback
的任務會一直推遲執行,若是在當前幀設置timeout
,瀏覽器會在當前幀結束的下一幀開始判斷是否超時執行callback
。requestIdleCallback
任務沒有和瀏覽器的幀渲染對其,應用不當會形成掉幀卡頓,原則上requestIdleCallback
的FPS只有20,因此有高FPS要求的、須要和渲染幀對齊執行任務,如DOM動畫等,建議用requestAnimationFrame
,纔會達到最佳流暢效果。
下面介紹一下react
中有關requestIdleCallback
的介紹。
react
中 requestIdleCallback pollyfill
的實現前面提到requestIdleCallback
工做只有20FPS,通常對用戶來感受來講,須要到60FPS纔是流暢的, 即一幀時間爲 16.7 ms,因此這也是react
團隊本身實現requestIdleCallback
的緣由。實現大體思路是在requestAnimationFrame
獲取一楨的開始時間,觸發一個postMessage
,在空閒的時候調用idleTick
來完成異步任務。
源碼在
packages/scheduler/src/forks/SchedulerHostConfig.default.js
下,分別對非DOM和DOM環境有不一樣的實現。
export let requestHostCallback; // 相似requestIdleCallback
export let cancelHostCallback; // 相似cancelIdleCallback
export let requestHostTimeout; // 非dom環境的實現
export let cancelHostTimeout; // 取消requestHostTimeout
export let shouldYieldToHost; // 判斷任務是否超時,須要被打斷
export let requestPaint; //
export let getCurrentTime; // 獲取當前時間
export let forceFrameRate; // 根據fps計算幀時間
// 非dom環境
if (typeof window === 'undefined' || typeof MessageChannel !== 'function') {
let _callback = null; // 正在執行的回調
let _timeoutID = null;
const _flushCallback = function() {
// 若是回調存在則執行,
if (_callback !== null) {
try {
const currentTime = getCurrentTime();
const hasRemainingTime = true;
// hasRemainingTime 相似deadline.didTimeout
_callback(hasRemainingTime, currentTime);
_callback = null;
} catch (e) {
setTimeout(_flushCallback, 0);
throw e;
}
}
};
// ...
requestHostCallback = function(cb) {
// 若_callback存在,表示當下有任務再繼續,
if (_callback !== null) {
// setTimeout的第三個參數能夠延後執行任務。
setTimeout(requestHostCallback, 0, cb);
} else {
// 不然直接執行。
_callback = cb;
setTimeout(_flushCallback, 0);
}
};
cancelHostCallback = function() {
_callback = null;
};
requestHostTimeout = function(cb, ms) {
_timeoutID = setTimeout(cb, ms);
};
cancelHostTimeout = function() {
clearTimeout(_timeoutID);
};
shouldYieldToHost = function() {
return false;
};
requestPaint = forceFrameRate = function() {};
} else {
// 一大堆的瀏覽器方法的判斷,有performance, requestAnimationFrame, cancelAnimationFrame
// ...
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// yieldInterval每幀的時間,deadline爲最終期限時間
deadline = currentTime + yieldInterval;
const hasTimeRemaining = true;
try {
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
if (!hasMoreWork) {
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// 若是有更多的工做,就把下一個消息事件安排在前一個消息事件的最後
port.postMessage(null);
}
} catch (error) {
// 若是調度任務拋出,則退出當前瀏覽器任務,以便觀察錯誤。
port.postMessage(null);
throw error;
}
} else {
isMessageLoopRunning = false;
}
needsPaint = false;
};
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
requestHostCallback = function(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
};
}
複製代碼
由上可見,非DOM模式下requestHostCallback
是setTimeout
模擬實現的,而在DOM下是基於MessageChannel
消息的發佈訂閱模式postMessage
和onmessage
實現的。
requestIdleCallback
須要注意的:
requestIdleCallback
是屏幕渲染以後執行的。
一些低優先級的任務可以使用 requestIdleCallback
等瀏覽器不忙的時候來執行,同時由於時間有限,它所執行的任務應該儘可能是可以量化,細分的微任務(micro task)比較適合requestIdleCallback
。
requestIdleCallback
不會和幀對齊,因此涉及到DOM的操做和動畫最好放在requestAnimationFrame
中執行,requestAnimationFrame
在從新渲染屏幕以前執行。
Promise 也不建議在這裏面進行,由於 Promise 的回調屬性 Event loop 中優先級較高的一種微任務,會在 requestIdleCallback
結束時當即執行,無論此時是否還有富餘的時間,這樣有很大可能會讓一幀超過 16 ms。
歡迎各位大佬批評指正,,,
🐶🐶🐶🐶🐶🐶
參考連接:
developers.google.com/web/updates…