timer.js

/**
 * @file 计时器
 * @author lisfan <goolisfan@gmail.com>
 * @version 1.1.2
 * @licence MIT
 */

import validation from '@~lisfan/validation'
import Logger from '@~lisfan/logger'

import FormatDate from './format-date'
import MODE_TYPE from './enums/mode-type'

/**
 * 私有行为
 * @private
 */
const _actions = {
  /**
   * 返回向下取整后的时间戳
   *
   * @since 1.1.0
   *
   * @param {number} [timeStamp=Date.now()] - 时间戳
   *
   * @returns {number}
   */
  timeStamp(timeStamp = Date.now()) {
    return Math.floor(timeStamp / 1000) * 1000
  },
  /**
   * 格式化时间,返回formatDate实例
   *
   * @since 1.1.0
   *
   * @param {Timer} self - timer实例
   *
   * @returns {FormatDate}
   */
  formatDate(self) {
    const date = self.$mode === '+'
      ? (self.$options.timeStamp - self.$remainTimeStamp) + self.$timeZoneTimeStamp
      : self.$remainTimeStamp + self.$timeZoneTimeStamp

    return new FormatDate({
      name: self._logger.$name,
      debug: self._logger.$debug,
      date,
      format: self.$format
    })
  }
}

/**
 * @classdesc
 * 计时类
 *
 * @class
 */
class Timer {
  /**
   * 默认配置选项
   *
   * @since 1.0.0
   *
   * @static
   * @readonly
   * @memberOf Timer
   *
   * @type {object}
   * @property {string} name='timer' - 日志器命名空间
   * @property {boolean} debug=false - 调试模式
   * @property {string} format='mm:ss' -
   *   日期时间格式化字符串,支持使用字母占位符匹配对应的年月日时分秒:Y=年、M=月、D=日、h=时、m=分、s=秒、ms=毫秒,年和毫秒字母占位符可以使用1-4个,其他占位符可以使用1-2个,如果实际结果值长度大于占位符的长度,则显示值实际结果值,如果小于,则前置用0补足
   * @property {string} mode='-' - 计时模式类型,可选值请参考 {@link MODE_TYPE}
   */
  static options = {
    // timeStamp: undefined, // 必须
    name: 'timer',
    debug: false, // 开启调试模式
    format: 'mm:ss', // 日期时间格式化字符串
    mode: '-', // 计时模式
  }

  /**
   * 更新默认配置选项
   *
   * @since 1.0.0
   *
   * @see Timer.options
   *
   * @param {object} options - 其他配置选项见{@link Timer.options}
   *
   * @returns {Timer}
   */
  static config(options) {
    Timer.options = {
      ...Timer.options,
      ...options
    }

    return this
  }

  /**
   * 构造函数
   *
   * @see Timer.options
   *
   * @param {object} options - 其他配置选项见{@link Timer.options}
   * @param {number} options.timeStamp - 剩余计时时间戳,毫秒单位,且毫秒数必须为1000单位,不能是1234这样,超出时会自动向下取整
   */
  constructor(options) {
    this.$options = {
      ...Timer.options,
      ...options
    }

    this._logger = new Logger({
      name: this.$options.name,
      debug: this.$options.debug
    })

    if (!this.$options.timeStamp) {
      return this._logger.error('require timeStamp option param, please check!')
    }

    this.$status = 'prepare'

    // 当前时间戳
    this.$remainTimeStamp = _actions.timeStamp(this.$options.timeStamp)

    // 当前倒计时过去的时间戳
    this.$throughTimeStamp = 0

    // 当前倒计时间字符串
    // 如果是倒计时,则将当前的时间设置为最大值
    this._formatDate = _actions.formatDate(this)
  }

  /**
   * 日志打印器,方便调试
   *
   * @since 1.0.0
   *
   * @private
   */
  _logger = undefined

  /**
   * 日期时间格式化实例
   *
   * @since 1.0.0
   *
   * @private
   */
  _formatDate = undefined

  /**
   * 延迟计时器ID
   *
   * @since 1.0.0
   *
   * @private
   */
  _timeouter = undefined

  /**
   * 实例初始配置项
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {object}
   */
  $options = undefined

  /**
   * 实例状态,可能处于以下状态:prepare=准备创段、processing=进行中、finished=已完成、stoped=暂停中
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {string}
   */
  $status = undefined

  /**
   * 当前地区的偏移时区时间戳
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {number}
   */
  $timeZoneTimeStamp = new Date().getTimezoneOffset() * 1000 * 60
  /**
   * 剩余计时时间戳
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {number}
   */
  $remainTimeStamp = undefined
  /**
   * 已经过的时间戳
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {number}
   */
  $throughTimeStamp = undefined

  /**
   * 开始计时时间戳
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {number}
   */
  $startTimeStamp = undefined

  /**
   * 暂停计时时间戳
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {number}
   */
  $stopTimeStamp = undefined

  /**
   * 计时器开始计时起 => 计时器完成时的一轮周期计时时间戳
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {number}
   */
  $endTimeStamp = undefined
  /**
   * 当前时间的时间戳,用于修正计时器在空间维度中出现异常流逝的时间戳
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {number}
   */
  $currentTimeStamp = undefined

  /**
   * 计时器每次记录的妙表功能
   *
   * @since 1.0.0
   *
   * @readonly
   *
   * @type {string[]}
   */
  $stopwatch = []

  /**
   * 获取实例的计时时间戳配置项
   *
   * @since 1.0.0
   *
   * @getter
   * @readonly
   *
   * @type {number}
   */
  get $timeStamp() {
    return this.$options.timeStamp
  }

  /**
   * 获取实例的日期时间格式化配置项
   *
   * @since 1.0.0
   *
   * @getter
   * @readonly
   *
   * @type {string}
   */
  get $format() {
    return this.$options.format
  }

  /**
   * 获取实例的计时模式配置项
   *
   * @since 1.0.0
   *
   * @getter
   * @readonly
   *
   * @type {string}
   */
  get $mode() {
    return MODE_TYPE[this.$options.mode]
  }

  /**
   * 获取实例的日期时间字符串值
   *
   * @since 1.0.0
   *
   * @getter
   * @readonly
   *
   * @type {string}
   */
  get $datetime() {
    return this._formatDate.toString()
  }

  /**
   * 获取实例的日期时间数据片断
   *
   * @since 1.1.0
   *
   * @getter
   * @readonly
   *
   * @type {object}
   * @property {string} [year] - 剩余年数
   * @property {string} [month] - 剩余月数
   * @property {string} [date] - 剩余日数
   * @property {string} [hour] - 剩余时数
   * @property {string} [minute] - 剩余分数
   * @property {string} [second] - 剩余秒数
   * @property {string} [millisecond] - 剩余毫秒数
   */
  get $data() {
    return this._formatDate.$data
  }

  /**
   * 计时器开始计时
   * 计时器结束后进入resolved状态
   * 若在未结束前中途造成暂停,会触发rejectd状态
   *
   * @since 1.0.0
   *
   * @async
   *
   * @param {function} callback - 每秒执行的回调函数
   *
   * @returns {Promise}
   */
  start(callback) {
    return new Promise((resolve, reject) => {
      this.$status = 'processing'

      // 记录启动时间时间戳
      this.$startTimeStamp = _actions.timeStamp()

      // 设置结束时间戳
      this.$endTimeStamp = this.$startTimeStamp + this.$remainTimeStamp

      // 计时函数
      const ticktick = () => {
        // 如果当前已暂停,则停止倒计时
        if (this.$status !== 'processing') {
          return reject(this.$status)
        }

        // 当前的超时时间戳(当currentTimeStamp不在startTimeStamp和endTmeStamp内时,说明已超时)
        // 倒计时也可能在一些途中超时挂起,导致倒计时没有再走下去,所以每次需要重新捕获
        this.$currentTimeStamp = _actions.timeStamp()

        this.$remainTimeStamp -= 1000
        this.$throughTimeStamp += 1000

        this._logger.log('tick tick...', ((this.$throughTimeStamp / 1000) ) + 's')

        if (this.$currentTimeStamp > (this.$startTimeStamp + this.$throughTimeStamp)) {
          this._logger.log('超过时间流速,自动修正!')
          // 超过的时间流速
          const lostTime = this.$currentTimeStamp - this.$startTimeStamp
          this.$remainTimeStamp = this.$options.timeStamp - lostTime - 1000
          this.$throughTimeStamp = lostTime + 1000
        }

        // 计时时间已到达
        if (this.$remainTimeStamp < 0) {
          this.$remainTimeStamp = 0
          this.$throughTimeStamp = this.$endTimeStamp - this.$startTimeStamp
        }

        this._formatDate = _actions.formatDate(this)

        // 优先执行回调
        validation.isFunction(callback) && callback.call(this)

        /* eslint-enable max-len*/
        if (this._timeouter && this.$currentTimeStamp >= this.$endTimeStamp) {
          clearTimeout(this._timeouter)
          this._timeouter = undefined

          this.$status = 'finished'
          resolve(this.$status)
        } else {
          this._timeouter = setTimeout(() => {
            ticktick()
          }, 1000)
        }
      }

      this._timeouter = setTimeout(() => {
        ticktick()
      }, 1000)
    })
  }

  /**
   * 秒表:记录触发该操作的计时结果,保存结果的值将根据当前实例的format格式化
   *
   * @since 1.1.0
   *
   * @returns {Timer}
   */
  record() {
    this.$stopwatch.push(this.$datetime)
    return this
  }

  /**
   * 计时器停止
   *
   * @since 1.0.0
   *
   * @returns {Timer}
   */
  stop() {
    clearTimeout(this._timeouter)
    this._timeouter = undefined

    this.$status = 'stoped'
    this.$stopTimeStamp = _actions.timeStamp()

    return this
  }

  /**
   * 计时器复位
   *
   * @since 1.0.0
   *
   * @returns {Timer}
   */
  reset() {
    clearTimeout(this._timeouter)
    this._timeouter = undefined

    // 秒表复位
    this.$stopwatch = []

    this.$status = 'prepare'

    this.$remainTimeStamp = _actions.timeStamp(this.$options.timeStamp)
    this.$startTimeStamp = undefined
    this.$stopTimeStamp = undefined
    this.$endTimeStamp = undefined
    this.$currentTimeStamp = undefined

    this.$throughTimeStamp = 0

    this._formatDate = _actions.formatDate(this)

    return this
  }
}

export default Timer