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

先有问题再有答案

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

相关推荐
黑客老陈12 分钟前
新手小白如何挖掘cnvd通用漏洞之存储xss漏洞(利用xss钓鱼)
运维·服务器·前端·网络·安全·web3·xss
正小安17 分钟前
Vite系列课程 | 11. Vite 配置文件中 CSS 配置(Modules 模块化篇)
前端·vite
暴富的Tdy1 小时前
【CryptoJS库AES加密】
前端·javascript·vue.js
neeef_se1 小时前
Vue中使用a标签下载静态资源文件(比如excel、pdf等),纯前端操作
前端·vue.js·excel
m0_748235611 小时前
web 渗透学习指南——初学者防入狱篇
前端
℘团子এ1 小时前
js和html中,将Excel文件渲染在页面上
javascript·html·excel
z千鑫1 小时前
【前端】入门指南:Vue中使用Node.js进行数据库CRUD操作的详细步骤
前端·vue.js·node.js
m0_748250742 小时前
Web入门常用标签、属性、属性值
前端
m0_748230442 小时前
SSE(Server-Sent Events)返回n ,前端接收数据时被错误的截断【如何避免SSE消息中的换行符或回车符被解释为事件消息的结束】
前端