传统定时器的误差出现的原因
- setInterval、setTimeout 实现都会出现误差,这源于js的单线程。
- 他们的回调函数并不是到时候立即执行,而是等系统计算资源空闲下来后才会执行。
- setInterval、setTimeout 都属于宏任务(结合事件循环机制)。
setInterval的误差
setInterval 定时器在一定时间内执行某个方法,下面通过一个案例的思想💭向大家演示它在使用中出现误差的过程。
- 有一个setTimeout,4s后触发一次,耗时9s
- 有一个setlnterval,每
5s
触发一次,每次耗时4S - 用户在程序第3s的时候,触发点击事件,添加onClick事件,onClick耗时6s
按照以上逻辑直接来看浏览器是如何执行的:
看到控制台的最后一行输出,此时是setInterval的第二次执行
,按理来说它应该与上一次间隔5s
后执行,为什么这里只间隔了4s
呢🤔?
接着往下看:
📍为什么,定时器代码间隔会比预期要小?
在讲解以上时序图之前,必须先掌握三点概念:
- JS主线程和计时器线程是相互独立的;
- 宏任务队列一次只能执行一个 ;
- setInterval往宏任务队列注册事件时,如果它上一个回调函数没有被执行,那么新的事件不会被注册;
现在开始讲解上面的时序图:
- 在刚开始第3s时,执行了点击事件,onclick事件进入宏任务队列,并被执行。但是在第4s时setTimeout回调进入宏任务队列,setInterval在第5s注入回调。
- 在第9s时执行setTimeout回调,当timeout事件执行完毕后时间已经来到了
第18s,此时执行第一个setInterval
。在这个过程中的第10s和第15s时setInterval试图继续注入回调Interval-2和Interval-3,但由于Interval-1还没被执行,所以注入失败,Interval-2和Interval-3被丢弃。 - 在第20s时,Interval-4成功注入宏任务队列,
第22s时,Interval-4开始执行
。22s和18s之间间隔4s。 - 第30秒注册Interval-6,随后立即执行,没有收到其他延时干扰,所以在Interval-7注册时,setInterval时间间隔恢复正常。
setlnterval-累计效应
- 定时器代码执行之间的间隔可能比你预期的要小
- 定时器某些间隔被跳过(例如Interval-2和Interval-3的无效注册)
setInterval & setTimeout的执行时机
-
代码层面chrome 中 setInterval 最小延迟是1ms,而 setTimeout 则是0ms
-
以chrome浏览器为例, setTimeout 中 delay 小于1ms时和预期行为不符,是因为源码中小于1ms被定义为与0ms一样的‘立即’执行任务了。还有个小点 setTimeout 的delay是向下取整的即1.9ms和1ms等价、0.8ms和0ms等价
-
setTimeout 的最大延迟时间是2的31次方-1,超过这个数字会立即输出
-
node中 setTimeout 的delay小于1ms时会被修改为1ms
简单的示例一:
// 设置最大值
setTimeout(()=>{
console.log('a')
}, 2**31);
//设置最小值
setTimeout(()=>{
console.log('b')
}, 1);
setTimeout(()=>{
console.log('c')
}, 0.5);
//设置0
setTimeout(()=>{
console.log('d')
}, 0);
输出结果:a c d b
简单的示例二:
// 设置最大值
setInterval(()=>{
console.log('a')
}, 2**31);
//设置最小值
setInterval(()=>{
console.log('b')
}, 1);
setInterval(()=>{
console.log('c')
}, 0.5);
//设置0
setInterval(()=>{
console.log('d')
}, 0);
输出结果:a b c d
setTimeout 5 层以上的嵌套会导致至少 4ms 的延五成
let t1 = performance.now();
//打印时间
function printTime(count) {
const now = performance. now();
console.log(count,"==时间差:",now -t1);
t1 = now;
}
setTimeout(()=>{
printTime(1);
setTimeout(()=>{
printTime(2);
setTimeout(()=>{
printTime(3);
setTimeout(()=>{
printTime(4);
setTimeout(()=>{
printTime(5);
setTimeout(()=>{
printTime(6);
},0)
},0)
},0)
},0)
},0)
},0)
输出结果:
📍setTimeout 与 setlnvertal 的区别
- setTimeout 是递归循环,它基本上可以保证代码的执行顺序,每次的至少延迟时长大于等于设置的时间。
- setlnvertal 每次定时触发执行回调函数,它的执行时间间隔可能会比期待的要小,而且不关心前一个回调函数是否执行,不会重复注册回调。
新生代定时器
requestAnimationFrame
-
requestAnimationFrame 告诉浏览器,我希望执行一个动画,并要求浏览器在下次重绘之前执行指定的回调函数更新动画
-
回调函数执行次数与浏览器屏幕的刷新次数匹配。一般为每秒60次
requestAnimationFrame 在事件循环中的执行时机
-
回顾事件循环步骤:1个宏任务 -> 所有微任务 -> 是否需要渲染 -> 渲染UI
-
在事件循环中,requestAnimationFrame 实际上就是在UI渲染中执行的
requestAnimationFrame 对比 setTimeout
requestAnimationFrame 由系统决定回调函数的执行时机,不需要使用setTimeout/setlnvertal 去计算刷新时间,节省了不必要的浪费,动画看起来更加流畅。
渲染差异原因分析:
1、在Javascript中, setTimeout
属于异步队列,只有当主线程上的任务执行完以后,才会去检查该队列里的任务是否需要开始执行,因此 setTimeout
的实际执行时间一般要比其设定的时间晚一些。
2、刷新频率受屏幕分辨率
和屏幕尺寸
的影响,因此不同设备的屏幕刷新频率可能会不同,而 setTimeout
只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相同,从而引起丢帧现象。
3、requestAnimationFrame
最大的优势是由系统来决定回调函数的执行时机。如果屏幕刷新率是60Hz
,那么回调函数就每16.7ms
被执行一次;如果刷新率是75Hz
,那么这个时间间隔就变成了1000/75=13.3ms
;requestAnimationFrame
的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
转载:www.cnblogs.com/shymi/p/165…
requestAnimatFrame 优点
requestldleCallback
- requestldleCallback 方法在浏览器的空闲时段调用函数排队
⏱️ requestldleCallback 的空闲时间是怎么计算的
- 存在连续渲染的两帧,空闲时间就是帧的频率减去执行任务的时间,减去绘制的时间
- 当一段时间没有绘制或者任务发生,空闲时间将会尽可能变大,但不会超过50ms
requestIdleCallback 如何使用
以下代码可通过改变 timeout 的设定值来决定requestIdleCallback回调函数的执行时机:
<style>
.animate-ele{
width: 50px;
height: 50px;
background-color: red;
}
</style>
<body>
<div id="animateEle" class="animate-ele"></div>
<button id="start">开始</button>
<script>
//同步耗吋操作
function syncSleep (duration) {
const now = Date.now();
while (now + duration > Date.now()) { }
}
const element = document.getElementById('animateEle');
let count = 0
function step(timestamp) {
console.log ("渲染帧");
count++;
if (count < 500) {
element. style.transform = 'translateX(' + count + 'px)';
window.requestAnimationFrame(step);
}
}
start.onclick = function () {
console.log ("启动帧")
// 使用requestAnimationFrame循环调用step是为了让每一帧都有输出
// 使得每一帧都能让界面有绘制
window.requestAnimationFrame(step);
requestIdleCallback ((idleDeadline) => {
// didTimeout表示是否超时正在执行
const didTimeout = idleDeadline.didTimeout ? '超时正在执行' : '未超时执行'
// timeRemaining()表示当前帧还剩余多少时间(以毫秒计算)
const timeRemaining = idleDeadline.timeRemaining();
console.log("didTimeout==", didTimeout, "==", timeRemaining)
},{timeout: 50}); //timeout第二个参数,期望在规定时间内执行回调
console.log("执行onClick");
setTimeout (() => {
console.log("执行timeout");
syncSleep(1000);
console.log("执行timeout完成");
Promise.resolve().then(function () {
console.log("promise 微任务");
});
}, 50)
syncSleep(1000); // 同步阻塞1s
console.log("执行onClick完毕");
}
</script>
</body>
输出结果:
不设置第二个参数 timeout ,输出结果:
总结
-
setlnterval 本身存在累计效应
-
setTimeout 存在最低延迟时间;实现动画时,无法与屏幕刷新保持步调一致
-
requestAnimationFrame 系统自动调用,保障刷新频率
-
requestldleCallback 处理低优先级任务空闲时间调用
原文链接:https://juejin.cn/post/7313557322597154855 作者:Dr_哈哈