しばしば大量の入力イベントすべてについてAPIと通信して、最後の結果のみを得たくなる時があります。これはオートコンプリートであったり、画面上から動かないボタンをクリックすると通信が走る機能(こういった機能のを作るとまず間違いなく連打されます)であったりです。こういったものを素朴に毎回のイベントで通信するように作るとサーバーに負荷がかかりやすいです。特に最後の入力に対する通信結果さえあれば十分なのに毎回通信をするのはとても無駄です。これを最適化するのにデバウンスという方法が取れます。
デバウンスは、イベントが発生し続ける間は処理を行わず、イベントが一定時間発生しなくなったときだけ処理を実行する方法です。例えばテキスト入力のオートコンプリート機能にデバウンスを使用すると、ユーザーがタイピングを停止してから一定時間が経過した後にのみ、サジェストの表示を試みるようにできます。これによりタイプごとにサーバーへ不必要なリクエストを送ることを防げます。デバウンスは名前のある処理の方法というだけあって、ライブラリがいくつも作られています。例えば debounce という語で npm を検索すると1000件以上ヒットします。実際に何かにデバウンスを用いるのであればそういったライブラリを使うのが手っ取り早いのですが、せっかくなのでこの記事では自作します。
デバウンス関数の例が次です。このコードでは関数と待機時間を引数にとり、待機時間が経過したら渡された関数を実行し、実行した関数の返り値をPromsieで返します。
// debounce関数の定義。引数として関数(func)と待機時間(waitMilliSec)を受け取る function debounce<T extends (...args: any[]) => any>( func: T, waitMilliSec: number ): (...args: Parameters<T>) => Promise<ReturnType<T>> { let timeoutId: number | undefined; // タイムアウトを管理する変数 // 実行を待たせる関数。待たせた関数を実行した時の返り値をPromiseで返す return (...args: Parameters<T>): Promise<ReturnType<T>> => { return new Promise<ReturnType<T>>((resolve) => { // 待機時間が経過した後に実行される関数 const later = () => resolve(func(...args)); // 元の関数を実行してその結果を解決 clearTimeout(timeoutId); // 既存のタイムアウトをクリアして実行をキャンセル timeoutId = setTimeout(later, waitMilliSec); // 新しいタイムアウトを設定 }); }; } // debounceでラップした関数を用意 const wrapped = debounce(function (msg, idx) { console.log(msg); // 引数の msg を表示して return `これは${idx}の返り値です`; // 引数のidxを含む文字列を返す }, 1000); // then で返り値を処理する使用例 for (let i = 1; i <= 5; i++) { wrapped(`hello${i}`, i) // 約1秒後に"hello5"と"Completed: 5"が出力される .then(function (ret) { console.log(`Completed: ${ret}`); }); } // async/await で返り値を処理する使用例 async function main() { const ret = await wrapped(`hello${i}`, i); // 約1秒後に"hello5"と"Completed: 5"が出力される console.log(`Completed: ${ret}`); } for (let i = 1; i <= 5; i++) { main(); }
デバウンスは大雑把な実装方針であり、ライブラリによってその実装方法と備わっている機能は違います。外からデバウンスの待機時間を変更したり、待機中の処理をキャンセルしたり、複数回の実行をまとめて一つに実行したりなど様々な機能の拡張があります。プログラムを書いていて「○○なデバウンス」が欲しい、となった時、概ねライブラリを探すといい感じのものが見つかりやすいです。見つからなかった作りましょう。
↑の例のコードのJavaScript版
function debounce(func, waitMilliSec) { let timeoutId; return (...args) => { return new Promise((resolve) => { const later = () => resolve(func(...args)); clearTimeout(timeoutId); timeoutId = setTimeout(later, waitMilliSec); }); }; } const wrapped = debounce(function (msg, idx) { console.log(msg); return `これは${idx}の返り値です`; }, 1000); for (let i = 1; i <= 5; i++) { wrapped(`hello${i}`, i) .then(function (ret) { console.log(`Completed: ${ret}`); }); } async function main(i) { const ret = await wrapped(`hello${i}`, i); console.log(`Completed: ${ret}`); } for (let i = 1; i <= 5; i++) { main(i); }