[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;
相关推荐
WeiXiao_Hyy34 分钟前
成为 Top 1% 的工程师
java·开发语言·javascript·经验分享·后端
吃杠碰小鸡1 小时前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone1 小时前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_09011 小时前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农1 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king2 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳2 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵3 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星3 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_3 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js