在 JavaScript 中,计时器是经常用到的,但是我们在使用计时器时如果不小心就会踩到坑中
说到计时器肯定时离不开 setTimeout 和 setInertval 这两个函数的,这两个函数的用法基本一致,在使用时一般都要传入两个参数
第一个参数是一个回调函数,用于倒计时结束时调用;第二个参数是倒计时的时间 delay,第二个参数不写的话默认为 0。这两个函数的区别在于 setTimeout 只倒计时一次,setInterval 会每隔 delay 这段时间就执行回调函数一次
接下来说说计时器里面的一些坑
1.第一个坑
由于 JavaScript 是一门单线程的语言,它没办法同时执行多个任务,对于倒计时这种事件,JavaScript 解释器中有一个用来存放回调函数的队列,只有解释器中的同步事件都执行完了之后才会去队列里面将回调函数取出来执行
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
console.log(3);
比如上面的这段代码,可以看到打印出来的结果是 1 3 2
这是因为 JS 解释器会先执行同步的代码,将 1 和 3 打印出来,然后再执行 setTimeout 中的回调函数,将 2 打印出来。所以虽然 setTimeout 中的延时为 0,但是还是在 console.log(3)
这句代码执行完成之后才执行
对于 setTimeout 延时为 0 时的情况我们可以将它理解为尽快执行 setTimeout 中的代码(和立即执行有所区别)
2.第二个坑
如果你给某个东西(通常是按钮之类的)加上了触发定时器的事件,那么要记得每次在使用定时器之前要把之前的定时器清除掉,要不然你的定时器的速度会越来越快,这是因为你没有清理掉之前的定时器,所以当你不断点击按钮时,定时器不断的累加起来,这样每个定时器的间隔就越来越小,导致你的倒计时越来越快
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btn">开始计时</button>
<div id="num">0</div>
</body>
<script>
let num = 0;
document.getElementById("btn").addEventListener("click", () => {
setInterval(() => {
num++;
document.getElementById('num').innerText = num;
}, 1000);
})
</script>
</html>
在上面的这段代码中,当我们点击“开始计时”按钮之后,页面中的计时就启动了,但是如果你多次点击它的话,你会发现计时会越来越快
要解决这个问题就需要我们在开启一个新的计时器开启之前将已经开启了的计时器清除掉
let num = 0;
let timerId;
document.getElementById("btn").addEventListener("click", () => {
clearInterval(timerId);
timerId = setInterval(() => {
hasTimer = true;
num++;
document.getElementById('num').innerText = num;
}, 1000);
})
如上面代码所示,我们将开启新的计时器前的计时器清除掉,这样当用户重复点击按钮时,就不会出现速度越来越快的问题
3.第三个坑
我们读取的时间不能从用户的客户端读取,而是应该从服务端那边获取。如果从客户端直接读取的话,不同用户的客户端之间的时间可能存在误差,同时可能存在用户直接修改客户端的时间来改变你的倒计时
所以做倒计时相关的功能的时候我们的倒计时所开始的时间需要从服务端那边获取,但是从服务端那边获取到时间到倒计时显示在页面是有一定的时间差的
客户端从服务端获取到客户端将倒计时显示出来的过程大概如下
客户端发送 HTTP 请求给服务端 -> 服务响应并将时间发送给客户端 -> 客户端根据这个时间将倒计时显示在页面上
在这个过程中,是有一定的时间损耗的,比如数据请求和响应的过程,以及另外一个对倒计时影响更大的东西———— JavaScript 解释器会先执行同步的代码,执行完同步的代码之后再执行异步的代码
如果同步代码的执行时间太长的话,倒计时的精准度就下降了很多,
let start = new Date().getTime();
let count = 0;
// 模拟执行大量代码
setInterval(function(){
var i = 0;
while(i++ < 100000000);
}, 0);
// 倒计时
setInterval(function(){
count++;
console.log(new Date().getTime() - (start + count * 1000));
},1000);
上面的这段代码的执行效果如下图所示,可以看到如果有大量的代码执行,那么倒计时的精准度就会下降很多,所以我们需要找一个方法来解决这个问题
要解决倒计时精准度的问题,我们可以将当前的时间与上一次倒计时开始的时间的差值与 delay 做对比,将 delay 减去这个差值从而得到一个校准后的时间,将这个时间做为下一次倒计时的时间,代码如下面所示
// 模拟执行大量代码
setInterval(() => {
let i = 0;
while(i++ < 100000000);
}, 0);
// 倒计时
let delay = 1000, count = 0, startTime = new Date().getTime(),
ms = 100000 // 要倒计时的时间,一百秒
if( ms >= 0){
let timeCounter = setTimeout(countDown, delay);
}
function countDown(){
count++;
let offset = new Date().getTime() - (startTime + count * delay);
let nextTime = delay - offset;
if (nextTime < 0) { nextTime = 0 };
ms -= delay;
console.log("误差:" + offset + "ms,下一次执行:" + nextTime + "ms后,离活动开始还有:" + ms + "ms");
if(ms < 0){
clearTimeout(timeCounter);
}else{
timeCounter = setTimeout(countDown,nextTime);
}
}
通过上一次倒计时开始的时间和当前时间的对比,我们可以将当前应该倒计时的时间算出来,这样就可以使我们的倒计时尽可能精准