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

先有问题再有答案

  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运行时
浏览器:帧&渲染流程
浏览器:帧&事件循环

相关推荐
Cool----代购系统API22 分钟前
css设置盒子动画,CSS3 transition动画 animation动画
前端·css·css3
哟哟耶耶32 分钟前
css-设置元素的溢出行为为可见overflow: visible;
前端·css
sunly_35 分钟前
CSS:跑马灯
前端·css
2301_8187320642 分钟前
用layui表单,前端页面的样式正常显示,但是表格内无数据显示(数据库连接和获取数据无问题)——已经解决
java·前端·javascript·前端框架·layui·intellij idea
yqcoder44 分钟前
npm link 作用
前端·npm·node.js
林涧泣1 小时前
【Uniapp-Vue3】页面和路由API-navigateTo及页面栈getCurrentPages
前端·vue.js·uni-app
Komorebi゛1 小时前
【uniapp】获取上传视频的md5,适用于APP和H5
前端·javascript·uni-app
林涧泣1 小时前
【Uniapp-Vue3】动态设置页面导航条的样式
前端·javascript·uni-app
杰九1 小时前
【全栈】SprintBoot+vue3迷你商城(10)
开发语言·前端·javascript·vue.js·spring boot
Hopebearer_2 小时前
入门 Canvas:Web 绘图的强大工具
前端·javascript·es6·canva可画