众所周知,setTimeout是指在最小阈值(m个单位)后运行脚本,setInterval是指以最小阈值周期连续执行指定的脚本。请注意,我在这里使用术语“最小阈值”,因为它并不总是准确的。为什么setTimeout和setInterval不准确?要回答这个问题,首先需要了解JavaScript宿主环境(浏览器或Node.js)有一种称为事件循环的机制。现在简单介绍一下:首先,为什么要有事件循环?因为对于浏览器来说,需要接收用户的交互来实现一些UI的变化,而JS脚本在浏览器中是单线程运行的,无法直接知道用户的操作。所以这里加了一个事件循环机制,浏览器内部进程通知JS让JS执行指定的脚本。对于setTimeout和setIntervalAPI,它们比较特殊,因为它们都可以生成宏任务。那么什么是宏任务呢?宏任务可以理解为需要JS执行的任务。例如,用户交互点击会生成一个宏任务,浏览器会将点击的宏任务添加到任务队列中。当JS执行引擎空闲时,会取出队头的任务执行,从而执行点击绑定的回调函数。也就是说,setTimeout和setInterval不会立即执行。当我们在代码中调用它们的时候,会先将它们加入到任务队列中进行排队,然后JS执行引擎在空闲的时候会从队列的头部取出任务执行。如果是定时任务,会检查是否超时。如果过期,它将被获取并执行。如果未过期,将执行以下宏任务。执行完成后会开始新的循环,继续检查任务队列的头部。这也解释了为什么他们不准确,因为当他们入队任务时,可能已经有任务排在他们前面,他们必须排在那些任务后面。所以,当前面的任务完成,轮到他们执行的时候,实时间隔可能已经超过了我们传的值,这就是最小阈值的由来。实时间隔只能大于等于我们传递的值。事件循环机制可以查看其他信息。这里忽略微任务和宏任务的区别,只是为了解释为什么时机不准确。letcount=0;conststartTime=Date.now();setInterval(()=>{count+=1000;console.log('偏差',Date.now()-(startTime+count));},1000);//deviation:2//deviation:5//deviation:4//deviation:3从上面的代码可以看出setInterval总是不准确的。如果在代码中加入耗时任务,差异会越来越大(setTimeout也是一样)。如何得到一个比较准确的setInterval?1.使用while简单粗暴。我们可以直接使用while语句阻塞主线程,不断计算当前时间和下一次时间的时间差。一旦大于等于0,立即执行。代码如下:functionsleepInterval(cb,time){letcount=0,startTime=Date.now()while(count<=time){letnowTime=Date.now()-startTimeletnextTime=count+1000if(nowTime>=nextTime){count=nextTimecb&&cb();}}}functionfoo(){console.log('foo')}sleepInterval(loga,3000)这个函数可以作为js中的休眠函数,更多的定时执行函数这个方法可以控制差值稳定在0,但是这个方法会阻塞JS执行线程,导致JS执行线程无法停止,无法从队列中取出任务。这会导致页面冻结并且对任何操作都没有响应。这是破坏性的,因此不可取。2、使用requestAnimationFrame浏览器提供了requestAnimationFrameAPI,它告诉浏览器你要执行一个动画,要求浏览器在下次重绘前调用指定的回调函数来更新动画。这个回调函数会在浏览器下次重绘之前执行。每秒执行的次数将根据屏幕的刷新率来确定。60Hz的刷新率意味着每秒会有60次,也就是16.7ms左右。函数intervalTimer(time){让计数器=1;conststartTime=Date.now();函数main(){constnowTime=Date.now();constnextTime=开始时间+计数器*时间;if(nowTime-nextTime>=0){console.log('偏差',nowTime-nextTime);计数器+=1;}window.requestAnimationFrame(main);}main();}intervalTimer(1000);//偏差5//偏差7//偏差9//偏差12我们可以发现,由于16.7ms的间隔执行,很容易造成时间不准确。3、使用setTimeout+系统时间偏移该方案的原理是利用当前系统的准确时间在每次之后进行补偿校正,setTimeout保证后续的定时时间为补偿后的时间,从而减小时间差.functionintervalTimer(callback,interval=500){让计数器=1;让timeoutId;conststartTime=Date.now();函数main(){constnowTime=Date.now();constnextTime=startTime+counter*interval;timeoutId=setTimeout(main,interval-(nowTime-nextTime));console.log('偏差',nowTime-nextTime);计数器+=1;打回来();}timeoutId=setTimeout(main,interval);返回()=>{clearTimeout(timeoutId);};}letvalue=10;constcancelTimer=intervalTimer(()=>{if(value>0){value-=1;}else{cancelTimer();}},1000);//偏差3//偏差1//偏差0//偏差2,我们可以看到这个方案的时间差异比较小,可预见的差异不会随着时间的推移逐渐增大,始终稳定在可接受的范围内。综上所述,我们在不阻塞主线程的情况下实现了稳定且相对准确的setInterval,而第三种方式可能是获得准确倒计时的最佳方案。额外的!!!每天进步一点点,推荐大家关注前端之路小程序,里面有你想看的热门文章,定期的前端周刊,面试必看的八股文集,更重要的是,我们有一群志同道合的热爱前端开发的小可爱们~
