定时器组件选用setTimeout,而不使用setInterval的原因是...

80 阅读4分钟

组件接收参数

名称   类型   描述 
  time   Number|String   传入的时间

传入不同的类型的值

  • T1. Number:是一个正常的时间戳(秒/毫秒)
  • T2. Number:是一个大于0的正整数的值(计时秒数)
  • T3. String:是一个过期(到期)时间
值   类型 所对应的场景
 1704961397   Number T1
 1704961397896   Number T1
200 (3分20秒)NumberT2
7234 (2时34秒)NumberT2
2024-01-11 16:20:34StringT3

判断是否是一个有效的日期

function isValid (time) {
    return !isNaN( new Date(time) )
}

isValid('2024-01-11 16:20:34') // true
isValid('abc') // false
isValid('记得要点赞哟') // false

如果不是一个有效期时间,开发阶段直接抛出异常,如下图

un-valid-time.png

定义组件(components/countDown.vue)

统一值的处理:不管传入的值是一个毫秒时间戳时间戳具体的时间日期计时秒数,使用计算属性computed统一转化为计时秒数(场景T1T3统一转为T2

<template>
  <div class="__countdown-container">
    <slot v-bind="result"></slot>
  </div>
</template>

<script>
export default {
  props: {
    time: { type: [Number, String], default: 0 },
  },
  data() {
    return {
      result: { day: 0, hour: 0, minute: 0, second: 0 },
      timer: null,
    }
  },
  computed: {
    duration() {
      if (!this.isValid()) return

      let t = this.time
      if (Number.isInteger(t)) {
        if (String(t).length < 10) return t
        if (String(t).length === 10) t = +t * 1e3
      }
      return new Date(t).getTime() - Date.now()
    },
  },
  mounted() {
    this.startCountDown()
  },
  watch: {
    duration() {
      this.startCountDown()
    },
  },
  methods: {
    /**
     * 判断是否是一个有效的时间类型
     * @return true 有效时间
     */
    isValid() {
      if (Number.isInteger(this.time)) return !0
      return !isNaN(new Date(this.time))
    },

    /**
     * 开启计时器
     */
    startCountDown() {
      // 如果不是一个有效的时间类型,new Date(time)返回是一个NaN,则不往下执行
      if (!this.isValid()) {
        throw new Error('time is invalid')
      }

      this.duration && this.getTime(this.duration)
    },

    getTime(duration) {
      this.timer && clearTimeout(this.timer)
      if (duration < 0) return

      this.result = this.formater(duration - 1)
      duration--

      this.timer = setTimeout(() => {
        this.getTime(duration - 1)
      }, 1e3)
    },

    formater(duration) {
      let h, i, s
      if (!duration) s = 0
      let t = duration

      ;(s = t % 60), (t = (t - s) / 60)
      ;(i = t % 60), (t = (t - i) / 60)
      ;(h = t % 24), (t = (t - h) / 24)

      return { day: t, hour: h, minute: i, second: s }
    },
  },
}
</script>

父组件(views/demo/index.vue)中调用

count-down.png

<template>
  <div>
    <div class="title">倒计时组件</div>
    <div class="sub-select">
      <select v-model="selectTime">
        <option v-for="o in options" :key="o.title" :value="o.value">
          {{ o.title }}
        </option>
      </select>
      <span suffix>后过期</span>
    </div>
    <div class="sub-submit">
      <button type="button" @click="setCouterTime">开始计时</button>
    </div>
    <count-down v-if="time" v-slot="res" :time="time">
      <div class="count-down">
        剩余<strong>{{ fillZero(res.day) }}</strong
        >天<strong>{{ fillZero(res.hour) }}</strong
        >时<strong>{{ fillZero(res.minute) }}</strong
        >分<strong>{{ fillZero(res.second) }}</strong
        >秒
      </div>
    </count-down>
  </div>
</template>

<script setup>
const selectTime = ref(180)
const time = ref('')
const options = [
  { title: '不是一个有效的值', value: '不是一个有效的值' },
  { title: '3分钟', value: 180 },
  { title: '2小时', value: 7200 },
  { title: '1天', value: 86400 },
  { title: '3天', value: 3 * 86400 },
  { title: '1周', value: 7 * 86400 },
  { title: '1个月', value: 30 * 86400 },
  { title: '3个月', value: 90 * 86400 },
  { title: '6个月', value: 182 * 86400 },
  { title: '1年', value: 365 * 86400 },
]

const fillZero = computed(() => val => {
  if (String(val).length > 2) return val
  return `00${val}`.slice(-2)
})

// 设置倒计时时间
const setCouterTime = () => (time.value = selectTime.value)

onMounted(setCouterTime)

onUnmounted(() => {
  time.value = ''
})
</script>

<style lang="scss" scoped>
.title {
  text-align: center;
  font-size: 2rem;
}

input,
select {
  border: none;
  width: 100%;
  height: 36px;
  padding: 0 0.5rem;
  border-radius: 4px;
}

.sub-input,
.sub-select,
.sub-submit {
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  margin-top: 1rem;
  padding: 0 1rem;
  border: none;
  select {
    border-radius: 4px 0 0 4px;
  }
  span[suffix] {
    width: 100px;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: #fff;
    color: #000;
    height: 36px;
    line-height: 36px;
    border-radius: 0 4px 4px 0;
  }
}

.count-down {
  width: 80%;
  margin: 1rem auto;
  display: flex;
  justify-content: center;
  font-size: 1.5rem;
}
strong {
  font-family: monospace;
  color: #fff;
  text-align: center;
  margin: 0 0.2rem;
  padding: 0 0.25rem;
  border-radius: 4px;
  background-color: #9163f5;
}

button {
  width: 100%;
  height: 36px;
  font-weight: 600;
  padding: 0 1rem;
  border-radius: 4px;
  border: none;
  transition: all 0.3s ease-in-out;
  color: #fff;
  background-color: #9163f5;
  cursor: pointer;
  &:hover {
    background-color: rgba(145, 99, 245, 0.8);
  }
}
</style>

为什么使用setTimeout来模拟setInterval的计时行为

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

setInterval-image.png

上图可见,setInterval每隔100ms往队列中添加一个事件;

100ms后,添加T1定时器代码至队列中,主线程中还有任务在执行,所以等待,some event执行结束后执行T1定时器代码;

又过了100ms,T2定时器被添加到队列中,主线程还在执行T1代码,所以等待;

又过了100ms,理论上又要往队列里推一个定时器代码,但由于此时T2还在队列中,所以T3不会被添加,结果就是此时被跳过;

这里我们可以看到,T1定时器执行结束后马上执行了T2代码,所以并没有达到定时器的效果。

综上所述,setInterval有两个缺点:

  1. 使用setInterval时,某些间隔会被跳过;
  2. 可能多个定时器会连续执行;

可以这么理解:每个setTimeout产生的任务会直接push到任务队列中;而setInterval在每次把任务push到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中)

因而我们一般用setTimeout模拟setInterval,来规避掉上面的缺点。