场景
计时方法
- 前端可以用来计时的方法:setTimeout/setInterval/requsetAnimationFrame
最直接的实现
let interval
let count = 30
interval = setInterval(()=>{
count--
console.log(`${count}s`)
if(count==0){
clearInterval(interval)
}
},1000)
- 加上时间戳看一下结果

- 加上同步阻塞看一下时间戳结果

- 因为同步代码的阻塞导致setInterval的回调函数执行时间整体后延,如果同步计算时间较长,或dom重绘次数过多等,则用户需要等待的验证码发送时间将会远大于30s.
优化-requsestAnimationFrame代替setInterval
let animationFrame
let count = 30
let paintTimes = 0
requestAnimationFrame(function time(){
paintTimes++
if(paintTimes==60){
paintTimes = 0
count--
if(count<0){
window.cancelAnimationFrame(animationFrame)
}else{
console.log(`${count}s---执行时间戳${new Date().getTime()}`)
}
}
//一定不能放到if-else条件判断里
animationFrame = window.requestAnimationFrame(time)
})
- 加上同步阻塞看一下时间戳结果

- rAF并不能改变代码执行被同步代码阻塞的现象,rAF的回调函数最终也是要主线程执行,主线程阻塞的情况,rAF与setInterval(()=>{},1000/60)的效果差不多。
- rAF的优势在于重绘和回流的时机紧跟浏览器的刷新频率,这是setInterval设置16.7ms的时间间隔也不能做到的。
优化-setTimeout模拟setInterval
let timeout;
let count = 0;
let startTime = new Date().getTime();
const countTime = function(){
count++;
let target = startTime+count*1000;
let now = new Date().getTime();
let offset = target-now<0?0:target-now;
if(count>30){
clearTimeout(timeout);
}else{
setTimeout(()=>{
console.log(`${30-count}s---执行时间戳${new Date().getTime()}`)
countTime();
},offset)
}
}
- 加上同步阻塞看一下时间戳结果

- 执行11次之后,因为同步阻塞延迟的执行时间得到校正,最后用户的等待时间基本在30s左右。
- setTimeout每次回调之前会对用户现在的时间和正确无偏差计时的时间进行一次比较,如果用户现在的时间超过理论上的时间,则立即执行回调函数或跳过此次函数执行。
优化-与系统时间同步跳秒
let timeout
let times = 0
let startTime = new Date().getTime()
let firstTime = 1000-startTime%1000
setTimeout(()=>{{
startTime = startTime+firstTime
//这里没有进行count++
countTime()
},firstTime)
const countTime = function(){
times++
let target = startTime+times*1000
let now = new Date().getTime()
let offset = target-now<0?0:target-now
if(count>30){
clearTimeout(timeout)
}else{
setTimeout(()=>{
console.log(`${30-count}s---执行时间戳${new Date().getTime()}`)
countTime()
},offset)
}
}
- 加上长时间的同步组塞看一下时间戳

- 除阻塞校正时间外,基本可以保证与系统时间同步跳秒
其他
- 时区问题:new Date()在不同时区返回的毫秒数相同,但显示的时间会随时区变化。
- 后台运行问题:浏览器后台运行时,计时器可能暂停或变慢。需在切换回来时及时进行计时器校正,web端可用visibilityChange事件监听。
总结
function countTimer(finalTime, interval, exactly, callback, timeoutCallBack) {
let startTime = new Date().getTime()
let firstTime = interval < 1000 ? interval - startTime % interval : 1000 - startTime % 1000
if (finalTime < startTime) { timeoutCallBack(new Date().getTime())
let times = 0
let timeout
if (exactly) {
firstTime = 0
} else {
finalTime = finalTime + firstTime
}
setTimeout(() => {
count()
startTime = startTime + firstTime
}, firstTime)
function count() {
times++
let targetTime = startTime + times * interval
if (targetTime <= finalTime) {
let now = new Date().getTime()
let offset = targetTime - now
if (offset < 0) {
count()
} else {
timeout = setTimeout(() => {
callback(new Date().getTime())
count()
}, offset)
}
} else {
timeoutCallBack(new Date().getTime())
clearTimeout(timeout)
return
}
}
}