背景

前端页面倒计时功能在很多场景中会用到,如运营活动开始倒计时和活动结束倒计时,又如购物网站的秒杀倒计时,抢购倒计时,还有我们手Q春节抢红包倒计时等等……. 最近的话费代付项目中,也涉及倒计时功能,但在开发过程中遇到一些麻烦和坑点,下面和大家分享一下最后是如何解决的。

坑点

手Q春节抢明星红包活动,就有产品吐槽两个手机在不同时间点打开同一个活动显示的开抢倒计时不一样,误差大的甚至相差几分钟,导致某些用户在活动显示还未开始红包就已被抢完了。为什么误差会这么大呢?

对于这个问题,前台开发同学一般会猜测是这个原因:倒计时读取了客户端时间造成的,因为客户端时间和服务端时间有误差,应该读取服务器时间。

是的,倒计时不应该读取客户端时间,客户端时间用户可以随时调整,会造成不一致,应读取服务器返回时间。但实践证明,做了这一步还未够,页面运行时间长了,新开的页面和原打开页面还是存在误差。

京东团购也存在这个问题:

造成误差的原因主要有几种可能:

1. 没有考虑js冻结运行耗费时间;(特别是移动端容易出现,下滑页面时倒计时不动了)

2. 没有考虑页面渲染和函数运行累积时间;(京东的误差貌似属于这种)

3. 其他代码逻辑问题(这种情况就复杂了,这里不讨论);

计时器原理

倒计时功能离不开setTimeout或setInterval这两个函数,要用好这两个函数必先了解好Javascript解释器的工作原理,前端界大牛John.Resig (jQuery作者) 有篇文章很好讲解了Javascript解释器工作原理 — 《How JavaScript Timers Work》。

前端开发同学都知道,javascript是单线程的(web worker除外),更好理解的解释是javascript解释器是单线程工作,它不能在处理一个ajax的callback的同时去处理click event的callback,而是必须按照先后队列顺序执行。

这图包含信息量很大,这里按照自己的理解描述一下:

这图从上往下看,垂直方向是时间,以ms为单位,蓝色模块是执行代码所占的时间段,如第一个代码模块执行js占用了约18ms, 第二个模块执行js占用了约11ms,其他模块类似。由于js是单线程执行,同一时间只能执行一个js代码(同一时间其他异步事件执行会被阻塞 ) , 当异步事件发生时,它会进入代码执行队列,执行线程空闲时依照队列顺序依次执行代码。

第一个模块初始化了两个定时器,一个10ms延迟的setTimeout和10ms的setInterval。这些定时器可能会在我们第一个代码块执行结束之前就触发,这取决于定时器在第一个代码块中启动的位置和时间。注意,定时器虽然触发了,但是并不会立即执行,它只是把需要延迟执行的函数按时间先后加入了执行队列,在线程的某一个空闲的时间点,这个函数就能够得到执行。

按照第一个模块事件触发的顺序(Mouse Click Occurs -. 10ms Timer Fires),第一个模块代码执行结束后,按照队列中等待的先后顺序执行事件,先执行Mouse Click CallBack再执行Timer。在执行Mouse Click CallBack模块时,Interval第一次触发未执行加入队列。在执行Timer模块时,Interval第二次触发未执行加入队列。待Mouse Click CallBack和Timer模块都执行完毕后,再依次执行队列中已触发的Interval事件。后面模块由于没有阻塞的事件了,所以按照既定10ms执行Interval事件。

倒计时问题

如果上面Javascript计时器原理理解了,就很好明白倒计时功能存在问题的隐患。

先看一段测试代码:

1
2
3
4
5
6
7
8

var start = new Date().getTime();
var count = 0;
//定时器测试
setInterval(function(){
     count++;
     console.log( new Date().getTime() - (start + count * 1000));
},1000);

目测代码就知道运行结果,定时器每秒执行一次,每次输出应该是0 。

实际输出:

结论:由于代码执行占用时间和其他事件阻塞原因,导致有些事件执行延迟了几ms,但影响很微。

下面加一段阻塞代码看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

var start = new Date().getTime();
var count = 0;
//占用线程事件
setInterval(function(){
     var j = 0;
     while(j++ < 100000000);
}, 0);
//定时器测试
setInterval(function(){
     count++;
     console.log( new Date().getTime() - (start + count * 1000));
},1000);

实际输出:

结论:由于加了很占线程的阻塞事件,导致定时器事件每次执行延迟越来越严重。

由于实际项目中,执行计时器的同时,会有很多其他异步阻塞事件,会导致倒计时功能不精确。

解决思路

这里先分析一下从获取服务器时间到前端显示倒计时的过程:

1. 客户端http请求服务器时间;

2. 服务器响应完成;

3. 服务器通过网络传输时间数据到客户端;

4. 客户端根据活动开始时间和服务器时间差做倒计时显示;

服务器响应完成的时间其实就是服务器时间,但经过网络传输这一步,就会产生误差了,误差大小视网络环境而异,这部分时间前端也没有什么好办法计算出来,一般是几十ms以内,大的可能有几百ms。

可以得出:当前服务器时间 = 服务器系统返回时间 + 网络传输时间 + 前端渲染时间 + 常量(可选),这里重点是说要考虑前端渲染的时间,避免不同浏览器渲染快慢差异造成明显的时间不同步,这是第一点。(网络传输时间忽略或加个常量呗)

获得服务器时间后,前端进入倒计时计算和计时器显示,这步就要考虑js代码冻结和线程阻塞造成计时器延时问题了,我的思路是通过引入计数器,判断计时器延迟执行的时间来调整,尽量让误差缩小,不同浏览器不同时间段打开页面倒计时误差可控制在1s以内。

关键实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

//继续线程占用
setInterval(function(){
     var j = 0;
     while(j++ < 100000000);
}, 0);
//倒计时
var  interval = 1000,
       ms = 50000,  //从服务器和活动开始时间计算出的时间差,这里测试用50000ms
       count = 0,
       startTime = new Date().getTime();
if( ms >= 0){
       var timeCounter = setTimeout(countDownStart,interval);                  
}
function countDownStart(){
       count++;
       var offset = new Date().getTime() - (startTime + count * interval);
       var nextTime = interval - offset;
       var daytohour = 0;
       if (nextTime < 0) { nextTime = 0 };
       ms -= interval;
       console.log("误差:" + offset + "ms,下一次执行:" + nextTime + "ms后,离活动开始还有:" + ms + "ms");
       if(ms < 0){
              clearTimeout(timeCounter);
       }else{
              timeCounter = setTimeout(countDownStart,nextTime);
       }
}

运行结果:

结论:由于线程阻塞延迟问题,做了setTimeout执行时间的误差修正,保证setTimeout执行时间一致。若冻结时间特别长的,还要做特殊处理。

转载:https://www.xuanfengge.com/js-realizes-precise-countdown.html

参考:https://blog.csdn.net/hhy_9288/article/details/79455390

js 活动倒计时详解相关推荐

  1. JS window对象详解

    JS window对象详解 1.window对象 2.窗口操作 打开窗口 关闭窗口 3.对话框 confirm prompt 4.定时器 setTimeout和clearTimeout setInte ...

  2. js_long.php,protobuf.js 与 Long.js的使用详解

    这次给大家带来protobuf.js 与 Long.js的使用详解,是急用protobuf.js 与 Long.js的注意事项有哪些,下面就是实战案例,一起来看一下. protobuf.js的结构和w ...

  3. 《Node.js开发实战详解》学习笔记

    <Node.js开发实战详解>学习笔记 --持续更新中 一.NodeJS设计模式 1 . 单例模式 顾名思义,单例就是保证一个类只有一个实例,实现的方法是,先判断实例是否存在,如果存在则直 ...

  4. highlight.js css,JS库之Highlight.js的用法详解

    下载到本地后,新建个页面测试 1.在head中加入css和js的引用 highlight hljs.initHighlightingOnLoad(); 2.添加对应要显示的内容 # 读取文件内容 de ...

  5. js排序算法详解-归并排序

    js系列教程5-数据结构和算法全解 js排序算法详解-归并排序 归并排序其实可以类比二分法,二分法其实就是二等分的意思,简而言之就是不断和新序列的中间值进行比较.归并排序似乎有异曲同工之妙,什么意思呢 ...

  6. js排序算法详解-基数排序

    全栈工程师开发手册 (作者:栾鹏) js系列教程5-数据结构和算法全解 js排序算法详解-基数排序 其实基数排序和桶排序挺类似的,都是找一个容器把属于同一类的元素装起来,然后进行排序.可以把基数排序类 ...

  7. js排序算法详解-桶排序

    全栈工程师开发手册 (作者:栾鹏) js系列教程5-数据结构和算法全解 js排序算法详解-桶排序 一看到这个名字就会觉得奇特,几个意思,我排序还要再准备几个桶不成?还真别说,想用桶排序还得真准备几个桶 ...

  8. js排序算法详解-计数排序

    全栈工程师开发手册 (作者:栾鹏) js系列教程5-数据结构和算法全解 js排序算法详解-计数排序 计数排序就是遍历数组记录数组下的元素出现过多次,然后把这个元素找个位置先安置下来,简单点说就是以原数 ...

  9. js排序算法详解-堆排序

    全栈工程师开发手册 (作者:栾鹏) js系列教程5-数据结构和算法全解 js排序算法详解-堆排序 这种排序方式呢,理论性太强,看动图的时候满脸写着懵逼,多看几遍似乎明白了编者的意图,但是要把这种理论的 ...

最新文章

  1. 十种经典排序算法精粹(c语言版本)
  2. 适合新手练手,用Python爬取OPGG里英雄联盟英雄胜率及选取率,详细讲解加注释(建议收藏练手)
  3. [C++11]弱引用智能指针weak_ptr初始化和相关的操作函数
  4. mysql客户端安装错误_windows下mysql 5.7以上版本安装及遇到的问题
  5. oracle数据库,增加序列,自增序列,规定位数,不足用0补足
  6. linux多线程九宫格,项目实战:Qt九宫格图片资源浏览器(支持window、linux、兼容各国产系统,支持子文件夹,多选,全选,图片预览,行数与列数设置等)...
  7. python 命名空间冲突_python-命名空间
  8. 在MacOS Big Sur中使用Safari 翻译功能的方法
  9. 中缀表达式转前缀表达式
  10. 如何制作媲美memz的炫酷特效恶搞程序
  11. 镇海计算机信息编程,镇海区加工中心编程培训
  12. java线程之读写锁
  13. SAP 库存报表查询
  14. MAC OS 虚拟机里的control键设置
  15. html5 video标签播放视频流解决方案
  16. 想要进行gene prioritization分析,请看这里!
  17. ES快速入门(七) ElasticSearch7.X分布式部署
  18. 2019汤家凤考研数学资料【超全】
  19. Git简单通俗易懂教程(包含常见问题与实战运用)
  20. 【上海交大oj】畅畅的牙签袋(改)(枚举+模拟)

热门文章

  1. Markdown基操
  2. Taq DNA聚合酶的种类与应用现状
  3. MySQL 中 delete where in 语句的子查询限制
  4. Centos yum和pip下载离线安装包
  5. C语言为何不会过时?你需要掌握多少种语言?
  6. java for循环 等待_在forEach循环中使用异步/等待
  7. iphone 通知声音_如何在iPhone上掌握通知
  8. 无限超越超级机器人nds_无限边境超级机器人大战OG传说超越隐藏宝箱
  9. 快速删除node_modules(rimraf)
  10. phpmywind教程:单页信息调用说明【进阶篇一】