countdowner.js

/**
 * 倒计时模块
 * @module countDowner
 * @param {Object} obj - 必填项,以对象字面量形式传参
 * @param {Number} obj.countFromInMs - 倒计时开始时的剩余时间,单位ms
 * @param {Function} obj.onTimeout - 倒计时结束时执行的回调函数
 * @param {Function} obj.onTimeChange - 每隔interval时间触发一次的回调函数,参数
 * @param {Number} obj.interval - 每间隔interval毫秒计算一次剩余时间,计算结果通过onTimeChange函数*入参传给调用方
 * @example: 
 * import Countdowner from 'fancy-mini/lib/countdowner'
 * var countdowner = new Countdowner({
 *    countFromInMs: 1000*60*60*24,  // 倒计时一小时
 *    onTimeChange: (res)=>{
 *     // 100ms执行一次,输出格式:[几天,几时,几分,几秒,几个(interval毫秒)]
 *     console.log(res.currentTimeArr);  // [00, 23, 59, 59, 9]
 *     console.log(res.currentTimeArrWithoutDay);  // [35, 23, 23, 23]
 *    },
 *    onTimeout: ()=>{
 *    },
 *    interval: 100 // 100ms输出一次当前时间
 * }); 
 * // 进阶用法:
 * //1、初始化完成后可通过实例绑定/解绑多个事件处理函数
 * countdowner.on('timechange/timeout', fn);
 * countdowner.off('timechange/timeout', fn);
 * //2、暂停/重启,countdowner.pause()/countdowner.restart();
 */
 export default class Countdowner {
  constructor({countFromInMs, onTimeChange, onTimeout, interval=1000}){
    if(onTimeout !== undefined)this.on('timeout', onTimeout);
    if(onTimeChange !== undefined)this.on('timechange', onTimeChange);
    if(countFromInMs !== undefined){
      countFromInMs = Number(countFromInMs);
      this.remainMs = this.countFromInMs = countFromInMs;
      this.deadline = Date.now() + countFromInMs;
      this.interval = interval;
      clearTimeout(this.timer);
      Countdowner.ticktock.call(this);
    }
  }

  timer = null;
  deadline = undefined;
  interval = 100;
  remainMs = undefined;
  isPause = false;
  pauseTime = undefined;
  eventsHandler = {
    timeout: [],
    timechange: []
  };

  /**
   * 事件监听函数
   * @param {string} eventName 事件名(timeout、 timechange)
   * @param {function} fn 回调函数
  */
  on(eventName, fn){
    if(eventName != 'timeout' && eventName != 'timechange'){
      console.error('仅支持timeout和timechange事件');
      return;
    }
    this.eventsHandler[eventName].push(fn);
  }

  /**
   * 取消事件监听
   * @param {string} eventName 事件名(timeout、 timechange)
   * @param {function} fn 回调函数
  */
  off(eventName, fn){
    if(eventName != 'timeout' && eventName != 'timechange'){
      console.error('仅支持timeout和timechange事件');
      return;
    }
    let index = this.eventsHandler[eventName].indexOf(fn);
    if(index > -1)this.eventsHandler[eventName].splice(index, 1);
  }

  /**
   * 暂停
   * @param {Object} Object.strict 是否从暂停时间开始计算剩余时间(默认为true)
  */
  pause({ strict = true }){
    this.pauseTime = Date.now();
    this.strictPause = strict;
    this.isPause = true;
  }

  /**
   * 重启
  */
  restart(){
    this.isPause = false;
    Countdowner.ticktock.call(this);
  }

  static onTimeout(){
    this.eventsHandler.timeout.forEach(fn => {
      typeof fn === 'function' && fn();
    })
  }

  static onTimeChange(timeSnapshot){
    this.eventsHandler.timechange.forEach(fn => {
      typeof fn === 'function' && fn(timeSnapshot);
    })
  }

  static async ticktock(){
    if (this.remainMs<=0) {
      this.countFromInMs>0 && Countdowner.onTimeout.call(this);
      return;
    }
    await new Promise((resolve, reject)=>{
      this.timer = setTimeout(resolve, this.interval);
    });

    // 如果使用了暂停功能,从暂停时间开始计算剩余时间
    let limit = Date.now();
    if(this.pauseTime && this.strictPause){
      limit = this.pauseTime;
      this.pauseTime += this.interval;
    }
    this.remainMs = Math.max(0, this.deadline - limit);

    let timeSnapshot = [
      this.remainMs / DAY,
      this.remainMs % DAY / HOUR,
      this.remainMs % HOUR / MINUTE,
      this.remainMs % MINUTE / SECOND,
      this.remainMs % SECOND / this.interval
    ].map(Math.floor).map((num,idx,arr)=>(padStart(num, 2, '0')));

    let timeSnapshotWithoutDay = [
      this.remainMs / HOUR,
      this.remainMs % HOUR / MINUTE,
      this.remainMs % MINUTE / SECOND,
      this.remainMs % SECOND / this.interval
    ].map(Math.floor).map((num,idx,arr)=>(padStart(num, 2, '0')));
    
    Countdowner.onTimeChange.call(this, {
      currentTimeArr: timeSnapshot,
      currentTimeArrWithoutDay: timeSnapshotWithoutDay
    });
    if(!this.isPause)Countdowner.ticktock.call(this);
  }
}

const SECOND = 1000;
const MINUTE = 60 * SECOND;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;

function padStart(str, minLen, leadChar) {
  str = String(str);
  while (str.length < minLen)
    str = leadChar+str;
  return str;
}