在網頁中,有許多耗時可是卻又不能那麼緊要的任務。它們和緊要的任務,好比對用戶的輸入做出及時響應的之類的任務,它們共享事件隊列。若是二者發生衝突,用戶體驗會很糟糕。咱們可使用setTimout,對這些任務進行延遲處理。可是咱們並不知道,setTimeout在執行回調時,是不是瀏覽器空閒的時候。html
而requestIdleCallback就解決了這個痛點,requestIdleCallback會在幀結束時而且有空閒時間。或者用戶不與網頁交互時,執行回調。web
requestIdleCallback((deadline) => { // deadline.timeRemaining() 返回當前空閒時間的剩餘時間 if (deadline.timeRemaining() > 0) { task() } }, { timeout: 500 }) 複製代碼
requestIdleCallback 的callback會在瀏覽器的空閒時間運行,那麼什麼是空閒時間呢?瀏覽器
如上圖。當咱們在執行一段連續的動畫的時候,第一幀已經渲染到屏幕上了,到第二幀開始渲染,這段時間內屬於空閒時間。這種空閒時間會很是的短暫,若是咱們的屏幕是60hz(1s內屏幕刷新60次)的。那麼空閒時間會小於16ms(1000ms / 16)。markdown
另一種空閒時間,當用戶屬於空閒狀態(沒有與網頁進行任何交互),而且沒有屏幕中也沒有動畫執行。此時空閒時間是無限長的。可是爲了不不可預測的事(用戶忽然和網頁進行交互),空閒時間最大應該被限制在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指定的時間內,執行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延遲數據的上報,能夠避免一些渲染阻塞。
<!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> 複製代碼
而若是不使用 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> 複製代碼
緣由分析:
若是使用了requestIdleCallback:
監聽事件處理 --> 頁面渲染 --> 數據上報(空閒時) --> 監聽事件處理 --> 頁面渲染 --> 數據上報(空閒時)
若是不使用requestIdleCallback:
監聽事件處理 --> 數據上報(被添加到主線程中) --> 監聽事件處理 --> 數據上報(被添加到主線程中) --> 監聽事件處理 --> 數據上報(被添加到主線程中) --> 頁面渲染
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') }) 複製代碼