概要
scroll
イベントやresize
イベントなど頻発するイベントに対して、毎回ハンドラーを実行するのが勿体無いし、パフォーマンス面に悪い影響が出る恐れはあるため、それを遅延するような関数が登場する。
基本概念
1. throttleの意味
- 何回に呼ばれても、指定の間隔時間内に最後の呼出だけが実行される
- 指定の間隔時間内に必ず1回が実行される
利用例:scrollイベントハンドラーなどで使用される
解説:スクロール途中にハンドラー処理により画面表示内容が変わるような時に利用するのが適する
つまり、実行する頻度を避けるが、一定の間隔で必ず実装する。
2. debounceの意味
- 何回に呼ばれても、最後の呼出だけが有効になる
- 最後の呼出の遅延時間を経ってたら実行される
- 遅延時間内に新たな呼出があったら、遅延時間がリセットされ、0から数える
- 極端に言うと、遅延時間の直前に連続で新たに呼び出すと永遠に呼ばれなくなる
利用例:サジェストのキーワード入力フィールドにキーワードを変更する時のAPI通信処理
解説:途中のキーワードで取得したデータが必ず捨てられるのでdebounceすることで無駄な通信が避けられる
つまり、相応しい遅延時間を設けることで、途中の連続操作による呼出を全て捨てる。
実装例
react component
がunmount
された後、遅延された処理を呼出しても意味ない場合が多いため、unmount
する前にそれをキャンセルした方が良いのはほとんどだと思われる。従って、キャンセルできる仕組みを設ける。
TypeScriptでの実装例をメモする。
1. throttleの実装例
export const throttle = <Args extends unknown[]>(
fn: (...args: Args) => void,
interval: number,
): [(...args: Args) => void, () => void] => {
// クロージャでタイマーIDと処理時間を覚える
let timerId: ReturnType<typeof setTimeout>;
let lastExecTime = 0;
// 遅延処理を入れて元の関数をラップする
const start = (...args: Args) => {
const currentTime = Date.now();
const execute = () => {
fn(...args);
lastExecTime = Date.now();
};
// 時間になってもtimerが実行されていない場合があるため、念のためクリアする
clearTimeout(timerId);
if (lastExecTime + interval <= currentTime) {
// 前回の実行から指定の間隔時間を経ったら、関数を実行する
execute();
} else {
// 前回の実行から指定の間隔時間を経ってなければ、タイマーを登録する
const newInterval = lastExecTime + interval - currentTime;
timerId = setTimeout(execute, newInterval);
}
};
// 遅延された処理をキャンセル
const cancel = () => {
clearTimeout(timerId);
};
return [start, cancel];
};
2. throttleの実装例
export const debounce = <Args extends unknown[]>(
fn: (...args: Args) => void,
delay: number,
): [(...args: Args) => void, () => void] => {
// クロージャでタイマーIDを覚える
let timerId: ReturnType<typeof setTimeout>;
// 遅延処理を入れて元の関数をラップする
const start = (...args: Args) => {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn(...args);
}, delay);
};
// 遅延された処理をキャンセル
const cancel = () => {
clearTimeout(timerId);
};
return [start, cancel];
};
まとめ
純粋なthrottle
とdebounce
関数を色んな場面で活用することで、無駄な処理をなくして、より効率的な実装ができる。