本文主要写的是window对象上的requestIdleCallback,用一个例子示范下如何使用requestIdleCallback。
在理解requestIdleCallback的概念之前,我们需要知道一个概念,叫做浏览器的空闲时期。
众所周知,浏览器是会不断地去绘制页面,比如我们最常听到的版本是每隔16.7ms的时候刷新一次页面,以保证流畅性。
一开始的时候,我们的网页刚进来,由于任务很繁重,所以会出现页面卡顿,或者是白屏,如下图的performance中显示的花花绿绿的方块。
而当我们的页面渲染的比较"稳定"之后,我们会看到如下面的图中很多的空白区域。
而其中的840ms到852ms就是一个渲染周期,由于此时已经结束了前期很繁重的任务,所以会看到中间会有很多的空闲时间,这段时间,我们的浏览器就很"空闲".此时的时间,由于是还有些许的任务,所以一个渲染周期我们可以理解为12ms一次渲染。
到了后期,页面更加稳定了,基本很久都不动了,这时候我们就会发现,页面的渲染周期更长了,有50ms,并且空闲的时间更多了,大概有49ms
个人建议好好去了解下前端的微任务以及宏任务,一个渲染周期等,我觉得这个我可以水一篇博客(0.0)
window.requestIdleCallback() 方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。
假设,我们的项目中有不少的琐碎的且优先级比较低的一些操作,比如是错误上报。
错误上报是一个不怎么重要的事情,因为我们只需要上报出错误,不需要管回来的数据是什么,所以只需要几ms就可以了,而这个事件就可以放在我们的空闲时间中。
当然,我本文的例子中不会使用错误上报来做例子,毕竟只有几毫秒,我们很难直接看到效果,所以我会选择在输入框的input时间做一些耗时的操作。
首先,来一段代码,内容很简单,一个TextArea,当触发input事件的时候,会做一串的操作,而每次的操作时间都超过50ms,模拟一个比较耗时的操作。
而由于每次都超过50ms,所以到了后面,每次输入都会很卡,甚至会让页面卡死。
<html>
<title>测试requestIdleCallback</title>
<body>
<textarea
class="textarea"
rows="10"
cols="40"
oninput="handleChange(this)"
placeholder="请在这儿输入"
></textarea>
<div class="logDiv"></div>
</body>
<script>
// 假的敏感词数组。
const arr = new Array(1000 * 1000).fill(1).map((v, index) => index);
const sendList = [];
// 触发事件
function handleChange(e) {
sendList.push(e.value);
sendRequest();
}
// 假装发送请求校验。
function sendRequest() {
if (sendList.length > 0) {
const value = sendList[0];
const startTime = +new Date();
// 两个耗时的map事件
arr.map((num) => num == value);
arr.map((num) => num == value);
?
const endTime = +new Date();
console.log(`time:${endTime - startTime}`);
sendList.shift();
}
}
</script>
</html>
?
一般来说,我们是会在用户最终提交数据的时候才做敏感词校验,但是这儿是做一个示例,所以请不要介意这么多,也不要考虑防抖与节流。
如上图,我一直按着"1",但是由于一直触发着sendRequest的事件,所以页面并没有很顺畅,甚至在后期的时候很卡顿。
前面已经讲了,requestIdleCallback的用途是将一些不怎么重要的事件放在空闲时间中去运行。接下来先讲解下相关的语法。
requestIdleCallback(callback)
requestIdleCallback(callback, options)
requestIdleCallback需要传入的一个必然参数是callback,也就是我们想要在空闲时间调用的那个参数,options 是一个可选的参数,且只有一个timeout的参数,示例的代码如下
requestIdleCallback(
(deadline) => {
if (deadline.timeRemaining() > 16.7 || deadline.didTimeout)
{ }
}
, { timeout: 60 * 1000 }
)
在调用的函数中,获取到了一个对象–deadline,且含有以下两个属性
timeout
回调在 timeout 毫秒过后还没有被调用,那么回调任务将放入事件循环中排队,即使这样做有可能对性能产生负面影响。
didTimeout
一个布尔值,如果回调是因为超过了设置的超时时间而被执行的,则其值为 true。
timeRemaining
返回一个浮点数字,用来表示当前闲置周期的预估剩余毫秒数。如果闲置期已经结束,则其值为 0。你的回调函数可以重复调用该函数,以判断目前是否有足够的时间来执行更多的任务。
既然了解了requestIdleCallback的用法,现在就将其结合在我们的项目中。
至于前面的16.7,是因为下面 的说法,但是不是绝对的。
回调函数执行次数通常是每秒 60 次,但在大多数遵循 W3C 建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。
结合上面,我们的代码如下
<html>
<title>测试requestIdleCallback</title>
<body>
<textarea
class="textarea"
rows="10"
cols="40"
oninput="handleChange(this)"
placeholder="请在这儿输入"
></textarea>
<div class="logDiv"></div>
</body>
<script>
// 假的敏感词数组。
const arr = new Array(1000 * 1000).fill(1).map((v, index) => index);
const sendList = [];
// 触发事件
function handleChange(e) {
sendList.push(e.value);
sendRequest();
}
// 假装发送请求校验。
function sendRequest() {
requestIdleCallback(
(deadline) => {
if (
// 当前的空余时间不超过16.7s,并且不是被强制执行的。
(deadline.timeRemaining() > 16.7 || deadline.didTimeout) &&
sendList.length > 0
) {
const value = sendList[0];
const startTime = +new Date();
// 两个耗时的map事件
arr.map((num) => num == value);
arr.map((num) => num == value);
?
const endTime = +new Date();
console.log(`time:${endTime - startTime}`);
sendList.shift();
}
// 判断是否有时间可以继续进行循环
sendList.length > 0 && requestIdleCallback(sendRequest);
},
{ timeout: 60 * 1000 }
);
}
</script>
</html>
?
sendRequest 代码的逻辑如下:
当事件触发的时候,我们会调用该API,将事情放在空闲时间中去执行,判断当前的时间是否是大于16.7ms,这个根据每个项目来判定,因为不同的情况会有不同的判断,而此时我判断16.7ms,是为了让其在50ms的真正空闲时间内执行。
当我们在空余时间执行了之后,会再次调用一次事件,这样子会继续在调用一次,看本次是否还剩余时间足够我们进行操作。
如果在60s内,事件没有被调用过,那么就会强制执行!
效果如下
可以看到页面已经好了很多,并且页面没有什么大问题了,只是页面还是有点卡顿,这是因为我们的页面到了后期的时候还是在一直发起sendRequest方法,所以页面就有点卡,但是比之前好多了。
我想,通过了上面的例子,大家都理解了这个API的方法是怎么用的了,但是需要注意,我们是将任务堆放在了空闲时间中,而空闲时间一般不超过16.7ms,我们很难知道16.7ms内能做什么事情,尤其是我们很少有这么低优先级以及这么琐碎的任务。
但是对于一些框架来说,这个API可以做一些架构级别的处理,比如React的fiber就根据这个Api解决了一些小问题,并且重构了Vdom树的结构,下一篇博客会解释。
但是!!!!
正如我前面所说,我们很难知道一些事务是否很小,可以在16.7ms内完成,若事件的时间超过16.7ms,还会影响下次的渲染,所以我们一般会选择使用settimeout来将其放在下一次渲染,而大的事件会放在后面的空闲时间。
但是!!!!
上面代码中,我们无法预测用户如何停止输入文本框的事情,所以我们无法使用settimeout去指定一个时间执行业务,而此时,这个API就显得很重要,因为我们可以知道页面什么时候真的"空闲",不过还是那句话,除非我们构建自己的框架,不然很少用到
且不少同学会不小心的在这儿更新一些页面的操作,所以就很不友好,毕竟很少有可以预测的小任务。
这个API,而且在兼容性上也有问题,所以在百度等网站中,我们会看到下面的代码
this.requestIdleFn = window.requestIdleCallback ?
window.requestIdleCallback.bind(window) :
function (e) {
var t = Date.now();
return setTimeout(function () {
e({
didTimeout: !1,
timeRemaining: function () {
return Math.max(0, 16 - (Date.now() - t))
}
})
}, 1)
}
一定要小心,要是requestIdleCallback中放入了执行时间较长的任务,会导致页面出现卡顿, 也就是我们所说的"丢帧".
建议无聊时间看看岛田庄司的<屋顶上的小丑>,书的结构还不错。
====================================================
前端开发的博客,偶尔写一些历史的整理,由衷期望各位大佬们扫码关注