react hooks 封装一个countDown 倒计时组件

1,756 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第八天,点击查看活动详情

开发技术

react , hooks , ts , taro

countdown.gif

需求分析

需要一个可以按天,时,分和秒来进行倒计时的组件。

简单使用

  • 注:主要逻辑请看 useCountDown
import CountDown from '@/components/countDown';
import { useEffect, useState } from 'react';
import Taro from '@tarojs/taro';

const curTime = Date.now()

export default () => {
  const [createTime, setCreateTime] = useState<number>(0);

  useEffect(() => {
    setTimeout(() => {
      // 假设:异步获取五分钟前创建的日期
      setCreateTime(Date.now() - 5 * 60_000)
    }, 1000)
  }, [])

  return (
    <>
      {/* 倒计时10秒 */}
      <CountDown 
        value={curTime} 
        total={10_000} 
        onChange={v => {
          if(v <= 0) Taro.showToast({title: '到时间了', icon: 'none'})
        }} 
      />
      {/* 倒计时3天 */}
      <CountDown value={curTime} total={3 * 86400_000} />
      {/* 异步获取五分钟前创建的日期 */}
      <CountDown value={createTime} total={50 * 60_000} />
    </>
  )
}

countDown组件的封装

props 传参很简单,只有一个 value 起始时间 , 一个 total 倒计时总时间,还有一个 onChange 返回当前的倒计时剩余时间,当为0的时候就可以进行业务需要的倒计时为0的操作了。

import useCountDown from '@/hooks/useCountDown';
import { formatRemainTime } from '@/utils/format';
import { Text } from '@tarojs/components'
import { useEffect } from 'react';
import './index.less';

type PropsType = { 
  /** 起始时间 如果不传就是现在的时间 */
  value?: number
  /** 倒计时时间,默认40min */
  total?: number
  onChange?: (val: number) => void
}

export default ({value, total = 2400_000, onChange}: PropsType) => {
  // 倒计时
  const [countDownNum, setCountDown] = useCountDown()
  useEffect(() => {
    if(value !== void 0) {
      setCountDown(value, total)
    }
  }, [value])
  useEffect(() => {
    if(countDownNum !== void 0) {
      onChange?.(countDownNum)
    }
  }, [countDownNum])
  return (
    <Text className='com-count-down'>
      {formatRemainTime(countDownNum)}
    </Text>
  )
}

useCountDown 的封装

首先定义一个计时器变量 timerRef 主要是用于保存/清除计时器;定义一个用来保存一个当前时间的 state;最后再定义一个参数保存值。

const timerRef = useRef<NodeJS.Timer | undefined>()
const [time, setTime] = useState<number>()
const params = useRef({
  beginTime: 0,
  total: 0
})

当调用 setCountDown 函数时,开启计时器倒计时,同时需要保存一份函数的参数。

// 开启计时器的函数, beginTime: 输入的初始时间 total: 需要计算的时间
const setCountDown = (beginTime: number, total: number) => {
  params.current = { beginTime, total }
  clearInterval(timerRef.current)
  _setInterval(beginTime, total)
}

开启计时器,根据传入的开始时间,倒计时总时间和当前时间来计算出,当前倒计时的剩余值,如果该值大于0,则表示还有时间可以倒计时。采用 setInterval 每隔一秒触发一次 setTime ,当时间小于0清除计时器即可。

const _setInterval = (beginTime: number, total: number) => {
  if(!beginTime || !total) return
  const interval = beginTime + total - Date.now()
  if(interval < 0) return
  setTime(interval)
  timerRef.current = setInterval(() => {
    setTime(t => {
      const _t = t! - 1000
      if(_t <= 0) {
        clearInterval(timerRef.current)
        return 0
      }
      return _t
    })
  }, 1000);
}
  • 注意一:销毁组件时,记得清除计时器防止内存泄漏
useEffect(() => {
  return () => {
    clearInterval(timerRef.current)
  }
}, [])
  • 注意二:由于我这里是用在小程序端的,当小程序退到后台隔几秒后,计时器会自动被停掉,所以当小程序重新展示出来时需要重新开始一下计时器。
useDidShow(() => {
  clearInterval(timerRef.current)
  _setInterval(params.current.beginTime, params.current.total)
})

useCountDown 完整代码

import { useEffect, useRef, useState } from "react"
import { useDidShow } from "@tarojs/taro"

/** 倒计时钩子 */
const useCountDown = () => {
  const timerRef = useRef<NodeJS.Timer | undefined>()
  const [time, setTime] = useState<number>()
  const params = useRef({
    beginTime: 0,
    total: 0
  })

  // 退到后台,大约过个六七秒后定时器会自动暂停掉(回来后自动继续之前的定时器),所以会导致时间不准确
  useDidShow(() => {
    clearInterval(timerRef.current)
    _setInterval(params.current.beginTime, params.current.total)
  })

  useEffect(() => {
    return () => {
      clearInterval(timerRef.current)
    }
  }, [])

  // 开启计时器的函数, beginTime: 输入的初始时间 total: 需要计算的时间
  const setCountDown = (beginTime: number, total: number) => {
    params.current = { beginTime, total }
    clearInterval(timerRef.current)
    _setInterval(beginTime, total)
  }

  const _setInterval = (beginTime: number, total: number) => {
    if(!beginTime || !total) return
    const interval = beginTime + total - Date.now()
    if(interval < 0) return
    setTime(interval)
    timerRef.current = setInterval(() => {
      setTime(t => {
        const _t = t! - 1000
        if(_t <= 0) {
          clearInterval(timerRef.current)
          return 0
        }
        return _t
      })
    }, 1000);
  }

  // 手动清除计时器
  const clearCountDownTimer = () => clearInterval(timerRef.current)

  return [time, setCountDown, clearCountDownTimer] as const
}

export default useCountDown

formatRemainTime 格式化剩余时间

根据时间需求:天,时,分和秒定义一个对象数组,包含它们的字符串和对应的时间,遍历该对象数组,处理好天,时,分和秒的情况即可,再在数字前适当添加0即可。

/** 格式化剩余时间 */
export function formatRemainTime(time?: number) {
  // 当初始化时间为 undefined 时返回
  if(time === void 0) return '0'
  const addZero = (n: number) => n >= 10 ? n : ('0' + n)
  const timeArr: {s: string, t: number}[] = [
    {s: '天', t: 86400},
    {s: '时', t: 3600},
    {s: '分', t: 60},
    {s: '秒', t: 1},
  ]
  time = Math.ceil(time / 1000) 
  let res = ''
  for(let i = 0; i < timeArr.length - 1; i++) {
    const item = timeArr[i]
    if(time >= item.t) {
      const tartget = ~~(time / item.t)
      res += (!i ? tartget : addZero(tartget)) + item.s
      time %= item.t
    }
  }
  res += addZero(~~(time)) + timeArr.at(-1)!.s
  return res
}

总结

回看整个倒计时的代码,感觉代码其实是挺简单的,但是我当时实现起来是花了挺长时间的,主要原因还是我当时对 hooks 不太熟悉,现在回看也觉得写得很一般,不是那么的 hooks;相信很多大牛都能写得比我好很多,我这里只是分享一下自己曾经写的一个小组件。大家可以按需修改使用。