こんにちは、かつコーチです。
前回は正規表現の基本を解説しました。
今回は、「一定時間後に処理を実行する」「一定間隔で処理を繰り返す」といったタイマー処理を実装できる、setTimeout と setInterval を扱います。
通知の自動非表示やカウントダウン表示など、Webサイトでよく見かける動きの裏側には、たいていこの2つが使われています。
setTimeout:一定時間後に1回だけ実行する
基本の書き方
setTimeout は、指定した時間が経過したあとに、一度だけ処理を実行する関数です。
// 3秒(3000ミリ秒)後にメッセージを表示する
setTimeout(() => {
console.log("3秒経過しました");
}, 3000);
console.log("この行は先に表示される");
実行すると、まず「この行は先に表示される」がすぐに表示され、3秒後に「3秒経過しました」が表示されます。
setTimeout は処理をその場で止めて待つわけではなく、「3秒後に実行する処理」をブラウザに予約しておくだけ、という点が重要です。
このあたりの「JavaScriptは基本的に処理を待たずに次へ進む」という性質については、非同期処理を扱った過去記事とも関連するので、あわせて振り返ってみてください。
実践例:通知メッセージを自動で消す
Webサービスでよく見る「保存しました」のような通知を、数秒後に自動で消す実装です。
<div id="notification" style="display: none;">保存しました</div>
function showNotification(message) {
const notification = document.getElementById("notification");
notification.textContent = message;
notification.style.display = "block";
// 3秒後に自動で非表示にする
setTimeout(() => {
notification.style.display = "none";
}, 3000);
}
showNotification("保存しました");
このパターンは、フォーム送信後の完了メッセージなど、あらゆる場面で使い回せる定番のテクニックです。
setInterval:一定間隔で繰り返し実行する
基本の書き方
setInterval は、指定した間隔で処理を繰り返し実行し続けます。
let count = 0;
// 1秒ごとにカウントアップし続ける
const timerId = setInterval(() => {
count++;
console.log(`${count}秒経過`);
}, 1000);
setTimeout との違いは「1回だけ実行するか、ずっと繰り返すか」という点です。
setIntervalを止める:clearInterval
setInterval は自分から止めない限り、ページを閉じるまで永遠に動き続けます。
止めるためには、setInterval の戻り値(タイマーID)を clearInterval に渡します。
let count = 0;
const timerId = setInterval(() => {
count++;
console.log(`${count}秒経過`);
// 5秒経過したらタイマーを止める
if (count >= 5) {
clearInterval(timerId);
console.log("タイマーを停止しました");
}
}, 1000);
つまずきやすいポイント:タイマーの止め忘れ
ページ遷移後もタイマーが動き続ける
私が実際にやってしまった失敗が、SPA(シングルページアプリケーション)でのタイマーの止め忘れです。
❌ Before:clearIntervalを呼ばずに画面を離れる
function startCountdown() {
let seconds = 10;
setInterval(() => {
seconds--;
console.log(`残り${seconds}秒`);
// このタイマーを止める処理がどこにもない
}, 1000);
}
startCountdown();
// この後、別の画面に遷移してstartCountdownを含む要素が消えても
// setIntervalの処理自体はブラウザ内で動き続けてしまう
カウントダウン機能を実装したページから別の画面に遷移しても、clearInterval を呼んでいなければタイマーは裏で動き続けます。
私はこれに気づかず、同じ画面を何度も出入りするうちに複数のタイマーが同時に走ってしまい、ログにカウントダウンの数値が何重にも表示されるという不具合を出してしまいました。
原因が分からず、最初は「なぜ同じ処理が2回も3回も呼ばれるのか」とかなり悩みました。
コンソールに timerId をログ出力して、実は同じ画面に入るたびに新しいタイマーが増え続けていたことにようやく気づいた、という苦い経験があります。
✅ After:タイマーIDを保持し、不要になったタイミングで確実に止める
let timerId = null;
function startCountdown() {
let seconds = 10;
// 既にタイマーが動いていたら、先に止めてから開始する
if (timerId !== null) {
clearInterval(timerId);
}
timerId = setInterval(() => {
seconds--;
console.log(`残り${seconds}秒`);
if (seconds <= 0) {
stopCountdown();
}
}, 1000);
}
function stopCountdown() {
if (timerId !== null) {
clearInterval(timerId);
timerId = null;
console.log("カウントダウンを停止しました");
}
}
startCountdown();
「タイマーを開始する関数」と「タイマーを止める関数」をセットで用意し、timerId を必ず変数で管理しておくことで、止め忘れを防げます。
Reactなどのフレームワークを使う場合は、コンポーネントが画面から消えるタイミング(クリーンアップ処理)で clearInterval を呼ぶのが定石です。
setTimeoutを止める:clearTimeout
実行前であればキャンセルできる
setTimeout も、実行される前であれば clearTimeout でキャンセルできます。
const timerId = setTimeout(() => {
console.log("この処理は実行されない");
}, 3000);
// 3秒経つ前にキャンセルする
clearTimeout(timerId);
応用:デバウンス処理
clearTimeout を活用した代表的なテクニックが「デバウンス」です。
「入力が止まってから一定時間後に処理を実行する」ことで、検索ボックスの入力補完などで無駄な処理を減らせます。
let debounceTimer = null;
function handleSearchInput(keyword) {
// 直前のタイマーが残っていればキャンセルする
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
console.log(`「${keyword}」で検索を実行`);
// ここで実際の検索処理(API呼び出しなど)を行う
}, 300);
}
// 入力のたびに呼ばれる想定
handleSearchInput("j");
handleSearchInput("ja");
handleSearchInput("java"); // 最終的にこの呼び出しだけが300ms後に実行される
入力のたびに clearTimeout で前のタイマーをキャンセルし直しているため、タイピングが止まってから300ミリ秒後の最後の呼び出しだけが実際に実行されます。
これにより、1文字入力するたびに検索APIを呼び出してしまうような無駄を防げます。
setTimeout(fn, 0)の不思議な挙動
待ち時間が0でもすぐには実行されない
一歩進んだ知識として、setTimeout の待ち時間に 0 を指定しても、他の同期処理より後回しにされるという性質があります。
console.log("1番目");
setTimeout(() => {
console.log("3番目(0ミリ秒後に予約したが、最後に実行される)");
}, 0);
console.log("2番目");
実行結果は「1番目」「2番目」「3番目」の順になります。
これは、setTimeout で予約された処理が、通常の同期処理がすべて終わったあとの「タスクキュー」という場所に一旦積まれてから実行される仕組みになっているためです。
「0ミリ秒後」と書いても「今すぐ」を意味するわけではない、という点は覚えておくとよいでしょう。
このあたりの実行順序が絡むエラーは、初心者がよくつまずくポイントの一つでもあります。
まとめ
この記事のポイント
setTimeoutは一定時間後に1回だけ、setIntervalは一定間隔で繰り返し実行するsetIntervalはclearIntervalで明示的に止めないと動き続けてしまう- タイマーIDを変数で管理し、開始・停止をセットで実装することで止め忘れを防げる
clearTimeoutを活用した「デバウンス」は、検索入力などの無駄な処理を減らす定番テクニックsetTimeout(fn, 0)を指定しても、同期処理より後回しに実行される
次に読むべき記事
次回は、コンソールに表示される「Uncaught TypeError」のような、JavaScriptのよくあるエラーの読み方を解説していきます。
エラーメッセージを正しく読めるようになると、デバッグの速度が大きく変わります。
→ 次の記事:よくあるJavaScriptエラーの読み方(Uncaught TypeError等)