Source: tiny/core/ticker/Ticker.js

import settings from '../settings';
import { config } from '../settings'; // eslint-disable-line
import * as utils from '../utils';
import { UPDATE_PRIORITY } from '../const';
import TickerListener from './TickerListener';

/**
 * A Ticker class that runs an update loop that other objects listen to.
 * This class is composed around listeners meant for execution on the next requested animation frame.
 * Animation frames are requested only when necessary,
 * e.g. When the ticker is started and the emitter has listeners.
 *
 * @class
 * @memberof Tiny.ticker
 */
export default class Ticker {
  /**
   *
   */
  constructor() {
    /**
     * The first listener. All new listeners added are chained on this.
     * @private
     * @type {TickerListener}
     */
    this._head = new TickerListener(null, null, Infinity);

    /**
     * Internal current frame request ID
     * @private
     */
    this._requestId = null;

    /**
     * Internal value managed by minFPS property setter and getter.
     * This is the maximum allowed milliseconds between updates.
     * @private
     */
    this._maxElapsedMS = 100;

    /**
     * Whether or not this ticker should invoke the method {@link Tiny.ticker.Ticker#start} automatically when a listener is added.
     *
     * @member {boolean}
     * @default false
     */
    this.autoStart = false;

    /**
     * Scalar time value from last frame to this frame.
     * This value is capped by setting {@link Tiny.ticker.Ticker#minFPS} and is scaled with {@link Tiny.ticker.Ticker#speed}.
     * **Note:** The cap may be exceeded by scaling.
     *
     * @member {number}
     * @default 1
     */
    this.deltaTime = 1;

    /**
     * Time elapsed in milliseconds from last frame to this frame.
     * Opposed to what the scalar {@link Tiny.ticker.Ticker#deltaTime} is based, this value is neither capped nor scaled.
     * If the platform supports DOMHighResTimeStamp, this value will have a precision of 1 µs.
     * Defaults to target frame time
     *
     * @member {number}
     * @default 16.66
     */
    this.elapsedMS = 1 / settings.TARGET_FPMS;

    /**
     * The last time {@link Tiny.ticker.Ticker#update} was invoked.
     * This value is also reset internally outside of invoking update, but only when a new animation frame is requested.
     * If the platform supports DOMHighResTimeStamp, this value will have a precision of 1 µs.
     *
     * @member {number}
     * @default 0
     */
    this.lastTime = 0;

    /**
     * Factor of current {@link Tiny.ticker.Ticker#deltaTime}.
     * @example
     * // Scales ticker.deltaTime to what would be the equivalent of approximately 120 FPS
     * ticker.speed = 2;
     *
     * @member {number}
     * @default 1
     */
    this.speed = 1;

    /**
     * Whether or not this ticker has been started. `true` if {@link Tiny.ticker.Ticker#start} has been called. `false` if {@link Tiny.ticker.Ticker#stop} has been called.
     * While `false`, this value may change to `true` in the event of {@link Tiny.ticker.Ticker#autoStart} being `true`
     * and a listener is added.
     *
     * @member {boolean}
     * @default false
     */
    this.started = false;

    /**
     * Internal tick method bound to ticker instance.
     * This is because in early 2015, Function.bind
     * is still 60% slower in high performance scenarios.
     * Also separating frame requests from update method
     * so listeners may be called at any time and with
     * any animation API, just invoke ticker.update(time).
     *
     * @private
     * @param {number} time - Time since last tick.
     */
    this._tick = (time) => {
      this._requestId = null;

      if (this.started) {
        // Invoke listeners now
        this.update(time);
        // Listener side effects may have modified ticker state.
        if (this.started && this._requestId === null && this._head.next) {
          this._requestId = requestAnimationFrame(this._tick);
        }
      }
    };
  }

  /**
   * Conditionally requests a new animation frame.
   * If a frame has not already been requested, and if the internal
   * emitter has listeners, a new frame is requested.
   *
   * @private
   */
  _requestIfNeeded() {
    if (this._requestId === null && this._head.next) {
      // ensure callbacks get correct delta
      this.lastTime = performance.now();
      this._requestId = requestAnimationFrame(this._tick);
    }
  }

  /**
   * Conditionally cancels a pending animation frame.
   *
   * @private
   */
  _cancelIfNeeded() {
    if (this._requestId !== null) {
      cancelAnimationFrame(this._requestId);
      this._requestId = null;
    }
  }

  /**
   * Conditionally requests a new animation frame.
   * If the ticker has been started it checks if a frame has not already
   * been requested, and if the internal emitter has listeners. If these
   * conditions are met, a new frame is requested. If the ticker has not
   * been started, but autoStart is `true`, then the ticker starts now,
   * and continues with the previous conditions to request a new frame.
   *
   * @private
   */
  _startIfPossible() {
    if (this.started) {
      this._requestIfNeeded();
    } else if (this.autoStart) {
      this.start();
    }
  }

  /**
   * Register a handler for tick events. Calls continuously unless
   * it is removed or the ticker is stopped.
   *
   * @param {Function} fn - The listener function to be added for updates
   * @param {Function} [context] - The listener context
   * @param {number} [priority=Tiny.UPDATE_PRIORITY.NORMAL] - The priority for emitting
   * @returns {Tiny.ticker.Ticker} This instance of a ticker
   */
  add(fn, context, priority = UPDATE_PRIORITY.NORMAL) {
    return this._addListener(new TickerListener(fn, context, priority));
  }

  /**
   * Add a handler for the tick event which is only execute once.
   *
   * @param {Function} fn - The listener function to be added for one update
   * @param {Function} [context] - The listener context
   * @param {number} [priority=Tiny.UPDATE_PRIORITY.NORMAL] - The priority for emitting
   * @returns {Tiny.ticker.Ticker} This instance of a ticker
   */
  addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) {
    return this._addListener(new TickerListener(fn, context, priority, true));
  }

  /**
   * Internally adds the event handler so that it can be sorted by priority.
   * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run
   * before the rendering.
   *
   * @private
   * @param {TickerListener} listener - Current listener being added.
   * @returns {Tiny.ticker.Ticker} This instance of a ticker
   */
  _addListener(listener) {
    //@version 1.1.7 引入,用于控制所有实例化的 Ticker 的帧率
    if (listener.fn !== utils.__frameDot) {
      const func = listener.fn;
      listener.preFn = func;
      listener.fn = function (t) {
        if (utils.equalsFramCount(config.fps)) {
          func.call(this, t);
        }
      };
    }
    // For attaching to head
    let current = this._head.next;
    let previous = this._head;

    // Add the first item
    if (!current) {
      listener.connect(previous);
    } else {
      // Go from highest to lowest priority
      while (current) {
        if (listener.priority > current.priority) {
          listener.connect(previous);
          break;
        }
        previous = current;
        current = current.next;
      }

      // Not yet connected
      if (!listener.previous) {
        listener.connect(previous);
      }
    }

    this._startIfPossible();

    return this;
  }

  /**
   * Removes any handlers matching the function and context parameters.
   * If no handlers are left after removing, then it cancels the animation frame.
   *
   * @param {Function} fn - The listener function to be removed
   * @param {Function} [context] - The listener context to be removed
   * @returns {Tiny.ticker.Ticker} This instance of a ticker
   */
  remove(fn, context) {
    let listener = this._head.next;

    while (listener) {
      //@version 1.1.7 引入,_addListener 中重设后的恢复
      if (listener.fn !== utils.__frameDot) {
        listener.fn = listener.preFn;
      }
      // We found a match, lets remove it
      // no break to delete all possible matches
      // incase a listener was added 2+ times
      if (listener.match(fn, context)) {
        listener = listener.destroy();
      } else {
        listener = listener.next;
      }
    }

    if (!this._head.next) {
      this._cancelIfNeeded();
    }

    return this;
  }

  /**
   * Starts the ticker. If the ticker has listeners
   * a new animation frame is requested at this point.
   */
  start() {
    if (!this.started) {
      this.started = true;
      this._requestIfNeeded();
    }
  }

  /**
   * Stops the ticker. If the ticker has requested
   * an animation frame it is canceled at this point.
   */
  stop() {
    if (this.started) {
      this.started = false;
      this._cancelIfNeeded();
    }
  }

  /**
   * Destroy the ticker and don't use after this. Calling
   * this method removes all references to internal events.
   */
  destroy() {
    this.stop();

    if (this._head) {
      let listener = this._head.next;

      while (listener) {
        listener = listener.destroy(true);
      }

      this._head.destroy();
      this._head = null;
    }
  }

  /**
   * 定时触发,暂不支持 stop
   * 注意:该方法已不推荐使用,请直接使用 `Tiny.ticker.CountDown`
   *
   * @version 1.0.2
   * @deprecated since version 1.1.7
   * @param {object}    opts
   * @param {number}    opts.duration   - 间隔时长(单位:ms)
   * @param {function}  opts.callback
   * @param {number}    opts.times      - 次数,不传即无限次
   * @param {function}  opts.complete   - 完成后的回调
   */
  countDown(opts) {
    console.warn('This Function is deprecated, use Tiny.ticker.Countdown instead.');
    const duration = opts.duration || 1e3;
    const times = opts.times || Infinity;
    const callback = opts.callback ||
      function () {
      };
    const complete = opts.complete ||
      function () {
      };
    let start = utils.getTime();
    let count = 0;
    const self = this;
    const fn = function (time) {
      if (count >= times) {
        self.remove(fn);
        complete(time);
        return;
      }
      if (utils.getTime() - start >= duration) {
        callback(time);
        start += duration;
        count++;
      }
    };
    self.add(fn);
  }

  /**
   * Triggers an update. An update entails setting the
   * current {@link Tiny.ticker.Ticker#elapsedMS},
   * the current {@link Tiny.ticker.Ticker#deltaTime},
   * invoking all listeners with current deltaTime,
   * and then finally setting {@link Tiny.ticker.Ticker#lastTime}
   * with the value of currentTime that was provided.
   * This method will be called automatically by animation
   * frame callbacks if the ticker instance has been started
   * and listeners are added.
   *
   * @param {number} [currentTime=performance.now()] - the current time of execution
   */
  update(currentTime = performance.now()) {
    let elapsedMS;

    // If the difference in time is zero or negative, we ignore most of the work done here.
    // If there is no valid difference, then should be no reason to let anyone know about it.
    // A zero delta, is exactly that, nothing should update.
    //
    // The difference in time can be negative, and no this does not mean time traveling.
    // This can be the result of a race condition between when an animation frame is requested
    // on the current JavaScript engine event loop, and when the ticker's start method is invoked
    // (which invokes the internal _requestIfNeeded method). If a frame is requested before
    // _requestIfNeeded is invoked, then the callback for the animation frame the ticker requests,
    // can receive a time argument that can be less than the lastTime value that was set within
    // _requestIfNeeded. This difference is in microseconds, but this is enough to cause problems.
    //
    // This check covers this browser engine timing issue, as well as if consumers pass an invalid
    // currentTime value. This may happen if consumers opt-out of the autoStart, and update themselves.
    if (currentTime > this.lastTime) {
      // Save uncapped elapsedMS for measurement
      elapsedMS = this.elapsedMS = currentTime - this.lastTime;

      // cap the milliseconds elapsed used for deltaTime
      if (elapsedMS > this._maxElapsedMS) {
        elapsedMS = this._maxElapsedMS;
      }

      this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed;

      // Cache a local reference, in-case ticker is destroyed
      // during the emit, we can still check for head.next
      const head = this._head;

      // Invoke listeners added to internal emitter
      let listener = head.next;

      while (listener) {
        listener = listener.emit(this.deltaTime);
      }

      if (!head.next) {
        this._cancelIfNeeded();
      }
    } else {
      this.deltaTime = this.elapsedMS = 0;
    }

    this.lastTime = currentTime;
  }

  /**
   * The frames per second at which this ticker is running.
   * The default is approximately 60 in most modern browsers.
   * **Note:** This does not factor in the value of
   * {@link Tiny.ticker.Ticker#speed}, which is specific
   * to scaling {@link Tiny.ticker.Ticker#deltaTime}.
   *
   * @member {number}
   * @readonly
   * @private
   */
  get FPS() {
    return 1000 / this.elapsedMS;
  }

  /**
   * Manages the maximum amount of milliseconds allowed to
   * elapse between invoking {@link Tiny.ticker.Ticker#update}.
   * This value is used to cap {@link Tiny.ticker.Ticker#deltaTime},
   * but does not effect the measured value of {@link Tiny.ticker.Ticker#FPS}.
   * When setting this property it is clamped to a value between
   * `0` and `Tiny.settings.TARGET_FPMS * 1000`.
   *
   * @member {number}
   * @private
   * @default 10
   */
  get minFPS() {
    return 1000 / this._maxElapsedMS;
  }

  set minFPS(fps) {
    // Clamp: 0 to TARGET_FPMS
    const minFPMS = Math.min(Math.max(0, fps) / 1000, settings.TARGET_FPMS);

    this._maxElapsedMS = 1 / minFPMS;
  }
}
Documentation generated by JSDoc 3.4.3 on Thu May 31 2018 14:40:21 GMT+0800 (CST)