JS -- 获取倒计时相差值的几种方式

839 阅读2分钟

倒计时的方式五花八门,寻找一个合适的也挺麻烦的,这里我收集了一些覆盖比较广的实现方式,代码仅供参考。

方式1

推荐的方式

这种,根据一种相差值动态去推算其他

返回根据天为最高单位的结果

const diffTime = (time: number | Date) => {
  try {
    const diffM = dayjs(time).diff(new Date(), 'minute')
    const D = diffM / 60 / 24
    const H = (D - parseInt(D.toString())) * 24
    const m = (H - parseInt(H.toString())) * 60
    return `${parseInt(D.toString())}d,${parseInt(H.toString())}h,${parseInt(m.toString())}m`
  } catch (error) {
    return '0d,0h,0m'
  }
}

OR

const diffTime = (time: number | Date) => {
  try {
    const diffM = differenceInSeconds(time, new Date()) / 60;
    const D = diffM / 60 / 24;
    const H = (D - parseInt(D.toString())) * 24;
    const m = (H - parseInt(H.toString())) * 60;
    return `${parseInt(D.toString())}d,${parseInt(H.toString())}h,${parseInt(
      m.toString(),
    )}m`;
  } catch (error) {
    return '0d,0h,0m';
  }
};

如果是小时为顶峰

// const toString = (n: number) => n > 10 ? parseInt(n.toString()) : '0' + parseInt(n.toString());
const toString = (n: number) => (n > 10 ? n.toFixed(0) : '0' + n.toFixed(0));
const diffTime = (time: number | Date) => {
  try {
    const diffM = differenceInSeconds(time, new Date()) / 60;
    const H = diffM / 60;
    const m = (H - parseInt(H.toString())) * 60;
    const s = (m - parseInt(m.toString())) * 60;
    return `${toString(H)}h,${toString(m)}m,${toString(s)}s`;
  } catch (error) {
    return '0h,0m,0s';
  }
};

请注意,用秒为单位的时候,我把tostring写了两个,然后注释了一个。这是因为,上面有一个bug是,定时器有稍微浮动,比如上一秒结果是2.9999999, 下一秒是2.00000000001, 这时候会在2秒的时候停留两次。所以使用四舍五入的方法解决这个问题。

方式2

也有这种

根据一种相差值,反复推算其他值,然后直接格式一定的时间格式

这种和第一种差不多,不过代码要长很多。(这是网络copy的,不是本人写的, 甩锅成功~~)

image.png

还有这种

image.png

不过其实都挺一般,代码又长性能又差。

方式3

这种是一种相对成熟的计算方式,考虑到了性能和格式等问题

下面一个是性能比较好的一个介绍,也是我比较早之前写的,不过今天重点不是这个,先看下代码。 原文链接 倒计时

const Fresh = (callBacks = []) => {
    let len    
    const _fresh = (endtime, cb, setReVal, dateIndex) => {
      // start end date
      const date = +new Date()
      , leftsecond = ((endtime - date) / 1000) | 0
      // funcs
      ,isStop = Boolean(leftsecond > 0)
      , result = state => val => ({state, val})
      // vars
      let dataObj = {d: void 0, h: void 0, m: void 0,s: void 0,}
      ,re = void 0
      
      // default result val format
      if(!setReVal)
          setReVal = ({d, h, m, s}) => `${d}${h}小时${m}${s}秒`
      //  computed date
      dataObj = {
          d: parseInt(leftsecond / 3600 / 24),
          h: parseInt((leftsecond / 3600) % 24),
          m: parseInt((leftsecond / 60) % 60),
          s: parseInt(leftsecond % 60),
      }
      // set result
      re = isStop 
          ? result(true)
              (setReVal(dataObj))
          : result(false)
              ("结束")
      // stop
      isStop || callBacks.splice(dateIndex, 1)
      // cb
      cb(re)
    }
  
    // export
    const init = (endDateVal, cb, setReVal) => {
      callBacks.push([endDateVal, cb, setReVal])
    }
    
    // export
    const start =  () => {  
      const palyInterval = () => {
        if(callBacks.length == 0){
            clearInterval(sh)
            return
        }
        callBacks.forEach((params, index) => {
            len = len ? len : params.length
            params[len] = index
            _fresh(...params)
        })
      }
      palyInterval()
      // definition Interval
      const sh  = setInterval(palyInterval, 1000)
    }
    return {
      add: init,
      start
    }
  }
  
  
  export default Fresh

这个代码考虑的比较多,但是用起来比较麻烦,就是对于时间的处理逻辑

image.png

这里是多任务合并到一个定时器里面, 在不同的时间段执行不同的处理,红色的代表时间轴 绿色的代表当前时间 黑色的表示一个倒计时进程

方式4

我感觉上面写的都不怎么样,要么太长了,要么太麻烦了,要么不利于阅读。

const downTimer: (op: { date1: number; date2?: number }) => string = ({
  date1,
  date2 = Date.now(),
}) => {
  const date = new Date(Math.abs(date1 - date2));
  const Y = date.getFullYear() - 1970;
  const M = date.getMonth();
  const D = date.getDate();
  const h = date.getHours();
  const m = date.getMinutes();
  const s = date.getSeconds();
  return `${Y}Y ${M}M ${D}D ${h}h ${m}m ${s}s`;
};

这种方式就很酷,但是有限制,格式固定到了年,而第一种方式是固定到了天,更符合一般业务。

注: 思路由Air提供

数据的更新

除了方式三外,其他的都没有自动更新功能,动态更新数据的方式

const CardPutSaleTimer: React.FC<{
  openAt: Date;
}> = memo(({ openAt }) => {
  const classes = useStyles();
  const [timeValue, setTime] = useState('');
  const n = useCount();
  useEffect(() => {
    setTime(diffTime(openAt));
  }, [openAt, n]);
  return (
    <div className={classes.container}>
      <div className={classes.time}>{timeValue}</div>
    </div>
  );
});

useCount是一个动态计算的值, 可以充当刷新功能

const DELAY = 3000;
export const useCount = (delay = DELAY) => {
  const [count, setCount] = useState(0);
  useInterval(() => {
    setCount(currentCount => currentCount + 1);
  }, delay);
  return count;
};

组件例子

import { Box, makeStyles, Theme } from '@material-ui/core';
import differenceInSeconds from 'date-fns/differenceInSeconds';
import { useCount } from 'modules/common/hooks/useTimer';
import { t } from 'modules/i18n/utils/intl';
import { useEffect } from 'react';
import { useState } from 'react';
import { memo } from 'react';

const useStyles = makeStyles<Theme>(theme => ({
  container: {
    position: 'absolute',
    left: 0,
    top: 0,
    height: '100%',
    width: '100%',
    background: 'rgba(0,0,0,0.5)',
    color: '#fff',
  },
  tips: {
    fontSize: 14,
    opacity: 0.4,
    marginBottom: 7,
  },
  time: {
    fontSize: 18,
  },
}));

const toString = (n: number) => (n > 10 ? n.toFixed(0) : '0' + n.toFixed(0));
const diffTime = (time: number | Date) => {
  try {
    const diffM = differenceInSeconds(time, new Date()) / 60;
    const H = diffM / 60;
    const m = (H - parseInt(H.toString())) * 60;
    const s = (m - parseInt(m.toString())) * 60;
    return `${toString(H)}h ${toString(m)}m ${toString(s)}s`;
  } catch (error) {
    return '0h 0m 0s';
  }
};

const CardPutSaleTimer: React.FC<{
  openAt: Date;
}> = memo(({ openAt }) => {
  const classes = useStyles();
  const [timeValue, setTime] = useState('');
  const reload = useCount(1e3);
  const [isTimeOver, setIsTimeOver] = useState(false);
  useEffect(() => {
    if (+openAt < Date.now()) {
      setIsTimeOver(true);
      return () => {};
    }
    setTime(diffTime(openAt));
  }, [openAt, reload]);
  return isTimeOver ? (
    <></>
  ) : (
    <Box
      display="flex"
      justifyContent="center"
      alignItems="center"
      className={classes.container}
    >
      <div>
        <div className={classes.tips}>{t('product-card.pool-start-tips')}</div>
        <div className={classes.time}>{timeValue}</div>
      </div>
    </Box>
  );
});

export default CardPutSaleTimer;

效果预览

image.png

除此之外,还可以在hook函数里面内置刷新,如

import { useInterval } from 'modules/common/hooks/useInterval';
import { Milliseconds, Minutes } from 'modules/common/types/unit';
import { getTimeRemaining } from 'modules/common/utils/getTimeRemaining';
import { t } from 'modules/i18n/utils/intl';
import { useMemo, useState } from 'react';

const INTERVAL_BREAKPOINT: Minutes = 2; // minutes
const SHORT_STEP: Milliseconds = 1000; // when time left less than INTERVAL_BREAKPOINT
const NORMAL_STEP: Milliseconds = 1000 * 30;

export const useTimer = (endDate: Date) => {
  const [timeRemaining, setTimeRemaining] = useState(getTimeRemaining(endDate));

  const delayInterval = useMemo(() => {
    if (timeRemaining.minutes >= INTERVAL_BREAKPOINT) {
      return NORMAL_STEP;
    }

    if (timeRemaining.total > 0) {
      return SHORT_STEP;
    }

    return null;
  }, [timeRemaining.minutes, timeRemaining.total]);

  useInterval(() => {
    if (!endDate) return;
    setTimeRemaining(getTimeRemaining(endDate));
  }, delayInterval);

  const isTimeOver = timeRemaining.total <= 0;

  const duration = t('time.time-left-short', {
    days:
      timeRemaining.days !== 0 &&
      t('time.days-short', {
        days: timeRemaining.days,
      }),
    hours:
      timeRemaining.hours !== 0 &&
      t('time.hours-short', {
        hours: timeRemaining.hours,
      }),
    minutes:
      timeRemaining.minutes !== 0 &&
      t('time.minutes-short', {
        minutes: timeRemaining.minutes,
      }),
    seconds:
      timeRemaining.minutes < INTERVAL_BREAKPOINT &&
      timeRemaining.seconds !== 0 &&
      t('time.seconds-short', {
        seconds: timeRemaining.seconds,
      }),
  });

  const modificationDate = (value: number): string => {
    return value < 10 ? `0${value}` : `${value}`;
  };
  // 'Ended on 01/07/2021'
  const endDetailedDate = t('time.time-end', {
    days: modificationDate(endDate.getDate()),
    months: modificationDate(endDate.getMonth() + 1),
    years: endDate.getFullYear(),
  });

  return { timeRemaining, duration, isTimeOver, endDetailedDate };
};

不过我觉得除了方式三必须内置外,其他不内置其实才是最方便的

另外 也希望大家可以给多语言内部贡献倒计时插件等,这样大家才可以彻底摆脱倒计时手写的麻烦。

-- 完--