写点什么

如何写好倒计时

用户头像
echeverra
关注
发布于: 23 小时前
如何写好倒计时

引言

本文讲解倒计时为什么建议使用setTimeout而不使用setInterval,倒计时为什么存在误差,以及如何解决。

倒计时器

在前端开发中,倒计时器功能比较常见,比如活动倒计时,假定只有 10 秒,比较常见的两种写法如下:


//setTimeout实现方式var countdownTime = 10; //倒计时秒数
var countdown = function() { var setTimeoutHandler = setTimeout(function () { countdownTime -- ; console.log('倒计时:' + countdownTime + ' 秒');
if(countdownTime === 0) { console.log('倒计时结束!'); clearTimeout(setTimeoutHandler); }else { countdown(); }
}, 1000)};
countdown();
复制代码


//setInterval实现方式var countdownTime = 10; //倒计时秒数
var countdown = function() { var setIntervalHandler = setInterval(function () { countdownTime -- ; console.log('倒计时:' + countdownTime + ' 秒');
if(countdownTime === 0) { console.log('倒计时结束!'); clearInterval(setIntervalHandler); }
}, 1000)};
countdown();
复制代码


控制台打印都是一样的:



分析上面的两种写法,第一种使用setTimeout方式,countdown递归函数调用,第二种使用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第 1 次加入到事件队列中,并在 0.5-1.5s 期间执行了 1s。


因为时间间隔为 0.5s,所以在 1s 时func第 2 次加入到队列中,但此时 JS 引擎处理方式是:当使用setInterval时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。因为在 1s 时,第 1 次加入队列的func还在执行,所以无法成功将func加入队列中,这就出现了丢帧现象。


时间又过了 0.5s,在 1.5s 时,func第 3 次加入到队列中,此时第 1 次加入到队列中func刚执行完毕,第 3 次func可成功加入到队列中并开始执行。此时暴露出setInterval另一个问题,两次func执行的时间间隔远小于 0.5s,代码的执行间隔比设定的间隔要小

setTimeout 执行机制

那么同样的功能,使用setTimeout又会是什么现象呢,代码片段:


setTimeout(function(){    //do something    //arguments.callee 获取对当前执行的函数的引用,在ES5严格模式中已废弃。    setTimeout(arguments.callee, interval);},interval)
复制代码


func函数执行时间为 1s,interval时间间隔为 0.5s,代码的执行流程图如下:



0s 时,setTimeout函数触发,等待 0.5s 后,func第 1 次加入到事件队列中,并在 0.5-1.5s 期间执行了 1s。


1.5s 时func执行结束,第二个setTimeout函数被触发,等待 0.5s 后,func 第 2 次加入到队列中,并在 2s - 2.5s 期间执行了 1s。


两次func执行间隔与设定的interval 0.5s 一致,且不会出现丢帧的现象。

如何选择

通过setTimeoutsetInterval两个函数的执行机制来看,setInterval存在两个问题:


  1. 丢帧,如果 JS 队列中已经有一个它的实例,就不会向队列中添加事件,所以这次的事件执行就会丢失。

  2. 两次的事件执行时间间隔变小甚至无间隔,当前事件执行完后,马上就会执行队列中已添加的事件。


所以,使用setTimeout,而不使用setInterval

倒计时误差

倒计时器是存在误差的,我们做个测试,一看便知:


var countIndex = 1; //倒计时任务执行次数const timeout = 1000; //时间间隔1秒const startTime = new Date().getTime();
countdown(timeout);
function countdown(interval) { setTimeout(function () { const endTime = new Date().getTime();
//误差 const deviation = endTime - (startTime + countIndex * timeout); console.log('第'+ countIndex +'次:累计误差 '+ deviation + ' ms');
countIndex ++ ;
//执行下一次倒计时 countdown(timeout); }, interval)}
复制代码


控制台打印:



这段代码的作用是,计算出每次定时器结束时间开始时间加上总轮询的时间的差值,也就是累计的误差。可以从控制台打印信息看出,平均每秒存在 2ms 的误差值。虽然每次误差值都不大,但是如果倒计时 10 分钟,最后就会差 1.2 秒,这在抢购秒杀的业务场景下是致命的 BUG 了。


如果你将浏览器切换 Tab 或者最小化一段时间后,再切回打开控制台看又会看到神奇的一幕:



打印第 5 次浏览器最小化,第 10 次时浏览器恢复,可以看到从第 6 次到第 9 次浏览器最小化期间,每次偏差值是 1000ms 左右,等第 11 次浏览器恢复后,每次偏差值又变回 2ms 左右。惊不惊喜,意不意外!

为什么会存在误差

存在 2ms 的误差是因为 JS 是单线程的,执行了setTimeout中的代码块耗时 2ms 左右,例子中的代码块没有复杂逻辑就花费了 2ms,可想而知在实际业务中肯定要消耗更长时间,而且会随着计时器执行次数叠加,造成更大的误差。


而浏览器最小化后每次 1000ms 的误差是因为浏览器性能优化的一种机制。参考MDN中关于setTimeout的一段描述:


未被激活的 tabs 的定时最小延迟>=1000ms

为了优化后台 tab 的加载损耗(以及降低耗电量),在未被激活的 tab 中定时器的最小延时限制为 1S(1000ms)。

Firefox 从 version 5 (see bug 633421开始采取这种机制,1000ms 的间隔值可以通过 dom.min_background_timeout_value 改变。Chrome 从 version 11 (crbug.com/66078)开始采用。Android 版的 Firefox 对未被激活的后台 tabs 的使用了 15min 的最小延迟间隔时间 ,并且这些 tabs 也能完全不被加载。

如何解决误差

倒计时器的误差是不可避免的,但是我们可以通过误差值去调整每次执行的时间间隔:


var countIndex = 1; //倒计时任务执行次数const timeout = 1000; //时间间隔1秒const startTime = new Date().getTime();
countdown(timeout);
function countdown(interval) { setTimeout(function () { const endTime = new Date().getTime();
//误差 const deviation = endTime - (startTime + countIndex * timeout); countIndex ++ ;
//执行下一次倒计时,去除误差的影响 countdown(timeout - deviation); }, interval)}
复制代码


执行下一次倒计时,去除误差的影响countdown(timeout - deviation),这里我们通过对下一次任务的调用时间做了调整,前面延迟了多少毫秒,那么我们下一个任务执行就加快多少毫秒,这就是处理倒计时误差的基本思路。


还有一种解决办法就是通过获取后台服务器的时间去校准倒计时,获取本地时间实际上是不严谨的,new Date()获取到的时间是本机系统的时间,用户可以通过调整系统时间欺骗浏览器。所以通过获取服务器时间校对是比较靠谱的一种做法。



对于切换 Tab 浏览器倒计时器产生的大误差,解决思路是切回浏览器界面后,通过监听页面可见或被隐藏visibilitychange事件,获取最新的时间,这样用户看到的就是没有误差的倒计时了。


document.addEventListener('visibilityChange', function() {    if (!document.hidden) {      // get newest time    }});
复制代码


你学“废”了么?



文章首发于我的博客 https://echeverra.cn/countdown,原创文章,转载请注明出处。


欢迎关注我的微信公众号 echeverra,一起学习进步!不定时会有资源和福利相送哦!



发布于: 23 小时前阅读数: 7
用户头像

echeverra

关注

Let`s go, together. 2021.09.18 加入

个人博客:https://echeverra.cn 微信公众号:echeverra

评论

发布
暂无评论
如何写好倒计时