フロントエンド開発の過程で、多くの場合、リサイズ、スクロール、キーダウンなどのイベントの継続的なトリガに遭遇するいくつかの回、このようなイベントをトリガするようなAjaxリクエスト、時間のかかる演算、ページのレンダリングなどを持っている、イベントの継続的なトリガの過程にされたくないので、頻繁に関数を実行します。一般的に言えば、アンチシェイクとスロットルがより良いソリューションです。
ヒント:アンチシェイクもスロットリングも、関数内で変数への参照を一定に保つためにクロージャを使用します。 もうひとつ注意すべき点は、thisと引数の受け渡しです。
ブレ防止
アンチシェイクとは、イベントがトリガーされてからnミリ秒後に関数を実行し、その間に再度トリガーされた場合、関数を再計算してイベントを実行することです。
function debounce (func, wait) {
let timeout;
return function () {
/*
注意:setTimeoutはarrow関数を使っているが、これは必要ない。
const context = this;
this現在のコンテキストを指す
*/
const args = arguments;
// イベントがトリガーされるたびにタイマーはクリアされ、関数の実行時間を再計算するために新しいタイマーが生成される。
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
/*
注:applyはこれを手動でバインドする。
これをしないと、funcのthisは関数が宣言されたコンテキストを指すことになる。
*/
func.apply(this, args);
}, wait);
};
}
即時実装バージョン:
// leading = true 即時実行
function debounce (func, wait, leading) {
let timeout;
return function () {
const args = arguments;
if (timeout) clearTimeout(timeout);
if (leading) {
if (!timeout) func.apply(this, args);
timeout = setTimeout(() => {
func.apply(this, args);
timeout = null;
}, wait);
} else {
timeout = setTimeout(() => {
func.apply(this, args);
}, wait);
}
};
}
スロットリング
スロットリングとは、連続してトリガーされたイベントがnミリ秒に1回だけ実行されることを意味し、スロットリングは関数の実行頻度を希薄化します。複数の実行を最後の1回に変更するアンチディザリングとは異なり、スロットリングは複数の実行を1回おきに変更します
スロットリングは通常、タイムスタンプ版とタイマー版の2つの方法で実現できます。
タイムスタンプ版:
function throttle (func, wait) {
let prev = 0;
return function () {
const now = new Date();
const args = arguments;
if (now - prev > wait) {
func.apply(this, args);
prev = now;
}
};
}
タイマー版:
function throttle (func, wait) {
let timeout;
return function () {
const args = arguments;
if (timeout) return;
timeout = setTimeout(() => {
func.apply(this, args);
timeout = null;
}, wait);
};
}
タイムスタンプ版とタイマー版の違いは、タイマーは最初にトリガーされた時に即座に実行されるのではなく、ある期間の終わりに一度だけ遅延して実行されることです。タイムスタンプ版は初回に即座に実行され、最後には実行されません。
タイムスタンプ版もタイマー版も単独では欠陥があり、最初のトリガーで即座に関数を実行し、最後に1回実行することを好みます。また、連続的にトリガーされた場合、nミリ秒ごとに偶数回実行されます。
インタースタンプのバージョンとタイマーを組み合わせると、次のようなテストバージョンが得られます。
function throttle(func, wait) {
let timeout;
let prev = 0;
return function () {
const args = arguments;
const now = new Date();
if ( now - prev > wait) {
func.apply(this, args);
prev = now
} else {
if(!timeout) {
timeout = setTimeout(() => {
func.apply(this, args);
timeout = null;
prev = new Date() // なお、ここではprevは使えない。= now これはクロージャである
}, wait)
}
}
}
}
連続したトリガーの場合、関数の実行時間が連続した区間 n であることを保証する方法がないこと、そしてカスタマイザーに残りの実行時間が与えられるべきであることは容易にわかります。
function throttle (func, wait) {
let timeout;
let prev = 0;
return function () {
const args = arguments;
const now = new Date();
const remaining = wait - (now - prev); //残り
/*
注意:remainingを<= 0
なぜなら=0関数が適切なタイミングでトリガーされれば、即座に実行される。
この時、タイマーもちょうど実行時間に設定されているので、2回連続して
*/
if (remaining < 0) { // 最初のトリガーは即座に実行される。
func.apply(this, args);
prev = now;
} else {
if (!timeout) {
timeout = setTimeout(() => {
func.apply(this, args);
timeout = null;
prev = new Date();
}, remaining);
}
}
};
}
これは完璧に実現されています。
/*
func (Function): ファンクションをスロットルする。
[wait=0] (number): ミリ秒をスロットルする必要がある。
[options={}] (Object): オプション・オブジェクト
[options.leading=true] (boolean): スロットリングが始まる前に呼び出されるように指定する。
[options.trailing=true] (boolean): スロットルの終了時に呼び出されるように指定する。
*/
function throttle (func, wait, { leading = true, trailing = true } = {}) {
let timeout;
let prev = 0;
return function () {
const now = new Date();
const remaining = wait - (now - prev);
const args = arguments;
if (remaining < 0 && leading) {
func.apply(this, args);
prev = now;
} else if (!timeout && trailing) {
timeout = setTimeout(() => {
func.apply(this, args);
timeout = null;
prev = new Date();
}, trailing ? (leading ? remaining : wait) : remaining);
}
};
}
間違いがあれば遠慮なく指摘してください。




