JavaScript实现精准倒计时
当需要实现精准倒计时,但setTimerout和setInterval并不理想时,或许你可以看看这篇文章!
「精准」倒计时
需求
- 购物网站需要秒杀倒计时
- 抢购倒计时
- 倒计时抢红包
有
精确
计时的需求
JavaScript实现技能功能自然会想到以下两个方法
1 | setTimeout() |
存在问题
用以上方法实现后发现,两个手机打开同一个活动页面后倒计时的时间不同
,可能存在几秒甚至几分钟的误差。
前几天在抢薛之谦演唱会的票时就发现和朋友的时间不一致(可能这就是导致我没抢到票的原因吧哈哈哈)
可能原因
1.读取的是客户端时间,客户端时间可以随时调整,所以与服务器时间可能不一致。
- 运行时间长了,也会造成不一致
1 | // 以下代码并不一定在1s后执行 |
1 | // 例1 |
1 | // 例2 |
- 预期的输出都是0,因为每一秒触发一次,每次+1s
- 如果再加上一个定时器执行其他任务的话,这个就会更不准
timeout并不一定在1s后输出
- JS是单线程的
- 只有主线程的任务执行完后才会去轮询任务队列
- 主线程 > 微任务 > 宏任务
- setTimeout属于宏任务
- 只有for执行完主线程没有其他任务时才会去轮询任务队列,任务队列还是遵循先进先出的。
没有考虑js冻结运行耗费时间;(特别是移动端容易出现,下滑页面时倒计时不动了)
没有考虑页面渲染和函数运行累积时间;
其他代码逻辑问题;
计时器原理
参考前端界大牛John.Resig (jQuery作者) 写的Javascript计时器工作原理 — 《How JavaScript Timers Work》。
JavaScript是单线程的,必须按队列先后顺序执行
- 随着多核CPU的出现,为了充分发挥计算机算力,出现了
Web Worker
,为JavaScript创造多线程环境,运行主线程创建Worker线程,将一些任务分给Woker线程运行,两个线程互不干扰,Worker线程执行完后将结果返回给主线程- 参考
下图需要垂直往下看
- 左边10、20、30、40、50是时间轴,单位为ms
- 右边Timer是setTimeout、Interval是setInterval 前面的毫秒数是设置的时间
- Fires为触发
- 蓝色块为执行该代码块需要的时间
- 第一个代码块约执行18ms、第二个代码块约执行11ms、….
- js是单线程,同一时间只能执行一个js代码
- 当异步事件发生后,回放入执行队列,线程空闲后才会依次执行该队列
第一个模块初始化了两个定时器,第一个定时器在10ms后触发,此时第一个模块还没执行完成,所以该定时器并不会立即执行,会将其回调函数放入执行队列等待执行
因为Mouse Click CallBack比Timer先触发,所以会先执行Mouse Click CallBack的回调函数在执行Timer(如图)
第二次触发Interval的时候,第一次触发的Interval并未执行,此时并不管,直接把回调加入队列,所以导致下面两个Interval执行中间没有时间间隔
解决思路
客户端http请求服务器时间;
服务器响应完成;
服务器通过网络传输时间数据到客户端;
客户端根据活动开始时间和服务器时间差做倒计时显示;
网络也有误差
当前服务器时间 = 服务器系统返回时间 + 网络传输时间 + 前端渲染时间 + 常量(可选)
计算误差,然后减去误差时间得到等待下次执行时间
- 例如每隔1s执行一次,阻塞了1.5s,所以误差有0.5s,就等待0.5s后触发
- 这个0.5s的误差可以计算出
1 | //进行线程占用 |
以上实现可能导致倒计时忽快忽慢
- 例如可以每次减少20ms,慢慢靠近真值,不让用户发觉倒计时变快了。