如何写好倒计时

引言

本文讲解倒计时为什么建议使用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
     }
 });

你学“废”了么?


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

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


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容