如何写好倒计时

引言

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

,原创文章,转载请注明出处。

如何写好倒计时

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

Original: https://www.cnblogs.com/echeverra/p/15055121.html
Author: echeverra
Title: 如何写好倒计时

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/582364/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Redis配置参数详解

    Redis是一个应用非常广泛的高性能Key-Value型数据库,与memcached类似,但功能更加强大!本文将按照不同功能模块的方式,依次对各个功能模块的配置参数进行详细介绍。 …

    Linux 2023年5月28日
    074
  • 教你搞懂Jenkins安装部署!

    前言:请各大网友尊重本人原创知识分享,谨记本人博客: 南国以南i Jenkins介绍 Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具,用于监控持续重复的工作…

    Linux 2023年5月27日
    0104
  • 普通 Docker 与 Kubernetes 对比

    Docker提供基本容器管理 API 和容器镜像文件格式Kubernetes 管理运行容器的(物理或虚拟)主机群集,如果 Docker 是 OCP 的”内核&#8221…

    Linux 2023年6月6日
    081
  • 操作系统虚拟内存发展史

    404. 抱歉,您访问的资源不存在。 可能是URL不正确,或者对应的内容已经被删除,或者处于隐私状态。 [En] It may be that the URL is incorre…

    Linux 2023年5月27日
    0101
  • 【镜像取证篇】DD系统镜像仿真问题的一些补充说明

    【镜像取证篇】DD系统镜像仿真问题的一些补充说明 ​ 系统千千万,环境占一半,遇到问题建议多重新挂载镜像,多尝试,站在岸上永远学不会游泳。—【蘇小沐】 【镜像取证篇】D…

    Linux 2023年6月13日
    0104
  • 幸运的袋子 附加动图演示!

    幸运的袋子_牛客题霸_牛客网 (nowcoder.com) 厄运的袋子 用到了深度遍历 递归回溯法 这里假设一个例子: 1 1 1 2 2 3 4 5 7 8 因为要确认是否辛运,…

    Linux 2023年6月13日
    083
  • 查询windows日志

    系统日志可以用来查看系统的一些信息,比如警告、错误、验证、开关机等。 打开系统日志 按下快捷键 win+R,输入 eventvwr.exe,并点击确定 查询开关机记录 点击左侧 W…

    Linux 2023年6月8日
    0100
  • SpringBoot——自定义Redis缓存Cache

    SpringBoot自带Cache存在问题: 1.生成Key过于简单,容易冲突 默认为cacheNames + ":" + Key2.无法设置过期时间,默认时间…

    Linux 2023年5月28日
    088
  • 配置管理docker对象和守护进程

    使用 Docker 的主要工作是创建和使用各类对象:镜像、容器、网络、卷等。 1、Docker对象的标记 标记(Label):是一种将元数据应用于Docker对象(镜像、容器、网络…

    Linux 2023年6月8日
    075
  • docker安装redis

    首先考虑需要安装的redis版本,我这里是安装的redis 6.0.16,如果宿主机没有,那么就docker pull redis:6.0.16 一、指定redis配置文件 我的宿…

    Linux 2023年5月28日
    091
  • Shell 实现多线程(多任务)

    1.命令结尾添加:& 在命令的末尾加 & 符号,则命令将在后台执行,这样后面的命令不需要等待该命令执行完再开始执行。 2.解决主线程提前退出问题,添加 wait 3…

    Linux 2023年5月28日
    081
  • 其他

    1、【剑指Offer学习】【面试题01:实现赋值运算符函数】 2、【剑指Offer学习】【面试题02:实现Singleton 模式——七种实现方式】 5、【剑指Offer学习】【面…

    Linux 2023年6月13日
    075
  • 【凸优化】3 多面体,单纯形,半正定锥

    1 多面体 Polyhedra 定义:多面体为一系列的(有限个)线性等式和不等式的解集: [\mathcal{P}={x|a_j^T x \leq b_j, j=1,……

    Linux 2023年6月7日
    089
  • 初识前后端

    初识前后端 在学习了解前后端的过程中,自己看到了这一篇好的文章,摘下了一些当下用的的内容,供复习参考。 什么是前端开发? 前端开发主要涉及网站和 App,用户能够从 App 屏幕或…

    Linux 2023年6月13日
    0108
  • 大天使之剑H5游戏超详细图文架设教程

    引言 想体验传奇游戏霸服的快乐吗?想体验满级VIP的尊贵吗?想体验一刀99999的爽快吗?各种极品装备、翅膀、宠物通通给你,就在大天使之剑! 本文讲解大天使之剑H5游戏的架设教程,…

    Linux 2023年6月7日
    0100
  • 让滚动条自动滚动到最底部(可用)

    代码: <body> </body> 需求: main高度不断增加,保持页面滚动条始终在底部 解决方案: $(function(){ ….//main&…

    Linux 2023年6月13日
    0102
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球