详解 requestIdleCallback

为何须要 requestIdleCallback ?

在网页中,有许多耗时可是却又不能那么紧要的任务。它们和紧要的任务,好比对用户的输入做出及时响应的之类的任务,它们共享事件队列。若是二者发生冲突,用户体验会很糟糕。咱们能够使用setTimout,对这些任务进行延迟处理。可是咱们并不知道,setTimeout在执行回调时,是不是浏览器空闲的时候。javascript

而requestIdleCallback就解决了这个痛点,requestIdleCallback会在帧结束时而且有空闲时间。或者用户不与网页交互时,执行回调。html

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会在浏览器的空闲时间运行,那么什么是空闲时间呢?java

空闲时间1.png

如上图。当咱们在执行一段连续的动画的时候,第一帧已经渲染到屏幕上了,到第二帧开始渲染,这段时间内属于空闲时间。这种空闲时间会很是的短暂,若是咱们的屏幕是60hz(1s内屏幕刷新60次)的。那么空闲时间会小于16ms(1000ms / 16)。web

空闲时间2.png

另一种空闲时间,当用户属于空闲状态(没有与网页进行任何交互),而且没有屏幕中也没有动画执行。此时空闲时间是无限长的。可是为了不不可预测的事(用户忽然和网页进行交互),空闲时间最大应该被限制在50ms之内。浏览器

为何最大是50ms?人类对100ms内的响应会认为是瞬时的。将空闲时间限制在50ms之内,是为了不,空闲时间内执行任务,从而致使了对用户操做响应的阻塞,使用户感到明显的响应滞后。函数

在空闲期间,callback的执行顺序是以FIFO(先进先出)的顺序。可是若是在空闲时间内依次执行callback时,有一个callback的执行时间,已经将空闲时间用完了,剩下的callback将会在下一次的空闲时间执行。oop

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列表的末尾。动画

const startTask = (deadline) {
    // 若是 `task` 花费的时间是20ms
    // 超过了当前空闲时间的剩余毫秒数,咱们等到下一次空闲时间执行task
    if (deadline.timeRemaining() <= 20) {
        // 将任务带到下一个空闲时间周期内
        // 添加到下一个空闲时间周期callback列表的末尾
        requestIdleCallback(startTask)
    } else {
        // 执行任务
        task()
    }
}
复制代码

当咱们网页处于不可见的状态时(好比切换到其余的tag),咱们空闲时间将会每10s, 触发一次空闲期。ui

timeout

若是指定了timeout,可是浏览器没有在timeout指定的时间内,执行callback。在下次空闲时间时,callback会强制执行。而且callback的参数,deadline.didTimeout等于true, deadline.timeRemaining()返回0。google

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')
})
复制代码

参考

相关文章
相关标签/搜索