Throttle decorator
“スロットリング” デコレータ throttle(f, ms)
を作ります – これはラッパーを返し、ms
ミリ秒毎に最大一度 f
の呼び出しを渡します。“クールダウン” 期間に入る呼び出しは無視されます。
debounce
との違いは – もし無視された呼び出しがクールダウン中の最後のものであれば、遅延の終わりにそれを実行します。
実際のアプリケーションを確認して、要件のよりよい理解とそれがどこから来るのかを見てみましょう。
例えば、マウスの動きを追跡したいと思います。
ブラウザでは、マウスのマイクロレベルの動きに対して実行される関数を設定し、移動に応じたポインタの場所を取得する事ができます。マウス使用中、この関数は通常とても頻繁に実行され、1秒あたり100回(10ミリ秒毎)程度になります。
この追跡関数は web ページ上の一部の情報を更新する必要があります。
更新を行う関数 update()
はマイクロレベルの移動で実行するには重すぎます。また、100ms に1回より多くの頻度で実行しても意味がありません。
従って、オリジナルの update()
の代わりにマウス移動毎に実行する関数として throttle(update, 100)
を割り当てます。このデコレータは頻繁に呼ばれても、update()
が呼ばれるのは 100ms 毎に最大一回です。
視覚的に、次のようになります:
- 最初のマウスの移動に対して、デコレートされたバリアントは
update
へ呼び出しを渡します。これは重要で、ユーザは自身の移動に対するリアクションがすぐに見えます。 - その後、マウスが移動するのに対し
100ms
まで何も起こりません。デコレータは呼び出しを無視します。 100ms
が経過すると – 最後の座標でもう一度update
が発生します。- そして最終的に、マウスはどこかで停止します。デコレートされたバリアントは
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
を返します。
- 最初の呼び出しの間、
wrapper
は単にfunc
を実行し、クールダウンの状態を設定します(isThrottled = true
)。 - この状態では、すべての呼び出しは
savedArgs/savedThis
に記憶されます。コンテキストと引数両方とも等しく重要で、覚えておく必要があることに注意してください。呼び出しを再現するために、それらが同時に必要になります。 - …次に
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;
}