面试官:实现一个校正误差的倒计时

466 阅读4分钟

前言

相信大家在学习或日常工作中多多少少都会接触到倒计时相关的需求,本文通过定时器实现倒计时,并实现定时器的暂停、继续功能,同时倒计时过程中自动计算误差时间进行校正。

前置知识

为什么定时器执行过程中会存在误差?

定时器是一个异步宏任务,当我们定义一个定时器时,需要给定时器设置一个回调函数和一个延迟时间,当定时器的延时时间到期后,并不会马上执行设置的回调函数,而是将回调函数放置在任务队列中,需要等待任务队列中其它异步任务执行完毕之后才能执行该定时器的回调,所以定时器任务执行的等待时间实际是:延时时间+任务队列等待时间。正是由于这个问题所以导致通过定时器设置的倒计时长时间执行之后会存在时间不准的问题。

解决思路

在开启倒计时的时候,记录开启的时间点,并且每次执行倒计时的时候,获取当前时间,并记录执行的倒计时次数,可以通过 执行倒计时时间 - (倒计时开启时间 + 延时 * 执行次数) 来计算误差时间,并且在下次开启定时器的时候利用 延时 - 误差 来修改下次倒计时的延时,从而实现在每次倒计时执行的时候都能校正上次定时器产生的误差。

代码解析

倒计时计算时间采用的单位是 ms,需要我们自行去获取展示的数据和定义格式,下面提供一个获取相应时间的简单例子,大家也可以根据需求自行修改。

 // countDown.js
 /**
 * @function formatDate
 * @param time {Number} - 时间 ms
 * @returns {Object} - 时间格式化对象
 */
export function formatDate(time) {
  // 将毫秒转化为秒
  let leftTime = time / 1000
  // 获取天数
  const d = Math.floor(leftTime / (60 * 60 * 24))
  // 除去天数剩余时间
  leftTime = leftTime % (60 * 60 * 24)
  // 获取小时
  const h = Math.floor(leftTime / (60 * 60))
  // 除去天数和小时剩余时间
  leftTime = leftTime % (60 * 60)
  // 获取分钟数
  const min = Math.floor(leftTime / 60)
  // 获取秒数
  const s = Math.floor(leftTime % 60)
  // 定义返回的数据格式,根据需要自定义
  return {
    d: String(d).padStart(2, '0'),
    h: String(h).padStart(2, '0'),
    min: String(min).padStart(2, '0'),
    s: String(s).padStart(2, '0'),
  }
}

下面是倒计时实现的详细代码,除了实现倒计时之外还提供了暂停和继续的功能

// countDown.js
/**
 * @function countDown
 * @param leftTime {Number} - 剩余时间 ms
 * @param callback {Function} - 倒计时结束回调
 * @param getter {Function} - 通过回调返回倒计时时间
 * @returns {Object} - pauseCount: 停止倒计时  continueCount:继续倒计时
 */
export function countDown(leftTime, callback, getter) {
  let timer = null, start = new Date().getTime(), pause = false, interval = 1e3, count = 0
  if (timer) {
    clearTimeout(timer)
  }
  // 开始倒计时
  timer = setTimeout(() => {
    startCount()
  }, interval);

  function startCount() {
    // 记录执行了几次定时器,用于计算偏差时间 
    count++
    // 计算偏差时间
    let offset = new Date().getTime() - ( count * interval + start )
    // 计算下一次定时器延迟时间
    let nextTime = interval - offset
    if (nextTime < 0) {
      nextTime = 0
    }
    // 通过 getter 函数参数,返回倒计时时间
    getter(leftTime)
    leftTime -= interval
    if (leftTime < 0) {
      console.log('定时执行完毕');
      clearTimeout(timer)
      callback()
    } else {
      // 暂停
      if (pause) {
        return
      } else {
        timer = setTimeout(() => {
          startCount()
        }, nextTime);
      }
    }
  }

  // 暂停函数
  function pauseCount() {
    if (pause) return
    pause = true
    clearTimeout(timer)
  }

  // 继续函数
  function continueCount() {
    if (!pause) return
    // 重置开始时间和计数
    count = 0
    start = new Date().getTime()
    pause = false
    setTimeout(() => {
      startCount()
    }, interval);
  }
  // 返回暂停倒计时和继续倒计时的方法
  return { pauseCount, continueCount }
}

下面的代码是完整的使用案例,结合上面的两段代码,可以直接复制代码进行使用(着重实现功能,样式部分可根据需要自行添加)

<template>
  <div class="container">
      <p>{{ date.d }}天{{ date.h }}:{{ date.min }}:{{ date.s }}</p>
      <button @click="continueCount">继续</button>
      <button @click="pauseCount">暂停</button>
    </div>
  </div>
</template>
<script setup>
import { reactive } from 'vue'
import { countDown, formatDate } from 'utils/countDown'
// 用于展示的数据
const date = reactive({
  d: '',
  h: '',
  min: '',
  s: ''
})

function callback() {
  // 定时结束回调
  console.log('定时结束');
}

const { pauseCount, continueCount } = countDown(30000, callback, (leftTime) => {
  // getter 函数,从参数中获取剩余时间并格式化数据
  const { d, h, min, s } = formatDate(leftTime)
  date.d = d
  date.h = h
  date.min = min
  date.s = s
})

</script>

<style lang='scss' scoped>
.container {
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}
</style>

以上代码实现了一个简单的校正误差的倒计时功能,大家也可以根据业务需求,对上述代码进行改装,可以添加重置等功能,格式化数据也可以根据需求进行扩展。在真实业务需求倒计时开发中,需要结合后端来实现完整的倒计时功能,大致的思路就是当页面渲染时需要调用接口获取活动实际的时间来进行判断,如果剩余时间为0则表示不需要进行倒计时,还有剩余时间则将剩余时间传递给倒计时函数,大家可以自行补充。