Skip to content
字数
962 字
阅读时间
5 分钟

背景

实习期间遇到个需求,要求根据后端返回的 expire_time 实现一个倒计时功能,刚开始还愣了一下,第一反应就是 setInterval() ,但再细想觉得不可能这么简单,毕竟是现实场景的业务,话不多说就开始了如下的(倒计时)迭代过程

setInterval

js
function countDown(seconds) {   // seconds就是传入的倒计时,一般是毫秒expired_time / 1000得到的值
	setInterval({
		seconds -= 1;
		fn()
		if (seconds <= 0) {
			cleatInterval();  // 倒计时为0,清除倒计时
		}
	}, 1000)
}

问题:定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行

也就是说,使用 setInterval 在时间基数大的情况下,调用次数一多,偏差也会增大,没办法做到准确倒计时

改进一:setTimeout

这时候有同学就要问了,setTimeout 和 setInterval 不是一样执行时间逻辑吗,怎么这个就能改进呢

我们先看代码

js
    const interval = 1000 // 设定倒计时规则为每秒倒计时
    let totalCount = 30000 // 设定总倒计时长为30s
    let count = 0 // 记录递归已执行次数,以倒计时时间间隔 interval=1s 为例,那么count就相当于如果没有时间偏差情况下的理想执行时间
    
    const startTime = new Date().getTime(); // 记录程序开始运行的时间
    let timeoutID = setTimeout(countDownFn, interval)
    
    // 倒计时回调函数
    function countDownFn() {
        count++ // count自增,记录理想执行时间
        // 获取当前时间减去刚开始记录的startTime再减去理想执行时间得到时间偏差:等待执行栈为空的时间
        const offset = new Date().getTime() - startTime - count * interval
        let nextTime = interval - offset // 根据时间偏差,计算下次倒计时设定的回调时间,从而达到纠正的目的
        if (nextTime < 0 ) {
            nextTime = 0
        }
        totalCount -= interval
        if (totalCount < 0) {
            clearTimeout(timeoutID)
        } else {
            timeoutID = setTimeout(countDownFn, nextTime)
        }
    }

核心思想就是 diffTime : 利用进入函数时获取的 startTime 和 offeset,计算得到下一次时间的执行准确时间,如果超时,进行重置操作,以此来不断调整倒计时时间达到精确计时

改进二:使用 requestAnimationFrame

本质上还是 diffTime 的思想,只不过计时的精度又提升了,从 setTimeout 到 requestAnimationFrame

API 参考:window.requestAnimationFrame - Web API 接口参考 | MDN

js
durationFormat(time) {
  if (!time) return { ss: 0 }
  let t = time
  const ss = t % 60
  t = (t - ss) / 60
  if (t < 1) return { ss }
  const mm = t % 60
  t = (t - mm) / 60
  if (t < 1) return { mm, ss }
  const hh = t % 24
  t = (t - hh) / 24
  if (t < 1) return { hh, mm, ss }
  const dd = t
  return { dd, hh, mm, ss }
},
startCountDown() {
  this.curTime = Date.now()
  requestAnimationFrame(this.getCountDownTime.bind(this, this.getTime))
},
getCountDownTime(time) {
  if (time < 0) {
	return
  }
  const { dd, hh, mm, ss } = this.durationFormat(time)
  this.days = dd || 0
  this.hours = hh || 0
  this.mins = mm || 0
  this.seconds = ss || 0
  const now = Date.now()
  const diffTime = (now - this.curTime) / 1000
  if (diffTime < 1) {
	requestAnimationFrame(this.getCountDownTime.bind(this, time))
  } else {
	const step = Math.floor(diffTime)
	this.curTime = now
	requestAnimationFrame(this.getCountDownTime.bind(this, (time - step)))
  }
  // console.log(diffTime)
}


function countDown(seconds) {
    const interval = 1000 // 设定倒计时规则为每秒倒计时
    let totalCount = seconds // 总倒计时长

    let startTime = Date.now() // 记录程序开始运行的时间
    requestAnimation(countDownFn(totalCount))
    
    // 倒计时回调函数
    function countDownFn() {
        if (totalCount < 0) {
            return
        }
        const offset = Date.now() - startTime // 时间偏差
        if (offset < 0 ) {
            requestAnimation(countDownFn(totalCount))
        } else {
            startTime = new Date().getTime()
            requestAnimation(countDownFn(totalCount - offset))
        }
    }
}

从两个 API 的设计来看,requestAnimationFrame 实现的倒计时更为精准,但 setTimeout 应该也够用了,看自己具体使用场景的取舍

贡献者

The avatar of contributor named as jiechen jiechen

页面历史

撰写