场景

  • 重发验证码的倒计时需求

计时方法

  • 前端可以用来计时的方法:setTimeout/setInterval/requsetAnimationFrame

最直接的实现

let interval;
let count = 30;
interval = setInterval(()=>{
    count--;
    console.log(`${count}s`);
    if(count==0){
    	clearInterval(interval)
    }
},1000)复制代码
  • 加上时间戳看一下结果

前端倒计时二三事_前端

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

前端倒计时二三事_前端_02

  • 因为同步代码的阻塞导致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)
})复制代码
  • 加上同步阻塞看一下时间戳结果

前端倒计时二三事_前端_03

  • 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)
    }
}复制代码
  • 加上同步阻塞看一下时间戳结果

前端倒计时二三事_前端_04

  • 执行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)
    }
}复制代码
  • 加上长时间的同步组塞看一下时间戳

前端倒计时二三事_前端_05

  • 除阻塞校正时间外,基本可以保证与系统时间同步跳秒
其他
  • 时区问题: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()); return }
        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;
            }
        }
    }复制代码