[ahooks] useCountDown源码阅读

用于管理倒计时的hooks

参数

我们先来看Hooks的入参类型和返回值类型

Params

可以接收四个参数:

  • 如果同时传入leftTimetargetDate,则leftTime优先级更高
  • targetDate的类型是dayjs.ConfigType: string | number | Date | Dayjs | null | undefined
  • interval的默认值是1000ms
tsx 复制代码
// Params
export interface Options {
  // 剩余时间(毫秒)
  leftTime?: number;
  // 目标时间
  targetDate?: TDate;
  // 变化时间间隔(毫秒)
  interval?: number;
  // 倒计时结束触发的函数
  onEnd?: () => void;
}

Result

返回两个参数:

  • countdown: 倒计时毫秒时间戳, 类型为number
  • formattedRes: 格式化后的倒计时,类型如下:
tsx 复制代码
export interface FormattedRes {
  // 天数
  days: number;
  // 小时
  hours: number;
  // 分钟
  minutes: number;
  // 秒
  seconds: number;
  // 毫秒
  milliseconds: number;
}

源码

整体源码在最后, 我们一步步拆解

入参处理

入参处理的思路便是参数归一化:剩余时间 -> 目标时间 -> 剩余时间:

  • memoLeftTime: 判断传入的剩余时间是否是有效的,如果是,则将传入的剩余时间转换成备用的目标时间

  • target: 最终的目标时间,这里先判断leftTimetargetDate的优先级,如果传入了leftTime,则取memoLeftTime作为目标时间,否则取targetDate

  • [timeLeft, setTimeLeft]: 初始化的剩余时间的毫秒时间戳state

  • onEndRef: 倒计时结束时触发的函数,使用useLatest包一层是为了避免闭包问题

tsx 复制代码
const useCountDown = (options: Options = {}) => {
    const { leftTime, targetDate, interval = 1000, onEnd } = options || {}; 
    
    const memoLeftTime = useMemo<TDate>(() => { return isNumber(leftTime) && 
    leftTime > 0 ? Date.now() + leftTime : undefined; }, [leftTime]); 
    
    const target = 'leftTime' in options ? memoLeftTime : targetDate; 
    
    const [timeLeft, setTimeLeft] = useState(() => calcLeft(target)); 
    
    const onEndRef = useLatest(onEnd);
    ...
}

calcLeft

上面初始化timeLeftstate时,使用到了calcLeft函数,主要功能是计算出目标时间和当前时间的差值,如果小于等于0,则倒计时结束:

tsx 复制代码
const calcLeft = (target?: TDate) => {
  if (!target) {
    return 0;
  }
  // 计算差值
  const left = dayjs(target).valueOf() - Date.now();
  // 如果小于0,则返回0,否则返回差值本身
  return left < 0 ? 0 : left;
};

倒计时源码

先判断目标时间是否有效,若无效,则直接结束倒计时

接下来是立即执行一次差值计算,目的是为了消除首次渲染的延迟显示以及确保依赖变化时状态同步更新

然后开启一个定时器,依赖传入的interval参数,每隔一段时间执行一次差值计算。如果最后返回的差值为0,则清除定时器,同时触发传入的onEnd函数

当依赖变化和组件卸载时,清除定时器

tsx 复制代码
useEffect(() => {
  if (!target) {
    // for stop
    setTimeLeft(0);
    return;
  }

  // 立即执行一次
  setTimeLeft(calcLeft(target));

  const timer = setInterval(() => {
    const targetLeft = calcLeft(target);
    setTimeLeft(targetLeft);
    if (targetLeft === 0) {
      clearInterval(timer);
      onEndRef.current?.();
    }
  }, interval);

  return () => clearInterval(timer);
}, [target, interval]);

格式化返回值

最后是对返回值进行格式化,parseMs函数是将当前timeLeft毫秒数结构化为时间单位(天,时, 分, 秒, 毫秒),其中使用%取模是为了避免计算结果超出规定的自然的时间导致显示出错:

tsx 复制代码
const formattedRes = useMemo(() => parseMs(timeLeft), [timeLeft]);

return [timeLeft, formattedRes] as const;
  • parseMs:
tsx 复制代码
const parseMs = (milliseconds: number): FormattedRes => {
  return {
    days: Math.floor(milliseconds / 86400000),
    hours: Math.floor(milliseconds / 3600000) % 24,
    minutes: Math.floor(milliseconds / 60000) % 60,
    seconds: Math.floor(milliseconds / 1000) % 60,
    milliseconds: Math.floor(milliseconds) % 1000,
  };
};

整体源码

tsx 复制代码
import dayjs from 'dayjs';
import { useEffect, useMemo, useState } from 'react';
import useLatest from '../useLatest';
import { isNumber } from '../utils/index';

export type TDate = dayjs.ConfigType;

// Params
export interface Options {
  leftTime?: number;
  targetDate?: TDate;
  interval?: number;
  onEnd?: () => void;
}

export interface FormattedRes {
  days: number;
  hours: number;
  minutes: number;
  seconds: number;
  milliseconds: number;
}

const calcLeft = (target?: TDate) => {
  if (!target) {
    return 0;
  }
  // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
  const left = dayjs(target).valueOf() - Date.now();
  return left < 0 ? 0 : left;
};

const parseMs = (milliseconds: number): FormattedRes => {
  return {
    days: Math.floor(milliseconds / 86400000),
    hours: Math.floor(milliseconds / 3600000) % 24,
    minutes: Math.floor(milliseconds / 60000) % 60,
    seconds: Math.floor(milliseconds / 1000) % 60,
    milliseconds: Math.floor(milliseconds) % 1000,
  };
};

const useCountdown = (options: Options = {}) => {
  const { leftTime, targetDate, interval = 1000, onEnd } = options || {};

  const memoLeftTime = useMemo<TDate>(() => {
    return isNumber(leftTime) && leftTime > 0 ? Date.now() + leftTime : undefined;
  }, [leftTime]);

  const target = 'leftTime' in options ? memoLeftTime : targetDate;

  const [timeLeft, setTimeLeft] = useState(() => calcLeft(target));

  const onEndRef = useLatest(onEnd);

  useEffect(() => {
    if (!target) {
      // for stop
      setTimeLeft(0);
      return;
    }

    // 立即执行一次
    setTimeLeft(calcLeft(target));

    const timer = setInterval(() => {
      const targetLeft = calcLeft(target);
      setTimeLeft(targetLeft);
      if (targetLeft === 0) {
        clearInterval(timer);
        onEndRef.current?.();
      }
    }, interval);

    return () => clearInterval(timer);
  }, [target, interval]);

  const formattedRes = useMemo(() => parseMs(timeLeft), [timeLeft]);

  return [timeLeft, formattedRes] as const;
};

export default useCountdown;
相关推荐
用户479492835691515 小时前
改了CSS刷新没反应-你可能不懂HTTP缓存
前端·javascript·面试
还好还好不是吗16 小时前
老项目改造 vue-cli 2.6 升级 rsbuild 提升开发效率300% upupup!!!
前端·性能优化
sumAll16 小时前
别再手动对齐矩形了!这个开源神器让 AI 帮你画架构图 (Next-AI-Draw-IO 体验)
前端·人工智能·next.js
OpenTiny社区16 小时前
2025OpenTiny星光ShowTime!年度贡献者征集启动!
前端·vue.js·低代码
wangan09416 小时前
不带圆圈的二叉树
java·前端·javascript
狗哥哥16 小时前
从零到一:打造企业级 Vue 3 高性能表格组件的设计哲学与实践
前端·vue.js·架构
疯狂平头哥16 小时前
微信小程序真机预览-数字不等宽如何解决
前端
Drift_Dream16 小时前
前端趣味交互:如何精准判断鼠标从哪个方向进入元素?
前端
hqk16 小时前
鸿蒙ArkUI:状态管理、应用结构、路由全解析
android·前端·harmonyos
米思特儿林16 小时前
NuxtImage 配置上传目录配置
前端