レッスンに戻る

Throttle decorator

重要性: 5

“スロットリング” デコレータ throttle(f, ms) を作ります – これはラッパーを返し、ms ミリ秒毎に最大一度 f の呼び出しを渡します。“クールダウン” 期間に入る呼び出しは無視されます。

debounce との違いは – もし無視された呼び出しがクールダウン中の最後のものであれば、遅延の終わりにそれを実行します。

実際のアプリケーションを確認して、要件のよりよい理解とそれがどこから来るのかを見てみましょう。

例えば、マウスの動きを追跡したいと思います。

ブラウザでは、マウスのマイクロレベルの動きに対して実行される関数を設定し、移動に応じたポインタの場所を取得する事ができます。マウス使用中、この関数は通常とても頻繁に実行され、1秒あたり100回(10ミリ秒毎)程度になります。

この追跡関数は web ページ上の一部の情報を更新する必要があります。

更新を行う関数 update() はマイクロレベルの移動で実行するには重すぎます。また、100ms に1回より多くの頻度で実行しても意味がありません。

従って、オリジナルの update() の代わりにマウス移動毎に実行する関数として throttle(update, 100) を割り当てます。このデコレータは頻繁に呼ばれても、update() が呼ばれるのは 100ms 毎に最大一回です。

視覚的に、次のようになります:

  1. 最初のマウスの移動に対して、デコレートされたバリアントは update へ呼び出しを渡します。これは重要で、ユーザは自身の移動に対するリアクションがすぐに見えます。
  2. その後、マウスが移動するのに対し 100ms まで何も起こりません。デコレータは呼び出しを無視します。
  3. 100ms が経過すると – 最後の座標でもう一度 update が発生します。
  4. そして最終的に、マウスはどこかで停止します。デコレートされたバリアントは 100ms の期限まで待ち、その後最後の座標で update を実行します。従って、恐らく最も重要な最後のマウス座標は処理されます。

コード例:

function f(a) {
  console.log(a)
};

// f1000 は、1000ms 毎に最大1回 f へ呼び出しを渡します
let f1000 = throttle(f, 1000);

f1000(1); // shows 1
f1000(2); // (throttling, 1000ms not out yet)
f1000(3); // (throttling, 1000ms not out yet)

// when 1000 ms time out...
// ...outputs 3, intermediate value 2 was ignored

P.S. f1000 に渡される引数とコンテキスト this はオリジナルの f に渡される必要があります。

テストと一緒にサンドボックスを開く

function throttle(func, ms) {

  let isThrottled = false,
    savedArgs,
    savedThis;

  function wrapper() {

    if (isThrottled) { // (2)
      savedArgs = arguments;
      savedThis = this;
      return;
    }

    func.apply(this, arguments); // (1)

    isThrottled = true;

    setTimeout(function() {
      isThrottled = false; // (3)
      if (savedArgs) {
        wrapper.apply(savedThis, savedArgs);
        savedArgs = savedThis = null;
      }
    }, ms);
  }

  return wrapper;
}

throttle(func, ms) の呼び出しは wrapper を返します。

  1. 最初の呼び出しの間、wrapper は単に func を実行し、クールダウンの状態を設定します(isThrottled = true)。
  2. この状態では、すべての呼び出しは savedArgs/savedThis に記憶されます。コンテキストと引数両方とも等しく重要で、覚えておく必要があることに注意してください。呼び出しを再現するために、それらが同時に必要になります。
  3. …次に ms ミリ秒が経過後、setTimeout がトリガーをかけます。クールダウン状態が削除されます(isThrottled = false)。そして無視された呼び出しがあった場合には、最後に記憶した引数とコンテキストで wrapper が実行されます。

3つ目のステップは func ではなく wrapper を実行します。なぜなら、私たちは func を実行するだけではなく、再びクールダウン状態に入り、それをリセットするためのタイムアウトを設定する必要があるためです。

function throttle(func, ms) {

  let isThrottled = false,
    savedArgs,
    savedThis;

  function wrapper() {

    if (isThrottled) {
      // memo last arguments to call after the cooldown
      savedArgs = arguments;
      savedThis = this;
      return;
    }

    // otherwise go to cooldown state
    func.apply(this, arguments);

    isThrottled = true;

    // plan to reset isThrottled after the delay
    setTimeout(function() {
      isThrottled = false;
      if (savedArgs) {
        // if there were calls, savedThis/savedArgs have the last one
        // recursive call runs the function and sets cooldown again
        wrapper.apply(savedThis, savedArgs);
        savedArgs = savedThis = null;
      }
    }, ms);
  }

  return wrapper;
}

サンドボックスでテストと一緒に解答を開く