Debounce 実装(簡易) — 入力が落ち着いたら一度だけ実行
「キー入力やスクロールなど“連打されるイベント”を落ち着いてから1回だけ処理したい」—その定番テクが debounce。短いコードで、無駄な処理やAPI呼び出しを抑えられます。
基本の実装と挙動
// 簡易 debounce 実装
const debounce = (fn, ms) => {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), ms);
};
};
JavaScript- 仕組み: 呼び出されるたびに前のタイマーを取消し、最後の呼び出しから ms 経過したら実行。
- 効果: 高頻度イベントでも「最後の1回」だけが走る。APIや重い計算の呼び出し回数を削減。
例題:入力補完 API を呼ぶのは“手が止まってから”
// 入力が止まって300ms経ったら検索する
const search = (q) => {
console.log("API検索:", q);
// fetch(`/api/search?q=${encodeURIComponent(q)}`) ...
};
const onInput = debounce(search, 300);
document.querySelector("#q").addEventListener("input", (e) => {
onInput(e.target.value);
});
JavaScript- ラベル: 連続入力中は実行されず、最後のキー入力から300ms待って1回だけ検索。
すぐ使えるテンプレート集
スクロール末尾で処理
const handleScrollEnd = debounce(() => {
console.log("スクロールが一旦終了");
}, 200);
window.addEventListener("scroll", handleScrollEnd);
JavaScriptリサイズ完了時のみレイアウト再計算
const recalcLayout = debounce(() => {
console.log("レイアウト再計算");
}, 250);
window.addEventListener("resize", recalcLayout);
JavaScriptフォームのオートセーブ(入力が途切れたら保存)
const autoSave = debounce((data) => {
console.log("保存:", data);
// save(data)
}, 500);
// 値の変化時に呼ぶ想定
autoSave({ title: "ドラフト", body: "..." });
JavaScript実務でのコツと拡張版
this を保つ/キャンセル機能を付ける
function debounceEx(fn, ms = 300) {
let t;
const d = function (...args) {
const ctx = this;
clearTimeout(t);
t = setTimeout(() => fn.apply(ctx, args), ms);
};
d.cancel = () => clearTimeout(t); // 途中で無効化
return d;
}
JavaScript- ラベル: クラスやオブジェクトメソッドで使うなら
applyで this を維持。不要になったらcancel()。
先頭で一度だけ実行(leading)か末尾(trailing)か
function debounceAdv(fn, ms = 300, { leading = false } = {}) {
let t, called = false;
return (...args) => {
if (leading && !called) {
fn(...args);
called = true;
}
clearTimeout(t);
t = setTimeout(() => {
if (!leading) fn(...args); // trailing
called = false; // ウィンドウ終了でリセット
}, ms);
};
}
JavaScript- ラベル: 先頭で一度(leading)か、最後だけ(trailing)かを選べると実用度アップ。
よくある落とし穴と対策
- ラベル: 非同期を勘違い
- Debounceは「遅らせる」だけ。先頭で即時実行したい場合は leading オプションを使う。
- ラベル: 連打で永遠に実行されない
- ms の間ずっと呼ばれ続けると末尾実行が来ない。UI的に必要なら leading を併用。
- ラベル: this が失われる
- メソッドに直接適用すると
thisが変わる。applyで明示的に束縛する拡張版を使う。
- メソッドに直接適用すると
- ラベル: 取り消し忘れ
- ページ離脱やコンポーネント破棄時に未実行のタイマーが残る。
cancel()を呼ぶかclearTimeoutする。
- ページ離脱やコンポーネント破棄時に未実行のタイマーが残る。
練習問題(手を動かして覚える)
// 1) 入力停止後500msでログ
const logInput = debounce((v) => console.log("入力:", v), 500);
// 連続で呼ぶ(実際は input イベントで)
logInput("a"); logInput("ab"); logInput("abc"); // → 最後の "abc" だけ出る
// 2) leading 一度+末尾なし(連打の最初だけ)
const clickOnce = debounceAdv(() => console.log("最初だけ"), 1000, { leading: true });
clickOnce(); clickOnce(); clickOnce(); // 最初の1回だけ出る(1秒経過でリセット)
// 3) メソッドの this を保つ
const counter = {
n: 0,
inc() { this.n++; console.log(this.n); }
};
counter.inc = debounceEx(counter.inc, 300);
counter.inc(); counter.inc(); counter.inc(); // → 1(最後の1回だけ、thisは維持)
JavaScript直感的な指針
- 高頻度イベントは“落ち着いたら1回”にするのが基本。
- まずは簡易版で十分。this維持やleadingが必要なら拡張版にする。
- 破棄時はキャンセルを忘れない。APIや計算は控えめに、UIはなめらかに。
