乐趣区

关于javascript:如何写好倒计时

引言

本文解说倒计时为什么倡议应用 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,原创文章,转载请注明出处。

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


退出移动版