jtwang7 / Project-Note

开发小记 - 项目里的一些新奇玩意儿
1 stars 0 forks source link

倒数计时器的 JS 原生实现 #7

Open jtwang7 opened 3 years ago

jtwang7 commented 3 years ago

参考文章:

知识点:

源代码

// JS 实现倒计时

class CountDown {
  /**
   * @param {Object} style - 计时器自定义样式
   * @param {String} id - 实例挂载容器id;例如 '#xxx'
   * @param {String} endTime - 倒计时终止时间; 要求传入日期格式符合规范
   * @param {Function} onReady - 倒计时完毕触发的回调
   */
  constructor(props) {
    this.fontSize = props.style.fontSize ?? '40px'; // 字体大小
    this.fontFamily = props.style.fontFamily ?? 'MTC-7-Segment, YaoSuiXinShouXieTi-2'; // 字体样式; 
    this.color = props.style.color ?? '#111'; // 字体颜色
    this.id = props.id;
    this.endTime = props.endTime;
    this.onReady = props.onReady;

    this.spanId = 'count-down-span';

    this.init(); // 初始化
  }

  // 生成计时器文本节点
  createElement = () => {
    const span = document.createElement('span');
    span.setAttribute('id', this.spanId);
    span.style.setProperty('font-size', this.fontSize); // 设置字体大小
    span.style.setProperty('font-family', this.fontFamily); // 设置字体样式
    span.style.setProperty('color', this.color); // 设置字体颜色
    return span;
  }

  // 计算并返回剩余时间
  calPeriod = (endTime) => {
    const total = Date.parse(endTime) - Date.parse(new Date());
    const seconds = Math.floor((total / 1000) % 60); // 秒
    const minutes = Math.floor((total / 1000 / 60) % 60); // 分
    const hours = Math.floor((total / (1000 * 60 * 60)) % 24); // 时
    const days = Math.floor(total / (1000 * 60 * 60 * 24)); // 天
    return {
      days,
      hours,
      minutes,
      seconds,
    }
  }

  // 个位数补零
  addZero = (value) => {
    return (value < 10) ? ('0' + value) : value.toString();
  }

  // 基于 requestAnimationFrame API 启动计时动画
  setTimeInterval = (endTime, elem, target) => {
    const ctx = this; // 保存 this 指向

    function countdown() {
      const curTime = new Date().getTime();
      let time;
      if (Date.parse(endTime) - curTime > 0) {
        // 获取并更新倒计时时间
        const timeObj = ctx.calPeriod(endTime);
        time = Object.entries(timeObj).reduce((prev, cur) => {
          if (cur[0] === 'days') {
            return prev + ctx.addZero(cur[1]) + '天 ';
          } else if (cur[0] === 'seconds') {
            return prev + ctx.addZero(cur[1]);
          } else {
            return prev + ctx.addZero(cur[1]) + ': '
          }
        }, '');
        // 更新 element 元素的 innerText
        elem.innerText = time;
        // 递归执行动画
        window.requestAnimationFrame(countdown);
      } else {
        // 倒计时完成后
        elem.innerText = ''; // 清空倒计时文本节点
        elem.style.setProperty('font-family', 'Mandhor-ALEmp, YaoSuiXinShouXieTi-2'); // 更换字体样式
        ctx.onReady.call(ctx, '#' + ctx.spanId); // 执行 onReady 回调
      }
    }

    target.appendChild(elem); // 节点插入目标容器
    window.requestAnimationFrame(countdown); // 开启动画递归
  }

  init = () => {
    // 生成节点并插入文档树
    const elem = this.createElement(); // 用于倒计时展示的文本节点
    const target = document.querySelector(this.id); // 容器
    // 开启计时器
    this.setTimeInterval(this.endTime, elem, target);
  }
}

export default CountDown;

要点

  1. 计算剩余时间:计算结束时间与当前时间之间的时间差,Date.parse(endtime) - Date.parse(new Date())

    注意:Date.parse() 解析给定参数字符串并返回毫秒级的时间戳,它与 +new Date() 不同在于,+new Date() 返回的时间戳更加细粒度,在毫秒级之下。

  2. 将时间转换为可用格式:时间差是以毫秒为单位的一段时长,需要将它转化为‘天 / 时 / 分 / 秒’

  3. 添加前导 0:对于个位数的值,需要添加前置 0,例如 7 -> 07

  4. 跨页面保持时钟进度:此处我采用了 requestAnimationFrame() 来对时间计时,requestAnimationFrame() 会向动画队列推入一个回调,该回调被保证必定在浏览器重绘之前被执行,而浏览器刷新频率同步于计算机屏幕的刷新频率,因此每 16 ms 左右会重绘一次。也就是说每 16 ms 左右会执行一次 requestAnimationFrame() 的回调函数,计时及动画都是依赖于该机制实现的。此外,requestAnimationFrame() 仅会在页面激活时执行上述机制,当页面状态为 hidden 时,会自动保存当前执行位置,暂停执行回调,直到页面重新被激活(用户重新切换到该页面)。