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




























