介绍本文讲解倒计时为什么推荐使用setTimeout而不是setInterval,倒计时为什么会报错,以及如何解决。倒数计时器在前端开发中,倒数计时器功能比较常见,比如对活动进行倒计时,假设只有10秒,常见的两种写法如下://setTimeoutimplementationvarcountdownTime=10;//倒计时秒数varcountdown=function(){varsetTimeoutHandler=setTimeout(function(){countdownTime--;console.log('Countdown:'+countdownTime+'seconds');if(countdownTime===0){console.log('倒计时结束!');clearTimeout(setTimeoutHandler);}else{countdown();}},1000)};countdown();//setInterval实现varcountdownTime=10;//倒计时秒数varcountdown=function(){varsetIntervalHandler=setInterval(function(){countdownTime--;console.log('Countdown:'+countdownTime+'seconds');if(countdownTime===0){console.log('倒计时结束!');clearInterval(setIntervalHandler);}},1000)};倒数();控制台打印是一样的:分析上面两种写法,第一种使用setTimeout方法,倒计时递归函数调用,第二种使用setInterval方法。setInterval方法以指定的时间间隔(以毫秒为单位)调用函数或计算表达式。setTimeout方法用于在指定的毫秒数后调用函数或计算表达式。这两个函数的用法相信大家都很熟悉,都可以实现倒计时功能,而且setInterval函数的周期调用特性更符合倒计时业务场景,但是真的是这样吗?setTimeout和setInterval所以问题是,我应该使用setTimeout还是setInterval,或者两者都使用?setInterval执行机制JavaScript高级程序设计(第三版)关于时间间隔说明:设置一个定时器在150ms后执行,并不是说代码在150ms后立即执行,是说代码会在150ms后加入队列。如果此时队列中没有其他东西,那么这段代码就会被执行。有了这个描述,我们设置执行代码setInterval(func,interval),func函数的执行时间为1s,interval时间间隔为0.5s,那么这段代码的执行流程图如下:在0s时,setInterval函数触发,等待0.5s后,func第一次加入事件队列,0.5-1.5s期间执行1s。因为时间间隔是0.5s,所以func在1s时第二次加入队列,但是此时JS引擎的处理方式是:在使用setInterval时,只有当没有其他定时器的代码实例时,才计时将设置的服务器代码添加到队列中。因为在1s时,第一次加入队列的func还在执行中,所以func无法成功加入队列,导致丢帧。又过了0.5s,在1.5s时,func第三次加入队列。此时第一次加入队列的func刚刚执行完毕,第三次加入队列func就可以成功开始执行了。这时候setInterval的另一个问题就暴露出来了。两次func执行的时间间隔远小于0.5s,代码执行间隔小于设定间隔。setTimeout执行机制那么同样的函数,使用setTimeout会发生什么,代码片段:setTimeout(function(){//dosomething//arguments.callee获取当前正在执行的函数的引用,该函数已经在ES5严格模式下被废弃.setTimeout(arguments.callee,interval);},interval)func函数的执行时间为1s,interval时间间隔为0.5s。代码执行流程图如下:0s时触发setTimeout函数,等待0.5s后,第一次将func加入事件队列,在0.5-1.5s期间执行1s。func执行到1.5s结束,触发第二个setTimeout函数。等待0.5s后,func第二次加入队列,在2s-2.5s期间执行1s。两次func执行的间隔与设置的间隔0.5s一致,不会出现丢帧现象。如何选择从setTimeout和setInterval这两个函数的执行机制来看,setInterval有两个问题:丢帧,如果JS队列中已经有它的实例,则不会再往队列中加入事件,所以这次执行的事件将丢失。两次事件执行的时间间隔变小甚至没有间隔。当前事件执行完后,队列中添加的事件会立即执行。所以,使用setTimeout而不是setInterval。Countdownerror倒数计时器出错。我们来做个测试,一目了然:varcountIndex=1;//倒计时任务执行次数consttimeout=1000;//时间间隔1秒conststartTime=newDate().getTime();countdown(timeout);functioncountdown(interval){setTimeout(function(){constendTime=newDate().getTime();//错误constdeviation=endTime-(startTime+countIndex*timeout);console.log('th'+countIndex+'时间:累计误差'+deviation+'ms');countIndex++;//执行下一次倒计时countdown(timeout);},interval)}consoleprint:这段代码的作用是计算每个定时器的结束时间和开始时间的差加上总的轮询时间,即累计误差。从控制台打印的信息可以看出,平均有2毫秒/秒的错误值。虽然每次误差值不大,但如果倒计时10分钟,最后会有1.2秒的差异,这在闪购的业务场景中是一个致命的bug。如果你把浏览器切换到Tab或者最小化一段时间,再切换回来打开控制台,你会看到神奇的一幕:第五次浏览器最小化打印,第十次浏览器恢复,你从第6次到第9次浏览器最小化期间??,每次偏差值约为1000ms,第11次浏览器恢复后,每次偏差值又变回2ms左右。惊不惊喜,意想不到!为什么会出现错误?2ms的错误是因为JS是单线程的。执行setTimeout中的代码块大约需要2ms。示例中的代码块在没有复杂逻辑的情况下耗时2ms。可想而知,在实际业务中肯定会用到。这将花费更长的时间,并且会与计时器执行的次数相加,从而导致更大的错误。浏览器最小化后每次1000ms的误差是浏览器性能优化的机制造成的。参考MDN中setTimeout的描述:Theminimumtimingdelayofinactivetabs>=1000ms为了优化后台tabs的加载损耗(同时降低功耗),timerininactivetabs的最小延时限制为1S(1000ms)).Firefox从5版本开始就采用了这种机制(参见bug633421,可以通过dom.min_background_timeout_value改变1000ms的间隔值。Chrome从11版本开始采用这种机制(crbug.com/66078)。Android版的Firefox没有activatebackgroundtabs使用了15分钟的最小延迟间隔,根本无法加载这些tabs如何解决报错倒数计时器的错误是不可避免的,但是我们可以通过报错值来调整每次执行的时间间隔:varcountIndex=1;//倒计时任务执行次数consttimeout=1000;//时间间隔1秒conststartTime=newDate().getTime();countdown(timeout);functioncountdown(interval){setTimeout(function(){constendTime=newDate().getTime();//错误constdeviation=endTime-(startTime+countIndex*timeout);countIndex++;//执行下一次倒计时,去除错误countdown(timeout)的影响-deviation);},interval)}执行下一个countdown去除错误countdown(timeout-deviation)的影响,这里我们调整下一个任务的调用时间,之前延迟多少毫秒,我们下一个任务会执行得更快多少毫秒,这是基本思路处理倒计时错误。另一种方案是通过获取后台服务器的时间来校准倒计时。获取本地时间实际上是不精确的。newDate()获取的时间为本地系统时间。用户可以通过调整系统时间来欺骗浏览器。因此,通过获取服务器时间来查看更可靠。对于Tab浏览器切换倒计时导致的较大错误,解决方法是切换回浏览器界面,通过监听页面可见或隐藏的visibilitychange事件获取最新的时间,让用户看到的是倒计时没有错误。document.addEventListener('visibilityChange',function(){if(!document.hidden){//获取最新时间}});你学会“浪费”了吗?文章首发于本人博客e??cheverra,原创文章,转载请注明出处。同时,欢迎关注我的微信公众号,我们一起学习进步!时不时会有资源和福利!
