[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;
相关推荐
子林super9 分钟前
Selection ES集群6月28日压测报告(7.10与7.6.2压测对比)
前端
码哥DFS11 分钟前
JS进阶-day1 作用域&解构&箭头函数
前端·javascript
月光番茄19 分钟前
基于AI的智能自动化测试系统:从Excel到双平台测试的完整解决方案
前端
凌览22 分钟前
因 GitHub 这个 31k Star 的宝藏仓库,我的开发效率 ×10
前端·javascript·后端
喝西瓜汁的兔叽Yan23 分钟前
小效果--多行文本溢出出现省略号
前端·css
子林super23 分钟前
doris用户连接数被打满问题
前端
GISer_Jing29 分钟前
LLM对话框项目总结II
前端·javascript·node.js
恰薯条的屑海鸥31 分钟前
前端进阶之路-从传统前端到VUE-JS(第五期-路由应用)
前端·javascript·vue.js·学习·前端框架
子林super33 分钟前
TIDB常用命令手册
前端
好青崧33 分钟前
单页面和多页面的区别和优缺点
前端·vue