先有问题再有答案
倒计时和计时器两者有什么区别?
这些区别在技术实现上有什么影响?
如何实现一个毫秒级倒计时?
毫秒时倒计时需要每毫秒刷新一次UI嘛?
性能上有哪些注意事项?
如果用户手动更改了设备时间 如何保证倒计时的准确性?
倒计时&计时器
倒计时(Countdown)
倒计时有一个明确的结束点
,即当时间达到零的时候。因此,倒计时器需要持续更新当前剩余时间,并确保在实际时间到达目标点时准确显示零,并触发相应的事件。比如新年倒计时、比赛开始前的倒计时等。
计时器 (Timer)
计时器则是从现在的时间点开始,向未来计时。在设计上应能够一直计时,只要记得累计经过的时间,因此理论上它可以无限增长,它用于测量或记录某个过程的持续时间,比如在线测试的时间等。
区别
技术实现上,倒计时需要从目标时间点减去当前时间来计算剩余时间,
而计时器则是从某个起始点开始累加时间。
计时器需要考虑多种因素可能导致的计时精度问题。
计时器不准确的原因
-
可能会受到其他
JS代码的影响
。JavaScript是单线程运行的,也就是说在同一时间,只能有一个任务在运行。js的计时器是在设置时间后 加入到任务队列中 等待执行,只有将当前的任务队列清空才会执行到这个计时器。如果前面的代码执行时间过长,就会导致后续的计时器任务无法准时执行。例如,假设我们设置了一个定时器每隔1秒执行一次,但是在执行过程中任务队列中有一个操作耗时3秒,那么定时器就会在3秒后才能执行下一次,导致计时不准确。 -
另一个原因是
浏览器对于后台标签页的优化
处理。当浏览器标签页不在前台时,为了节省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 但是如果用户更改了本地时间会导致倒计时不准确 这个问题要如何解决?
解决
定期从服务器获取当前时间,并使用该时间校正当前的倒计时。
- 服务器时间作为可信来源:服务器时间被认为是可信且不会被用户所篡改。因此,通过与服务器时间的同步,可以避免因为用户手动修改本地时间导致的误差。
- 校准机制:通过比较获取到的服务器时间和本地时间,计算出一个时间偏移值(offset),这个偏移值可以校准任何由本地时间变化带来的误差。
- 增量调整:通过定期获取服务器时间,可以不断调整本地时间的偏移值,从而确保倒计时始终基于一个准确的时间源。
代码示例
同步服务端时间 计算出差值
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运行时
浏览器:帧&渲染流程
浏览器:帧&事件循环