如何写好倒计时

2022年01月14日 阅读数:5
这篇文章主要向大家介绍如何写好倒计时,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

引言

本文讲解倒计时为何建议使用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 方法用于在指定的毫秒数后调用函数或计算表达式。dom

相信你们对这两个函数的用法都是比较了解的,均可以实现倒计时功能,且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,原创文章,转载请注明出处。
同时欢迎关注个人微信公众号,一块儿学习进步!不定时会有资源和福利相送哦!