时间系列三:实现毫秒级倒计时

先有问题再有答案

  1. 倒计时和计时器两者有什么区别?
  2. 这些区别在技术实现上有什么影响?
  3. 如何实现一个毫秒级倒计时?
  4. 毫秒时倒计时需要每毫秒刷新一次UI嘛?
  5. 性能上有哪些注意事项?
  6. 如果用户手动更改了设备时间 如何保证倒计时的准确性?

倒计时&计时器

倒计时(Countdown)

倒计时有一个明确的结束点,即当时间达到零的时候。因此,倒计时器需要持续更新当前剩余时间,并确保在实际时间到达目标点时准确显示零,并触发相应的事件。比如新年倒计时、比赛开始前的倒计时等。

计时器 (Timer)

计时器则是从现在的时间点开始,向未来计时。在设计上应能够一直计时,只要记得累计经过的时间,因此理论上它可以无限增长,它用于测量或记录某个过程的持续时间,比如在线测试的时间等。

区别

技术实现上,倒计时需要从目标时间点减去当前时间来计算剩余时间,

而计时器则是从某个起始点开始累加时间。

计时器需要考虑多种因素可能导致的计时精度问题。

计时器不准确的原因

  1. 可能会受到其他JS代码的影响。JavaScript是单线程运行的,也就是说在同一时间,只能有一个任务在运行。js的计时器是在设置时间后 加入到任务队列中 等待执行,只有将当前的任务队列清空才会执行到这个计时器。如果前面的代码执行时间过长,就会导致后续的计时器任务无法准时执行。例如,假设我们设置了一个定时器每隔1秒执行一次,但是在执行过程中任务队列中有一个操作耗时3秒,那么定时器就会在3秒后才能执行下一次,导致计时不准确。

  2. 另一个原因是浏览器对于后台标签页的优化处理。当浏览器标签页不在前台时,为了节省CPU的计算资源,浏览器可能会暂停或降低背景任务的频率。这同样可能导致计时器不准确。例如,当用户切换到其他标签页,原本每隔1秒执行一次的计时器可能被浏览器暂停,等到用户再次切换回来时,计时器才恢复执行。

详情参考 时间系列二:高精度计时器方案,worker计时,settimeout差值补偿,raf计时器,setInterval

而倒计时相比于计时器在精度上的实现并不需要那么复杂的考虑。

因为js长任务的运行并不会导致倒计时出现结果不准 毕竟卡顿时是整个页面的阻塞 页面都不刷新 倒计时不刷新也是正常的。当页面可以正常刷新时 我们依然可以获取到正确的结果。

性能&刷新频率

实现毫秒级计时器,我们肯定不会每毫秒刷新一次UI。我们知道 通常情况下,在一帧的时间内,浏览器会尽可能地执行JavaScript代码,浏览器会在一帧的结束时进行一次页面渲染。因为页面渲染(包括回流&重绘)是一个相对昂贵的操作,频繁地进行页面渲染会消耗大量的CPU和GPU资源,降低页面的性能。 这是得不偿失的。

实际上,大多数现代显示器的刷新率是60Hz,即每16.67毫秒刷新一次。因此,即使每毫秒计算时间,也只需每16.67毫秒左右更新一次UI,以匹配显示器的刷新率。

那么如何实现16毫秒刷新一次呢?使用setInterval实现16ms的间隔调用嘛?记住:永远不要使用setInterval,可以使用setTimeout模拟setInterval。至于这么说的原因在 时间系列二 里已经有过详细的说明了。

在这里我们使用setTimeout模拟依然是不准确的 最好的使用方式当然是可以保证每一帧刷新一次。

我们可以利用 浏览器提供的requestAnimationFrame api 和渲染帧率保持同步刷新

demo实现

javascript 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .countdown {
            width: 300px;
            height: 100px;
            padding-left: 10px;
            background-color: pink;
            color: blue;
            display: flex;
            align-items: center;
            font-size: 17px;
            font-weight: bold;
        }
    </style>
</head>

<body>
    <div id="countdown" class="countdown"></div>
    <script type="text/javascript">
        function startCountdown(targetTime, callback) {
            function updateCountdown() {
                const now = Date.now();
                const distance = targetTime - now;

                if (distance <= 0) {
                    callback({
                        days: 0,
                        hours: 0,
                        minutes: 0,
                        seconds: 0,
                        milliseconds: 0,
                        done: true
                    })
                    return;
                }

                const days = Math.floor(distance / (1000 * 60 * 60 * 24));
                const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
                const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
                const seconds = Math.floor((distance % (1000 * 60)) / 1000);
                const milliseconds = distance % 1000;


                callback({
                    days,
                    hours,
                    minutes,
                    seconds,
                    milliseconds,
                    done: false
                })

                // 使用requestAnimationFrame更新倒计时
                requestAnimationFrame(updateCountdown);
            }
            // 初次调用
            updateCountdown();
        }
        const countdownElement = document.getElementById('countdown');
        startCountdown(Date.now() + 3 * 24 * 60 * 60 * 1000, (params) => {
            const {
                days,
                hours,
                minutes,
                seconds,
                milliseconds,
                done
            } = params;
            const display = `${days}天 ${hours}小时 ${minutes}分钟 ${seconds}秒 ${milliseconds}毫秒`;

            countdownElement.innerHTML = display;
        });
    </script>
</body>

</html>

准确性

问题

在实现倒计时能力时 通过前端侧会从服务端获取下发的一个目标时间戳 前端通过本地时间戳与目标时间戳计算差值 每间隔一段时间更新一次UI 但是如果用户更改了本地时间会导致倒计时不准确 这个问题要如何解决?

解决

定期从服务器获取当前时间,并使用该时间校正当前的倒计时。

  1. 服务器时间作为可信来源:服务器时间被认为是可信且不会被用户所篡改。因此,通过与服务器时间的同步,可以避免因为用户手动修改本地时间导致的误差。
  2. 校准机制:通过比较获取到的服务器时间和本地时间,计算出一个时间偏移值(offset),这个偏移值可以校准任何由本地时间变化带来的误差。
  3. 增量调整:通过定期获取服务器时间,可以不断调整本地时间的偏移值,从而确保倒计时始终基于一个准确的时间源。

代码示例

同步服务端时间 计算出差值

ini 复制代码
 let serverTimeOffset = 0;
        function getServerTime() {
            fetch(' https://yourserver.com/get-time')
                .then(response => response.json())
                .then(data => {
                    serverTimeOffset = data.serverTime - Date.now(); // 计算出差值
                })
        }

传入差值 纠正本地时间

javascript 复制代码
        function startCountdown(targetTime, callback, serverTimeOffset = 0) {
            function updateCountdown() {
                const now = Date.now() + serverTimeOffset;
                const distance = targetTime - now; 
                // 。。。。。。
        }
      }
javascript 复制代码
setInterval(getServerTime, 10 * 1000);  // 每10s同步一次

系列文章

时间系列一:认识js世界的日期与时间
时间系列二:高精度计时器方案,worker计时,settimeout差值补偿,raf计时器,setInterval
浏览器: 深入理解requestAnimationFrame优化js运行时
浏览器:帧&渲染流程
浏览器:帧&事件循环

相关推荐
吃杠碰小鸡4 分钟前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone9 分钟前
C#使用Aspose.Words把 word转成图片
前端·c#·word
Serene_Dream28 分钟前
JVM 并发 GC - 三色标记
jvm·面试
xjt_090129 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农40 分钟前
Vue 2.3
前端·javascript·vue.js
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js