[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;
相关推荐
不一样的少年_2 分钟前
产品催: 1 天优化 Vue 官网 SEO?我用这个插件半天搞定(不重构 Nuxt)
前端·javascript·vue.js
-dcr3 分钟前
50.智能体
前端·javascript·人工智能·ai·easyui
行者9613 分钟前
Flutter跨平台开发适配OpenHarmony:进度条组件的深度实践
开发语言·前端·flutter·harmonyos·鸿蒙
云和数据.ChenGuang14 分钟前
Uvicorn 是 **Python 生态中用于运行异步 Web 应用的 ASGI 服务器**
服务器·前端·人工智能·python·机器学习
IT_陈寒16 分钟前
SpringBoot 3.0实战:这5个新特性让你的开发效率提升50%
前端·人工智能·后端
哈__20 分钟前
React Native 鸿蒙跨平台开发:LayoutAnimation 实现鸿蒙端页面切换的淡入淡出过渡动画
javascript·react native·react.js
遗憾随她而去.24 分钟前
Webpack 面试题
前端·webpack·node.js
我要敲一万行25 分钟前
前端文件上传
前端·javascript
恋猫de小郭27 分钟前
Tailwind 因为 AI 的裁员“闹剧”结束,而 AI 对开源项目的影响才刚刚开始
前端·flutter·ai编程
要加油哦~27 分钟前
算法 | 整理数据结构 | 算法题中,JS 容器的选择
前端·javascript·算法