wot-design-uni 倒计时逻辑封装:useCountDown 组合式函数完整指南

原创2026-10-13 19:17:401,238 阅读
文章标签:UI组件移动开发小程序设计系统跨平台前端

wot-design-uni 倒计时逻辑封装:useCountDown 组合式函数完整指南

useCountDown 是 wot-design-uni 内置的组合式函数(Composable),用于在 Vue 3 + TypeScript 的 uni-app 项目中封装倒计时相关逻辑,提供开始、暂停、重置与剩余时间响应式读取能力,并支持秒级与毫秒级两种渲染精度。本文以 use-count-down.md 为骨架,结合仓库源码与测试,深入讲解其 API 用法、底层实现原理以及在 wd-count-down 组件中的实际落地方式,读完即可在任意页面或业务模块中独立接入倒计时能力。

useCountDown 是什么

useCountDown 是一个与 UI 无关的纯逻辑组合式函数,出自 wot-design-uni 的 composables 集合(与 useToast、useUpload、useConfigProvider 等并列)。它不依赖任何 DOM 结构,因此既可以在 <script setup> 中直接使用,也可以被组件内部调用以复用倒计时逻辑。

从仓库结构看,其实现位于 useCountDown.ts,并通过 composables/index.ts 统一导出,最终由 uni_modules/wot-design-uni/index.ts 中的 export * from './components/composables' 重新导出,因此可以像文档示例一样直接书写:

import { useCountDown } from '@/uni_modules/wot-design-uni'

在组件库内部,该函数已被 wd-count-down 组件和 wd-count-to 组件复用,是倒计时类能力的公共底层。需要渲染倒计时 UI 时可直接使用组件,而需要将倒计时逻辑嵌入自有业务(如验证码发送、活动限时、支付倒计时)时,组合式函数是更轻量的选择。

基础用法:三步接入倒计时

第一步:调用并解构能力

import { useCountDown } from '@/uni_modules/wot-design-uni'

const { start, pause, reset, current } = useCountDown({
  time: 60 * 1000, // 总时长 60 秒,单位毫秒
  onChange(current) {
    console.log('剩余时间', current)
  },
  onFinish() {
    console.log('倒计时结束')
  }
})

time 传入的是倒计时总时长(毫秒)。current 是一个 Vue 响应式 computed 对象,值为 CurrentTime 结构,可在模板中直接展示或通过 current.value 读取。

第二步:控制倒计时运行状态

// 开始倒计时
start()

// 暂停倒计时(保留剩余时间,再次 start 会从暂停点继续)
pause()

// 重置倒计时(回到初始 time)
reset()

// 获取当前时间
console.log(current.value)

第三步:展示倒计时内容

结合模板渲染,例如在 <script setup> 中使用:

<template>
  <view>剩余 {{ current.hours }}:{{ current.minutes }}:{{ current.seconds }}</view>
</template>

<script setup lang="ts">
import { useCountDown } from '@/uni_modules/wot-design-uni'
const { start, current } = useCountDown({ time: 60 * 1000 })
start()
</script>

API 详解

参数(UseCountDownOptions)

参数 说明 类型 默认值 源码定义
time 倒计时总时间,单位毫秒 number -(必传) useCountDown.ts
millisecond 是否开启毫秒级渲染,true 时每帧更新,false 时仅每秒更新 boolean false useCountDown.ts
onChange 倒计时变化回调,参数为当前 CurrentTime (current: CurrentTime) => void - useCountDown.ts
onFinish 倒计时结束回调 () => void - useCountDown.ts

其中 time 在源码类型中为必填项(UseCountDownOptions 中无 ? 修饰);millisecond、onChange、onFinish 均为可选。onChange 触发时点与渲染精度相关:秒级模式下只在秒数变化时触发,毫秒级模式下则按帧触发。

方法

方法名 说明 参数 返回值
start 开始倒计时;若当前处于暂停状态,会基于剩余时间继续计时 - -
pause 暂停倒计时,取消正在进行的渲染循环,保留剩余时间 - -
reset 重置倒计时;可选传入新总时长 time?: number,缺省时回到初始 options.time time?: number -

注意 reset 与文档示例中 reset() 的差别:reset 支持传入一个可选的时长参数用于重新设定总时间,例如 reset(90 * 1000) 会将倒计时重置为 90 秒;不传参数则恢复为初始化时的 time。reset 内部会先调用 pause,因此重置后处于停止状态,需要手动 start()(组件层面对此有 autoStart 包装,详见下文)。

CurrentTime 结构

type CurrentTime = {
  days: number
  hours: number
  total: number
  minutes: number
  seconds: number
  milliseconds: number
}

该结构与源码中导出的类型定义一致(useCountDown.ts)。各字段含义:

  • total:剩余总毫秒数(remain 的原始值);
  • days:剩余天数,由 Math.floor(time / DAY) 计算,其中 DAY = 24 * 60 * 60 * 1000;
  • hours:剩余小时数(不含天数进位);
  • minutes:剩余分钟数(不含小时进位);
  • seconds:剩余秒数(不含分钟进位);
  • milliseconds:不足 1 秒的毫秒部分,Math.floor(time % 1000)。

即 total 可表达为 days * DAY + hours * HOUR + minutes * MINUTE + seconds * SECOND + milliseconds 的完整分解,各段均为 0 到对应进位上界之间的整数,便于直接渲染。

源码级原理剖析

时间分解:parseTime

parseTime 函数(useCountDown.ts)把剩余毫秒数逐级拆分为天、时、分、秒、毫秒:

const SECOND = 1000
const MINUTE = 60 * SECOND
const HOUR = 60 * MINUTE
const DAY = 24 * HOUR

function parseTime(time: number): CurrentTime {
  const days = Math.floor(time / DAY)
  const hours = Math.floor((time % DAY) / HOUR)
  const minutes = Math.floor((time % HOUR) / MINUTE)
  const seconds = Math.floor((time % MINUTE) / SECOND)
  const milliseconds = Math.floor(time % SECOND)
  return { total: time, days, hours, minutes, seconds, milliseconds }
}

current 正是通过 computed(() => parseTime(remain.value)) 派生而来,因此只要 remain 变化,current 会自动重新计算并驱动视图更新。

秒级与毫秒级双模式调度:tick 分派

源码根据 options.millisecond 决定走 microTick(每帧更新)还是 macroTick(每秒更新):

  • microTick(毫秒级)(useCountDown.ts):每次回调都调用 setRemain(getCurrentRemain()) 写入最新剩余时间,只要剩余时间大于 0 就继续调度下一帧。
  • macroTick(秒级)(useCountDown.ts):先通过 isSameSecond(remainRemain, remain.value) 判断秒数是否真的变化,只有秒级数值变化或归零时才触发 setRemain,避免每秒 60 次无意义的响应式更新。

isSameSecond 的实现(useCountDown.ts)是:

function isSameSecond(time1: number, time2: number): boolean {
  return Math.floor(time1 / 1000) === Math.floor(time2 / 1000)
}

这一判断让秒级模式在性能和精度之间取得平衡:回调按帧触发但写入只在整秒边界发生。

帧循环底层:useRaf 的跨端适配

倒计时的调度循环并非使用 setInterval,而是基于 useRaf(useRaf.ts)。useRaf 按运行环境做了分支:

  • H5 环境使用 requestAnimationFrame;
  • 非 H5 环境(小程序等)使用 setTimeout(() => handle(Date.now()), 1000 / 30) 模拟约 30fps 的帧回调。

start/cancel 对 requestAnimationFrame 句柄或 setTimeout 句柄做了统一管理,并在 onUnmounted 时自动取消。这保证在 uni-app 多端环境下倒计时逻辑都能正常工作,且组件卸载不会留下悬挂的定时器。

结束与回调触发:setRemain

setRemain(useCountDown.ts)是数据更新的唯一出口:

const setRemain = (value: number) => {
  remain.value = value
  isDef(options.onChange) && options.onChange(current.value)
  if (value === 0) {
    pause()
    isDef(options.onFinish) && options.onFinish()
  }
}

可见:每次写入剩余时间都会触发 onChange(若配置);当剩余时间归零时自动 pause() 并触发 onFinish。getCurrentRemain 通过 Math.max(endTime - Date.now(), 0) 计算剩余时间,endTime 在 start() 时以 Date.now() + remain.value 为准,因此基于真实时间戳而非简单的计数器递减,天然规避了页面卡顿造成的累计误差——暂停后再次 start() 会基于保存的剩余时间重新计算 endTime。

组件卸载自动清理

组合式函数内部通过 onBeforeUnmount(pause)(useCountDown.ts)注册卸载钩子,页面或组件销毁时自动停止倒计时并取消帧循环,无需调用方手动清理,避免内存泄漏与后台空转。

实战:毫秒级渲染与手动控制

毫秒级倒计时

将 millisecond: true 与 format 配合,可展示到毫秒。组件库演示页 countDown/Index.vue 中的用法如下:

<wd-count-down :time="time" millisecond format="HH:mm:ss:SS" />

wd-count-down 的 format 支持 DD(天)、HH(小时)、mm(分钟)、ss(秒)、S/SS/SSS(毫秒 1/2/3 位),默认格式为 HH:mm:ss,其格式化逻辑实现在 wd-count-down/utils.ts,底层使用 padZero 补零(common/util.ts)。

手动控制与自动开始

wd-count-down 组件还提供了 auto-start 属性(默认 true),并暴露 start/pause/reset 实例方法。演示页中的手动控制示例(countDown/Index.vue):

<wd-count-down ref="countDown" :time="3000" millisecond :auto-start="false" format="ss:SSS" @finish="onFinish"></wd-count-down>
const start = () => countDown.value!.start()
const pause = () => countDown.value!.pause()
const reset = () => countDown.value!.reset()

wd-count-down 组件内部正是调用了 useCountDown,并将 onChange/onFinish 桥接为 change/finish 事件(wd-count-down.vue)。组件对 reset 做了增强:重置后若 auto-start 为 true 会自动重新开始,相关类型定义见 wd-count-down/types.ts。

复用与测试:useCountDown 在仓库中的证据

  • 组件复用:除 wd-count-down 外,wd-count-to(数字滚动动画组件)同样引入 useCountDown 作为计时基座(wd-count-to.vue),证明该组合式函数是组件库中倒计时类能力的通用底层。
  • 测试覆盖:wd-count-down 的单元测试 wd-count-down.test.ts 覆盖了默认渲染、自定义格式(DD 天 HH 时 mm 分 ss 秒、HH:mm:ss、mm:ss、ss)、autoStart 行为、pause 后不再触发 onChange、reset 恢复初始时间、time 属性变化更新显示、毫秒精度渲染(ss:SSS 格式、time: 1500 显示 01:500)以及 start/pause/reset 方法暴露等场景,可作为理解 useCountDown 行为边界的参考。

典型业务场景与注意事项

典型场景:发送短信验证码的 60 秒冷却、活动页限时抢购倒计时、支付超时提醒、答题计时等。由于 useCountDown 与渲染层解耦,可以轻松接入自定义 UI(如胶囊按钮、进度环、数字滚动动画),而不必受组件默认样式的约束。

注意事项:

  1. time 必传且单位为毫秒,传 60 * 1000 表示 60 秒;
  2. 默认(millisecond: false)为秒级渲染,秒内的毫秒值不会实时变化,需要毫秒精度时显式开启;
  3. reset() 不传参会回到初始 time,传新值可重置总时长;组合式函数层面的 reset 不会自动开始,需要自行调用 start()(组件层面由 auto-start 决定);
  4. 组件卸载后倒计时自动停止,若需要跨页面保持计时,应将状态提升到全局 store,而不是依赖组件实例;
  5. 秒级模式依赖 endTime - Date.now() 的真实时间差,设备休眠或切后台期间的计时以系统时间为准,恢复前台后会自动对齐,不会出现“少计”或“多计”。

小结

useCountDown 以约 130 行的轻量实现,提供了完整的倒计时状态机:时间分解(parseTime)、双精度调度(microTick/macroTick)、跨端帧循环(useRaf)、真实时间戳计时与卸载清理。无论你是直接在页面中使用它,还是阅读 wd-count-down 源码学习组件封装技巧,理解本文介绍的 API 与底层原理,都能让你在 wot-design-uni 中更从容地处理一切与时间相关的交互逻辑。

登录后查看全文
wot-design-uni