05月08, 2018

一个合格的定时器

一个可配置是否添加后立即执行、延迟执行时间(ms)、准时执行时间(ms)、持续执行时间(ms)、是否自动启动、是否循环执行、自动移除后回调的定时器想要么?

/**
 * 获取参数原始类型
 */
const _toString = Object.prototype.toString;

function toRawType(value) {
  return _toString.call(value).slice(8, -1);
}

/**
 * 是否是函数
 */
function isFunc(func) {
  return toRawType(func) === 'Function';
}

/**
 * 空函数
 */
function noop() {}

let key = 0;
/**
 * 格式数字为整千位
 * @param {*} num
 * @param {*} ratio
 */
const formatNum = (num, ratio) => {
  return Math.floor(Math.abs(num) / ratio) * ratio;
};
/**
 * 全局定时器
 * @param {Number} timeout 超时时间
 */
export default class {
  constructor(timeout = 1000) {
    this.timeout = timeout;

    this.init();
  }

  /**
   * 初始化
   */
  init() {
    // 记录状态
    this.timerID = null;
    // 执行队列
    this.timers = [];
    // 计数器
    this.count = 0;
    // 起始时间
    this.startTime = 0;
  }
  /**
   * 添加任务
   * @param {Function} fn 待执行函数
   * @param {Object} opts 配置项
   * @param {Number} opts.immediately 是否添加后立即执行
   * @param {Number} opts.delayTime 延迟执行时间(ms)
   * @param {Number} opts.triggerTime 准时执行时间(ms)
   * @param {Number} opts.duration 持续执行时间(ms)
   * @param {Boolean} opts.autoStart 是否自动启动
   * @param {Boolean} opts.loop 是否循环执行
   * @param {Function} opts.removed 自动移除后回调
   * @param {Function} opts.timeout 间隔时间(s)
   * @param {Array<Object>} opts.durationFns 倒计时时间节点回调
   * @param {Boolean} {desc} 是否倒序
   * @param {Number} {time} 时间点(ms)
   * @param {Function} {fn} 回调
   * @return {Number} 任务id
   *
   * @description 执行函数返回参数:duration:剩余执行时间,runTime:已执行时间
   */
  add(fn, opts = {}) {
    // 验证是否为函数
    if (!isFunc(fn)) {
      throw new Error('callback must be Function!');
    }

    opts = {
      autoStart: true,    // 默认自动启动
      immediately: false, // 默认不立即执行
      duration: -1,       // 默认持续时间无穷
      durationFns: [],    // 默认无时间节点回调
      delayTime: 0,       // 默认不延迟执行
      triggerTime: 0,     // 默认不准时执行
      timeout: 1,         // 默认每秒执行一次
      removed: noop,      // 默认移除回调
      loop: !opts.triggerTime, // 若有准时执行时间则默认不循环,否则循环
      ...opts,
      fn,                 // 避免配置中重名属性覆盖
      key: ++key,         // 任务id
      runTime: 0          // 任务已执行时长
    };
    const { autoStart, immediately, duration, runTime } = opts;

    // 若设置立即执行则先一步执行该函数
    immediately && fn({ duration, runTime });

    this.timers.push(opts);

    autoStart && this.start();

    return key;
  }
  /**
   * 移除任务
   * @param {Number} keys 任务id,可传多个
   */
  remove(...keys) {
    let ret = 0;

    keys.forEach(id => {
      const index = this.timers.findIndex(({ key }) => key === id);

      // 找到则删除任务
      if (index > -1) {
        this.timers.splice(index, 1);

        ret++;
      }
    });

    return ret === keys.length;
  }
  /**
   * 开启定时器
   */
  start() {
    // 仅在停止状态才会启动,避免启动多个定时器
    if (!this.timerID) {
      // 设置启动时间
      this.startTime = Date.now();

      const _this = this;

      (function runNext() {
        // 获取任务数量
        const len = _this.timers.length;

        // 只有timers中有待处理函数才会继续执行
        if (len > 0) {
          // 记录执行时间
          const now = Date.now();

          for (let i = 0; i < len; i++) {
            const { fn, delayTime, triggerTime, loop, duration, runTime, removed, durationFns, timeout } = _this.timers[i];
            const copyItem = {};
            /**
             * 移除该任务
             */
            const removeItem = () => {
              _this.timers.splice(i, 1);
              // 移除后触发回调
              isFunc(removed) && removed();
            };

            // 时间间隔相余后为0 && 延时结束 && (无准时执行时间 || 准时执行时间小于当前时间) && 持续时间内
            if (!(_this.count % timeout) && delayTime <= 0 && (!triggerTime || triggerTime < now) && duration !== 0) {
              // 剩余时间(ms)
              const leftTime = formatNum(duration, _this.timeout);

              fn({ duration: leftTime, runTime });
              // 计算运行时间
              copyItem.runTime = _this.timeout * (_this.count + timeout);

              // 执行时间节点回调
              copyItem.durationFns = durationFns.map((item) => {
                const { time, desc = false, fn = noop, hasExecute = false } = item;

                // ((逆向计时 && 剩余时间小于等于设定时间) || (正向计时 && 已执行时间大于等于设定时间)) && 未执行过
                if (((desc && time >= leftTime) || (!desc && runTime >= time)) && !hasExecute) {
                  // 时间符合条件
                  // 未执行过,则执行一次
                  fn({ duration: time });
                  // 标记为已执行
                  item.hasExecute = true;
                  return {
                    ...item,
                    hasExecute: true
                  };
                } else {
                  return item;
                }
              });

              // 若loop为false则在执行一次后移除该方法移除该任务
              !loop && removeItem();
            } else if (duration === 0) {
              // 若剩余时间为零移除该任务
              removeItem();
            }

            // 减少delay时间
            if (delayTime > 0) {
              copyItem.delayTime = delayTime - _this.timeout;
            } else if (duration > 0) {
              // 减少持续时间
              copyItem.duration = formatNum(duration - _this.timeout, _this.timeout);
            }

            // 更新任务配置
            _this.timers[i] = {
              ..._this.timers[i],
              ...copyItem
            };
          }

          // 重置时间计算包含队列偏移量
          const afterNow = Date.now();
          // 计算偏移量
          const offset = afterNow - (_this.startTime + _this.count++ * _this.timeout);
          // 下次执行时间
          const nextTime = (_this.timeout - offset) < 0 ? 0 : _this.timeout - offset;

          _this.timerID = setTimeout(runNext, nextTime);
        } else {
          _this.stop();
        }
      })();
    }
  }
  /**
   * 停止定时器
   */
  stop() {
    clearTimeout(this.timerID);
    this.init();
  }
}

本文链接:http://guchongxi.com/post/timer.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。