詳解 requestIdleCallback

爲何須要 requestIdleCallback ?

在網頁中,有許多耗時可是卻又不能那麼緊要的任務。它們和緊要的任務,好比對用戶的輸入做出及時響應的之類的任務,它們共享事件隊列。若是二者發生衝突,用戶體驗會很糟糕。咱們可使用setTimout,對這些任務進行延遲處理。可是咱們並不知道,setTimeout在執行回調時,是不是瀏覽器空閒的時候。html

而requestIdleCallback就解決了這個痛點,requestIdleCallback會在幀結束時而且有空閒時間。或者用戶不與網頁交互時,執行回調。web

requestIdleCallback API簡介

  • requestIdleCallback的第一個參數時callback
    • 當callback被調用時,回接受一個參數 deadline,deadline是一個對象,對象上有兩個屬性
      • timeRemaining,timeRemaining屬性是一個函數,函數的返回值表示當前空閒時間還剩下多少時間
      • didTimeout,didTimeout屬性是一個布爾值,若是didTimeout是true,那麼表示本次callback的執行是由於超時的緣由
  • requestIdleCallback的第二個參數是options
    • options是一個對象,能夠用來配置超時時間
requestIdleCallback((deadline) => {
    // deadline.timeRemaining() 返回當前空閒時間的剩餘時間
    if (deadline.timeRemaining() > 0) {
        task()
    }
}, {
    timeout: 500
})
複製代碼

空閒時間

requestIdleCallback 的callback會在瀏覽器的空閒時間運行,那麼什麼是空閒時間呢?瀏覽器

空閒時間1.png

如上圖。當咱們在執行一段連續的動畫的時候,第一幀已經渲染到屏幕上了,到第二幀開始渲染,這段時間內屬於空閒時間。這種空閒時間會很是的短暫,若是咱們的屏幕是60hz(1s內屏幕刷新60次)的。那麼空閒時間會小於16ms(1000ms / 16)。markdown

空閒時間2.png

另一種空閒時間,當用戶屬於空閒狀態(沒有與網頁進行任何交互),而且沒有屏幕中也沒有動畫執行。此時空閒時間是無限長的。可是爲了不不可預測的事(用戶忽然和網頁進行交互),空閒時間最大應該被限制在50ms之內。函數

爲何最大是50ms?人類對100ms內的響應會認爲是瞬時的。將空閒時間限制在50ms之內,是爲了不,空閒時間內執行任務,從而致使了對用戶操做響應的阻塞,使用戶感到明顯的響應滯後。oop

在空閒期間,callback的執行順序是以FIFO(先進先出)的順序。可是若是在空閒時間內依次執行callback時,有一個callback的執行時間,已經將空閒時間用完了,剩下的callback將會在下一次的空閒時間執行。動畫

const task1 = () => console.log('執行任務1')
const task2 = () => console.log('執行任務2')
const task3 = () => console.log('執行任務3')

// console
// 執行任務1
// 執行任務2
// 執行任務3
requestIdleCallback(task1)
requestIdleCallback(task2)
requestIdleCallback(task3)
複製代碼

若是當前的任務所須要的執行時間,超過了當前空閒時間週期內的剩餘時間,咱們也能夠將任務帶到下一個空閒時間週期內執行。在下一個空閒週期開始後,新添加的callback會被添加到callback列表的末尾。google

const startTask = (deadline) {
    // 若是 `task` 花費的時間是20ms
    // 超過了當前空閒時間的剩餘毫秒數,咱們等到下一次空閒時間執行task
    if (deadline.timeRemaining() <= 20) {
        // 將任務帶到下一個空閒時間週期內
        // 添加到下一個空閒時間週期callback列表的末尾
        requestIdleCallback(startTask)
    } else {
        // 執行任務
        task()
    }
}
複製代碼

當咱們網頁處於不可見的狀態時(好比切換到其餘的tag),咱們空閒時間將會每10s, 觸發一次空閒期。spa

timeout

若是指定了timeout,可是瀏覽器沒有在timeout指定的時間內,執行callback。在下次空閒時間時,callback會強制執行。而且callback的參數,deadline.didTimeout等於true, deadline.timeRemaining()返回0。線程

requestIdleCallback((deadline) => {
    // true
    console.log(deadline.didTimeout)
}, {
    timeout: 1000
})

// 這個操做大概花費5000ms
for (let i = 0; i < 3000; i++) {
    document.body.innerHTML = document.body.innerHTML + `<p>${i}</p>`
}
複製代碼

requestIdleCallback實踐:在requestIdleCallback中打點

使用requestIdleCallback延遲數據的上報,能夠避免一些渲染阻塞。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <input type="text" id="text" />
</body>
<script> const datas = [] const text = document.getElementById('text') let isReporting = false function sleep (ms = 100) { let sleepSwitch = true let s = Date.now() while (sleepSwitch) { if (Date.now() - s > ms) { sleepSwitch = false } } } function handleClick () { datas.push({ date: Date.now() }) // 監聽用戶響應的函數,須要花費150ms sleep(150) handleDataReport() } // ========================= 使用requestIdleCallback ============================== function handleDataReport () { if (isReporting) { return } isReporting = true requestIdleCallback(report) } function report (deadline) { isReporting = false while (deadline.timeRemaining() > 0 && datas.length > 0) { get(datas.pop()) } if (datas.length) { handleDataReport() } } // ========================= 使用requestIdleCallback結束 ============================== function get(data) { // 數據上報的函數,須要話費20ms sleep(20) console.log(`~~~ 數據上報 ~~~: ${data.date}`) } text.oninput = handleClick </script>
</html>
複製代碼

QQ20200304-173611-HD.gif

而若是不使用 requestIdleCallback , 直接進行數據上報,會直接卡死主線程,影響到瀏覽器的渲染。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <input type="text" id="text" />
</body>
<script> const datas = [] const text = document.getElementById('text') let isReporting = false function sleep (ms = 100) { let sleepSwitch = true let s = Date.now() while (sleepSwitch) { if (Date.now() - s > ms) { sleepSwitch = false } } } function handleClick () { datas.push({ date: Date.now() }) // 監聽用戶響應的函數,須要花費150ms sleep(150) handleDataReport() } // ========================= 不使用requestIdleCallback ============================== function handleDataReport () { if (isReporting) { return } isReporting = true report() } function report (deadline) { isReporting = false while (datas.length > 0) { get(datas.pop()) } if (datas.length) { handleDataReport() } } // ========================= 不使用requestIdleCallback結束 ============================== function get(data) { // 數據上報的函數,須要話費20ms sleep(20) console.log(`~~~ 數據上報 ~~~: ${data.date}`) } text.oninput = handleClick </script>
</html>
複製代碼

QQ20200304-175859-HD.gif

緣由分析:

若是使用了requestIdleCallback:

監聽事件處理 --> 頁面渲染 --> 數據上報(空閒時) --> 監聽事件處理 --> 頁面渲染 --> 數據上報(空閒時)

若是不使用requestIdleCallback:

監聽事件處理 --> 數據上報(被添加到主線程中) --> 監聽事件處理 --> 數據上報(被添加到主線程中) --> 監聽事件處理 --> 數據上報(被添加到主線程中) --> 頁面渲染

常見Q&A

Q1: requestIdleCallback 會在每一次幀結束時執行嗎?

A1: 只會在幀末尾有空閒時間時會執行,不該該指望每一次幀結束都會執行requestIdleCallback。

😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂😂

Q2: 什麼操做不適合放到 requestIdleCallback 的callback中。

A2: 更新DOM,以及Promise的回調(會使幀超時),什麼意思?請看下面的代碼。requestIdleCallback中代碼,應該是一些能夠預測執行時間的小段代碼。

// console
// 空閒時間1
// 等待了1000ms
// 空閒時間2
// Promise 會在空閒時間1接受後當即執行,即便沒有空閒時間了也是如此。拖延了進入下一幀的時間

requestIdleCallback(() => {
    console.log('空閒時間1')
    Promise.resolve().then(() => {
        sleep(1000)
        console.log('等待了1000ms')
    })
})

requestIdleCallback(() => {
    console.log('空閒時間2')
})
複製代碼

參考

相關文章
相關標籤/搜索