Polymer.Debouncerを使って任意のタスクを非同期に実行する

ドキュメント本文では軽く言及される程度でほとんど解説はないですが、とても有用なテクニックです。
(注意)この記事はPolymer 2.0をベースに解説しています。Polymer 1.0以前とはAPIが大きく変わっているので注意してください。
## やりたいこと
特定のタスクを任意のタイミングで非同期に実行する。
例えば以下のようなタイミングで：

- 一定時間経過後に
- ブラウザのタスクキューが空になったら
- フレームレート(FPS)の合間に
- 現在実行中のタスクが終わり次第

## 方法
[Polymer.Debouncer](https://github.com/Polymer/polymer/blob/v2.0.0-rc.2/lib/utils/debounce.html)と[Polymer.Async](https://github.com/Polymer/polymer/blob/v2.0.0-rc.2/lib/utils/async.html)を使えば任意のタスクを非同期にスケジューリングして実行できます。
以下は、タイマーを使って5秒経過後任意のコールバックをする例です。

~~~javascript
class SampleElement extends Polymer.Element {
  ...
  this._debouncer = Polymer.Debouncer.debounce(
                         // Deboucerオブジェクトを指定
                         this._debouncer,

                         // Polymer.Asyncを使って実行条件を指定
                         Polymer.Async.timeOut.after(5000),

                         // コールバックを指定
                         this.callback()
                       );
  callback(){
    // 実行したい処理
  }
 }
}
~~~
`Debouncer`オブジェクトは`bouncer`メソッドを呼び出す度に、インスタンスを新規に生成し、すでにインスタンスを生成していている場合には、アクティブなタスクをキャンセルします。

`Polymer.Async`には、タイマー処理以外にも以下のような条件を指定できます。

|メソッド|タイミング|利用場面|
|:---|:---|:---|
|timeOut.after|≒setTimeout<br>指定した時間が経過した後に実行|タスクをタイマー処理したい場合|
|animationFrame|≒requestAnimationFrame<br>FPSを目安に負荷を調整しながらフレームレートの合間に実行|アニメーションを実装する場合|
|idlePeriod|≒requestIdleCallback<br>ブラウザのタスクキューが空になったタイミングで実行|優先度の低いタスクを実行する場合|
|microTask|micro timing(※)で実行|優先度の高いタスクを指定する場合|

(※)microtask timingとは？
公式ドキュメントで以下のように説明されています。
> after the current method finishes, but before the next event from the event queue is processed

要は、現在実行中のタスクが終わり次第すぐに実行するということです。

## 参考
### Stackoverflowの投稿
http://stackoverflow.com/questions/42773171/debouncer-in-polymer-2-0

### Polymer.DebouncerとPolymer.Asyncの内部実装

~~~html:polymer/lib/utils/async.html
<!--
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="boot.html">
<link rel="import" href="mixin.html">
<!-- タスクのスケジューリングにasyncモジュールを使っています。 -->
<link rel="import" href="async.html">

<script>
(function() {
  'use strict';
  /** @typedef {{run: function(function(), number=):number, cancel: function(number)}} */
  let AsyncModule; // eslint-disable-line no-unused-vars
  class Debouncer {
    constructor() {
      this._asyncModule = null;
      this._callback = null;
      this._timer = null;
    }
    /**
     * Sets the scheduler; that is, a module with the Async interface,
     * a callback and optional arguments to be passed to the run function
     * from the async module.
     *
     * @param {!AsyncModule} asyncModule
     * @param {function()} callback
     */
    // debounceメソッドの内部で呼び出される
    setConfig(asyncModule, cb) {
      this._asyncModule = asyncModule;
      this._callback = cb;
      this._timer = this._asyncModule.run(() => {
        this._timer = null;
        this._callback()
      });
    }
    /**
     * Cancels an active debouncer and returns a reference to itself.
     */
    cancel() {
      if (this.isActive()) {
        this._asyncModule.cancel(this._timer);
        this._timer = null;
      }
    }
    /**
     * Flushes an active debouncer and returns a reference to itself.
     */
    flush() {
      if (this.isActive()) {
        this.cancel();
        this._callback();//コールバックを呼び出して終了
      }
    }
    /**
     * Returns true if the debouncer is active.
     *
     * @return {boolean}
     */
    isActive() {
      return this._timer != null;
    }
  /**
   * Creates a debouncer if no debouncer is passed as a parameter
   * or it cancels an active debouncer otherwise.
   *
   * @param {Polymer.Debouncer?} debouncer
   * @param {!AsyncModule} asyncModule
   * @param {function()} cb
   * @return {!Debouncer}
   */
    static debounce(debouncer, asyncModule, cb) {
      if (debouncer instanceof Debouncer) {
        //すでにインスタンス化されていれば呼び出すごとにキャンセルされる
        debouncer.cancel();
      } else {
        //インスタンス化されていなければ作成(呼び出しごとにユニークなオブジェクトが生成される)
        debouncer = new Debouncer();
      }
      // インターフェイスを介してasyncModuleとコールバックを設定
      debouncer.setConfig(asyncModule, cb);
      return debouncer;//オブジェクトを返す
    }
  }
  Polymer.Debouncer = Debouncer;
})();
</script>
~~~

~~~html:polymer/lib/utils/async.html
<!--
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="boot.html">

<script>
(function() {
  'use strict';
  /** @typedef {{run: function(function(), number=):number, cancel: function(number)}} */
  let AsyncInterface; // eslint-disable-line no-unused-vars
  // Microtask implemented using Mutation Observer
  let microtaskCurrHandle = 0;
  let microtaskLastHandle = 0;
  let microtaskCallbacks = [];
  let microtaskNodeContent = 0;
  let microtaskNode = document.createTextNode('');
  new window.MutationObserver(microtaskFlush).observe(microtaskNode, {characterData: true});
  function microtaskFlush() {
    const len = microtaskCallbacks.length;
    for (let i = 0; i < len; i++) {
      let cb = microtaskCallbacks[i];
      if (cb) {
        try {
          cb();
        } catch (e) {
          setTimeout(() => { throw e });
        }
      }
    }
    microtaskCallbacks.splice(0, len);
    microtaskLastHandle += len;
  }
  /**
   * Module that provides a number of strategies for enqueuing asynchronous
   * tasks.  Each sub-module provides a standard `run(fn)` interface that returns a
   * handle, and a `cancel(handle)` interface for canceling async tasks before
   * they run.
   *
   * @namespace
   * @memberof Polymer
   * @summary Module that provides a number of strategies for enqueuing asynchronous
   * tasks.
   */
  Polymer.Async = {
    /**
     * Async interface wrapper around `setTimeout`.
     *
     * @namespace
     * @memberof Polymer.Async
     * @summary Async interface wrapper around `setTimeout`.
     */
    // ≒ setTimeout
    // 指定した時間が経過した後に実行
    timeOut: {
      /**
       * Returns a sub-module with the async interface providing the provided
       * delay.
       *
       * @memberof Polymer.Async.timeOut
       * @param {number} delay Time to wait before calling callbacks in ms
       * @return {AsyncInterface} An async timeout interface
       */
      after(delay) {
        return  {
          run(fn) { return setTimeout(fn, delay) },
          cancel: window.clearTimeout.bind(window)
        }
      },
      /**
       * Enqueues a function called in the next task.
       *
       * @memberof Polymer.Async.timeOut
       * @param {Function} fn Callback to run
       * @return {*} Handle used for canceling task
       */
      run: window.setTimeout.bind(window),
      /**
       * Cancels a previously enqueued `timeOut` callback.
       *
       * @memberof Polymer.Async.timeOut
       * @param {*} handle Handle returned from `run` of callback to cancel
       */
      cancel: window.clearTimeout.bind(window)
    },
    /**
     * Async interface wrapper around `requestAnimationFrame`.
     *
     * @namespace
     * @memberof Polymer.Async
     * @summary Async interface wrapper around `requestAnimationFrame`.
     */
    // ≒ requestAnimationFrame
    // アニメーション実装向け。FPSを目安に負荷に応じて描画の合間に実行。
    animationFrame: {
      /**
       * Enqueues a function called at `requestAnimationFrame` timing.
       *
       * @memberof Polymer.Async.timeOut
       * @param {Function} fn Callback to run
       * @return {*} Handle used for canceling task
       */
      run: window.requestAnimationFrame.bind(window),
      /**
       * Cancels a previously enqueued `animationFrame` callback.
       *
       * @memberof Polymer.Async.timeOut
       * @param {*} handle Handle returned from `run` of callback to cancel
       */
      cancel: window.cancelAnimationFrame.bind(window)
    },
    /**
     * Async interface wrapper around `requestIdleCallback`.  Falls back to
     * `setTimeout` on browsers that do not support `requestIdleCallback`.
     *
     * @namespace
     * @memberof Polymer.Async
     * @summary Async interface wrapper around `requestIdleCallback`.
     */
    // ≒ requestIdleCallback
    // 優先度の低いタスク向け。タスクキューが空になったタイミングで実行
    idlePeriod: {
      /**
       * Enqueues a function called at `requestIdleCallback` timing.
       *
       * @memberof Polymer.Async.timeOut
       * @param {Function} fn Callback to run
       * @return {*} Handle used for canceling task
       */
      run(fn) {
        return window.requestIdleCallback ?
          window.requestIdleCallback(fn) :
          window.setTimeout(fn, 16);
      },
      /**
       * Cancels a previously enqueued `idlePeriod` callback.
       *
       * @memberof Polymer.Async.timeOut
       * @param {*} handle Handle returned from `run` of callback to cancel
       */
      cancel(timer) {
        return window.cancelIdleCallback ?
          window.cancelIdleCallback(timer) :
          window.clearTimeout(timer);
      }
    },
    /**
     * Async interface for enqueueing callbacks that run at microtask timing.
     *
     * Note that microtask timing is achieved via a single `MutationObserver`,
     * and thus callbacks enqueued with this API will all run in a single
     * batch, and not interleaved with other microtasks such as promises.
     * Promises are avoided as an implementation choice for the time being
     * due to Safari bugs that cause Promises to lack microtask guarantees.
     *
     * @namespace
     * @memberof Polymer.Async
     * @summary Async interface for enqueueing callbacks that run at microtask
     *   timing.
     */
    // microtask timingで実行
    // microtask timingは、公式ドキュメントで以下のように説明されています。
    // > after the current method finishes, but before the next event from the event queue is processed
    // 要は実行中のタスクが終わり次第すぐに実行されるということです。
    // idlePeriodとは対照的に、優先的に実行したいタスク向けといえます。
    microTask: {
      /**
       * Enqueues a function called at microtask timing.
       *
       * @memberof Polymer.Async.timeOut
       * @param {Function} fn Callback to run
       */
      run(callback) {
        microtaskNode.textContent = microtaskNodeContent++;
        microtaskCallbacks.push(callback);
        return microtaskCurrHandle++;
      },
      /**
       * Cancels a previously enqueued `microTask` callback.
       *
       * @param {*} handle Handle returned from `run` of callback to cancel
       */
      cancel(handle) {
        const idx = handle - microtaskLastHandle;
        if (idx >= 0) {
          if (!microtaskCallbacks[idx]) {
            throw new Error('invalid async handle: ' + handle);
          }
          microtaskCallbacks[idx] = null;
        }
      }
    }
  };
})();
</script>
~~~