为什么要说原生的 setInterval
性能比较低呢?这就要来说说 setInterval 的缺点了:
- 当页面被隐藏或最小化时,
setInterval
的回调函数仍然在后台执行,这就浪费了电脑的性能。 setInterval
每次将回调函数推入异步队列前,会检查异步队列中是否有该定时器的代码实例,如果存在,则不会添加本次回调函数,所以某次回调函数可能会被跳过。
所以在非必要的情况下,应该尽量避免直接使用 setInterval
。
今天本文要讲解的就是利用 window.requestAnimationFrame
来实现高性能版的 setInterval
。
requestAnimationFrame 是什么?
requestAnimationFrame
会在浏览器下次重绘之前调用指定的回调函数,并且将毫秒级的时间值传入到回调函数中。所以它的优点也很明显了:
-
执行频率是跟浏览器刷新频率保持同步的,不会卡顿、丢帧。
- 如果电脑的刷新频率为60HZ,
requestAnimationFrame
的回调函数就是在 1 / 60 ≈ 16.7ms 执行一次; - 如果电脑的刷新频率为90HZ,
requestAnimationFrame
的回调函数就是在 1 / 90 ≈ 11.1ms 执行一次;
- 如果电脑的刷新频率为60HZ,
-
节省CPU资源
当页面被隐藏或最小化时,
requestAnimationFrame
则会停止刷新动画,当页面恢复可见状态时,动画就从上次停止的地方继续执行。 -
高频率函数节流
在
resize
,scroll
等高频率事件中,为了防止屏幕在一个刷新间隔内发生多次函数执行,requestAnimationFrame
可保证在每个刷新间隔内函数只被执行一次。
利用 requestAnimationFrame 实现 setInterval
有的朋友可能会问,那用 setTimeout
来实现 setInterval
的性能会不会好一点呢?会好一点,但是比 requestAnimationFrame
差一点,因为对于 setTimeout
来说:
setTimeout
任务被放入异步队列,只有当主线程任务执行完后才会执行队列中的任务,因此实际执行时间总是比设定时间要晚。settimeout
设置的时间间隔不一定与屏幕刷新间隔时间相同,会引起丢帧。
所以,性能最好的是用requestAnimationFrame
来实现setInterval
。
实现思路:
- 首先肯定需要递归调用
requestAnimationFrame
,这样才达到setInterval
不断调用回调函数的效果。 - 通过一个变量来保存每次递归调用累积的时间,当这个变量大于等于设置的时间间隔时,就执行回调函数。
代码实现:
function setIntervalUsingRAF(callback, interval) {
let startTime = performance.now(); // 获取当前毫秒级的时间
let elapsedTime = 0; // 保存累积的时间
function loop(currentTime) {
const deltaTime = currentTime - startTime;
elapsedTime += deltaTime;
if (elapsedTime >= interval) {
// 大于设置的时间间隔,就执行回调函数,并且重置 elapsedTime 变量
callback();
elapsedTime = 0;
}
startTime = currentTime;
requestAnimationFrame(loop); // 递归调用 requestAnimationFrame
}
requestAnimationFrame(loop);
}
我们还缺少一个取消定时器的功能。
在添加这个功能之前,我们需要了解 requestAnimationFrame
的两个特点:
-
requestAnimationFrame
的回调函数是异步执行的,举个例子:window.requestAnimationFrame(() => { console.log(1); }); console.log(2);
打印顺序是:2,1。
-
不能在
requestAnimationFrame
的回调函数里取消本次的执行,只能取消下一次的执行,举个例子:const rafId = window.requestAnimationFrame(() => { cancelAnimationFrame(rafId); console.log(1); }); console.log(2);
依然会输出 2,1。想要取消本次的执行,只能在回调函数外部执行
cancelAnimationFrame(rafId)
,比如:const rafId = window.requestAnimationFrame(() => { console.log(1); }); cancelAnimationFrame(rafId); console.log(2);
此时只会输出 2。
因为
rafId
代表的是本次requestAnimationFrame
回调函数的执行,那本次回调函数已经执行了,还怎么在回调函数里面取消呢?
添加取消定时器
由于 requestAnimationFrame
是异步执行回调函数的,所以我们可以将递归调用 requestAnimationFrame
的代码放到最前面:
function setIntervalUsingRAF(callback, interval) {
// ...省略代码
function loop(currentTime) {
requestAnimationFrame(loop);
// ...省略代码
}
// ...省略代码
}
我们还需要一个变量来保存每次调用 requestAnimationFrame
函数的返回值 ID:
function setIntervalUsingRAF(callback, interval) {
// ...省略代码
let rafId;
function loop(currentTime) {
rafId = requestAnimationFrame(loop);
// ...省略代码
}
rafId = requestAnimationFrame(loop);
}
最后返回一个函数来取消定时器的功能:
function setIntervalUsingRAF(callback, interval) {
// ...省略代码
let rafId;
function loop(currentTime) {
rafId = requestAnimationFrame(loop);
// ...省略代码
}
rafId = requestAnimationFrame(loop);
return function cancalLoop() {
cancelAnimationFrame(rafId);
};
}
这时候如果在回调函数中执行返回的取消函数,那么取消的是下一次的执行。比如:
let count = 1;
const cancalSetInterval = setIntervalUsingRAF(() => {
if (count > 3) {
cancalSetInterval();
}
console.log(count);
count++;
}, 1000);
// 1
// 2
// 3
// 4
由于取消的是下一次的执行,就会多输出一次(4)。
完整代码
function setIntervalUsingRAF(callback, interval) {
let startTime = performance.now(); // 获取当前毫秒级的时间
let elapsedTime = 0; // 保存累积的时间
let rafId; // 保存每次执行 requestAnimationFrame 的 ID
function loop(currentTime) {
// 由于 requestAnimationFrame 是异步执行回调函数的
// 所以递归调用 requestAnimationFrame 可以放到最前面
rafId = requestAnimationFrame(loop);
const deltaTime = currentTime - startTime;
elapsedTime += deltaTime;
if (elapsedTime >= interval) {
// 大于设置的时间间隔,就执行回调函数,并且重置 elapsedTime 变量
callback();
elapsedTime = 0;
}
startTime = currentTime;
}
rafId = requestAnimationFrame(loop);
// 返回取消定时器的函数
return function cancalLoop() {
cancelAnimationFrame(rafId);
};
}