在网页中,有许多耗时可是却又不能那么紧要的任务。它们和紧要的任务,好比对用户的输入做出及时响应的之类的任务,它们共享事件队列。若是二者发生冲突,用户体验会很糟糕。咱们可使用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') }) 复制代码