還在用 setTimeout?試試 requestIdleCallback 吧!
大家好,我是 Sunday。
在開發(fā)中,setTimeout 咱們幾乎天天都在用。
無論是頁面初始化后延遲執(zhí)行邏輯、動畫間隔,還是接口請求防抖、埋點上報,咱們幾乎都離不開它。
但是 setTimeout 有時候并不好用,比如說:
- setTimeout 的執(zhí)行時間并不準(zhǔn)確,延遲時間只是任務(wù)準(zhǔn)備已出 EventLoop 的時間
- setTimeout 并不會判斷瀏覽器任務(wù)是否空閑,從而當(dāng)任務(wù)執(zhí)行時可能會出現(xiàn)卡頓的情況
瀏覽器是單線程的,所有任務(wù)都要經(jīng)過事件循環(huán)(Event Loop)來調(diào)度。當(dāng)你調(diào)用
setTimeout(fn, 0)時,這個任務(wù)會被放進(jìn) “宏任務(wù)隊列” 里,只有當(dāng)主線程空出來,才會去執(zhí)行。
因此,如果我們想要在 瀏覽器空閑時間 去執(zhí)行一些大任務(wù)操作(比如:埋點上報),那么 setTimeout 并不方便。
那么,有沒有一個更加聰明的 API,可以知道瀏覽器什么時候會空閑,從而可以 自動調(diào)用 任務(wù)呢?
它就是 requestIdleCallback。
requestIdleCallback
圖片
requestIdleCallback 的核心是 瀏覽器級空閑調(diào)度 API,它能讓你把一些非關(guān)鍵任務(wù)放到瀏覽器“閑”的時候去執(zhí)行,從而讓關(guān)鍵任務(wù)(如渲染、動畫、交互)始終保持流暢。PS: 也就是說,瀏覽器在處理完一幀的渲染、動畫、事件之后,如果還有空余時間,就會來執(zhí)行你的任務(wù)。
這個函數(shù)接收兩個參數(shù) callback, options,并且會返回一個 ID 作為結(jié)束回調(diào)參數(shù)(通過 Window.cancelIdleCallback() 結(jié)束回調(diào))。
圖片
基礎(chǔ)應(yīng)用
比如說:咱們要做一個埋點上報的系統(tǒng),希望在用戶瀏覽頁面后,上報一些埋點日志。
sendAnalyticsData() // 立即上報埋點如果代碼這么寫,則當(dāng)前代碼會在頁面加載階段就發(fā)請求,不僅占用主線程,還可能影響首屏性能。
那么如果使用 requestIdleCallback ,則可以等到瀏覽器“閑”下來再去上報。
requestIdleCallback(() => {
sendAnalyticsData()
})這就是一個典型的“低優(yōu)先級任務(wù)”場景。用 requestIdleCallback,讓瀏覽器自動幫咱們排好優(yōu)先順序。
利用 deadline 拆解任務(wù)
此時,假設(shè)我們有一個很大的任務(wù),比如:需要遍歷十萬條數(shù)據(jù)進(jìn)行處理。
const arr = Array.from({ length: 100000 }, (_, i) => i)
function task() {
while (arr.length > 0) {
helloSunday(arr.shift())
}
}
function helloSunday(i) {
console.log('hello', i)
}
task()如果咱們直接這樣寫代碼,那么在 企業(yè)項目 中,因為還需要處理更多的額外任務(wù),那么就一定會導(dǎo)致頁面嚴(yán)重卡頓,因為 JavaScript 是單線程的,這段任務(wù)會一直占著主線程不放。
而換成 requestIdleCallback,我們可以利用 deadline.timeRemaining() 檢查當(dāng)前幀的“空閑時間”,把任務(wù)拆成多次執(zhí)行。
- deadline:瀏覽器傳入的對象,包含當(dāng)前幀的剩余空閑時間
- deadline.timeRemaining():表示當(dāng)前幀還剩多少毫秒可以安全執(zhí)行任務(wù)
- deadline.didTimeout:表示任務(wù)是否超時(當(dāng)設(shè)置了 timeout 時,才會有用)
<body>
<div>測試</div>
<button onclick="renderClick()">點擊,進(jìn)行大量渲染</button>
<script>
const arr = Array.from({ length: 100000 }, (_, i) => i)
function workLoop(deadline) {
// 有安全執(zhí)行時間時,才會執(zhí)行
while (deadline.timeRemaining() > 0 && arr.length > 0) {
helloSunday(arr.shift())
}
if (arr.length > 0) {
// 再次觸發(fā)空閑回調(diào)
requestIdleCallback(workLoop)
}
}
function helloSunday(i) {
console.log('hello', i)
}
requestIdleCallback(workLoop)
// 渲染大量的 div
function renderClick() {
for (let i = 0; i < 50000; i++) {
const div = document.createElement('div')
div.textContent = `點擊渲染的元素 ${i}`
document.body.appendChild(div)
}
}
// 直接渲染
renderClick()
</script>
</body>通過以上代碼,咱們就可以測試出,在一開始瀏覽器忙的時候,requestIdleCallback 不會執(zhí)行。當(dāng)瀏覽器空閑下來之后,才會進(jìn)行處理。
咱們可以通過以下的表格,來對比下兩個函數(shù)的區(qū)別:
對比項 | setTimeout | requestIdleCallback |
調(diào)度方式 | 固定時間 | 主線程空閑時 |
精準(zhǔn)度 | 不穩(wěn)定,受任務(wù)隊列影響 | 智能調(diào)度,由瀏覽器控制(除非設(shè)置了 timeout) |
性能表現(xiàn) | 容易卡頓 | 平滑、不打斷渲染 |
適合場景 | 動畫延遲、節(jié)流防抖 | 預(yù)加載、日志、數(shù)據(jù)緩存、計算任務(wù) |
requestIdleCallback vs requestAnimationFrame
說完 requestIdleCallback,很多同學(xué)可能會想:它和 requestAnimationFrame(簡稱 rAF)是不是差不多啊?兩個名字都帶 request,還都和瀏覽器時機(jī)有關(guān)。
其實,它們的目標(biāo)是完全不同的。
requestAnimationFrame:關(guān)注 渲染幀,保證動畫和刷新同步。他會在 下一幀繪制前 調(diào)用,用來驅(qū)動動畫。requestIdleCallback:關(guān)注 空閑幀,在主線程空閑時執(zhí)行任務(wù)。他會在 瀏覽器空閑時 調(diào)用,用來執(zhí)行非關(guān)鍵任務(wù)。
兩者的典型場景
requestAnimationFrame:動畫、位移動效。
function moveBox() {
box.style.left = box.offsetLeft + 2 + 'px'
requestAnimationFrame(moveBox)
}
requestAnimationFrame(moveBox)這類任務(wù)要求和屏幕刷新頻率保持一致(比如:60fps),否則就會掉幀或卡頓,所以必須放在 rAF 中執(zhí)行。
例如:滾動聯(lián)動、進(jìn)度條、骨架屏、loading 動畫等。
requestIdleCallback:后臺任務(wù)、預(yù)加載。
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
helloSunday(tasks.shift())
}
})這類任務(wù)對時機(jī)要求不高,重點是不影響渲染。當(dāng)瀏覽器一幀執(zhí)行完、空出一點時間,它就會去做這些工作。
例如:日志上報、預(yù)取緩存、離線計算、大數(shù)據(jù)分片等。
Polyfill 與兼容性方案
目前,requestIdleCallback 并不是所有瀏覽器都支持,尤其是 Safari 和部分移動端 WebView。
但沒關(guān)系,我們可以自己實現(xiàn)一個簡易版本(Polyfill),通過 setTimeout 來模擬「空閑回調(diào)」的效果。
// 如果瀏覽器原生不支持 requestIdleCallback,則定義一個兼容版本
if (!window.requestIdleCallback) {
window.requestIdleCallback = function (cb) {
// 記錄當(dāng)前時間,用于計算剩余空閑時間
const start = Date.now()
// 使用 setTimeout 模擬空閑調(diào)度
// 在 1 毫秒后異步執(zhí)行回調(diào)函數(shù) cb
return setTimeout(() => {
// 手動構(gòu)造一個 deadline 對象,模擬瀏覽器傳入的參數(shù)
cb({
// 表示任務(wù)是否超時(這里固定為 false,因為沒有 timeout 機(jī)制)
didTimeout: false,
// timeRemaining 用于返回當(dāng)前幀還剩下多少“空閑時間”(毫秒)
// 假設(shè)一幀 50ms(對應(yīng) 20fps),
// 當(dāng)前時間 - start 表示已經(jīng)消耗的時間,
// 50 - 已消耗時間 = 剩余可用時間
// 若結(jié)果為負(fù),則取 0,避免返回負(fù)值
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start))
}
})
}, 1) // 延遲 1ms 調(diào)用,避免阻塞主線程
}
}
// 如果瀏覽器不支持 cancelIdleCallback,則提供對應(yīng)的取消方法
if (!window.cancelIdleCallback) {
window.cancelIdleCallback = function (id) {
// 直接調(diào)用 clearTimeout 取消 setTimeout 模擬的任務(wù)
clearTimeout(id)
}
}雖然這種方式無法真正識別主線程空閑時間,但在不支持 requestIdleCallback 的瀏覽器中,可以保證代碼結(jié)構(gòu)一致、功能不報錯。



























