JavaScriptのsetTimeoutを使う

setTimeoutは、指定した時間が経過した後に一度だけ関数を実行するJavaScriptの標準的な非同期関数。

勉強がてら、その使い方をざざっとまとめてみました。

目次
    スポンサーリンク

    setTimeoutとは何か

    JavaScriptのsetTimeout関数は、指定した時間(ミリ秒)後に特定の処理を実行するためのタイマー機能。基本的な使い方は以下のとおり。

    setTimeout(function() {
      // ここに実行したいコード
    }, 遅延時間);

    主な特徴は

    • 非同期処理:コードの実行を一時停止せず、指定した時間後にコールバック関数を実行する。
    • 一度だけ実行:指定した遅延時間後に1回だけ実行される。
    • タイマーID:関数を呼び出すと、戻り値としてそのタイマーを識別するためのIDが返される。

    例えば、3秒後にalert表示させたいなら

    setTimeout(function() {
      alert("3秒経過しました!");
    }, 3000);

    これを応用して、クリックしたら2秒後にalert表示させてみる。

    さらに応用して、ページが読み込まれてから10秒後にPRをモーダル表示するなんてことも可能。
    以下は、便宜的にクリックしてから2秒後にモーダル表示させている。

    JavaScriptの非同期関数とは

    普通の関数は、何かを計算したりデータを取得したりするのに時間がかかると、その間待つことになる。で、その待つ時間がもったいないということで、待たずに次のことを進めることができるのが非同期関数。

    setTimeoutは、時間を待っている間に他のコードを実行できるから非同期関数。指定した時間が経過するまでの間に、他の処理を続けることができる。

    例えば、このコード

    console.log("処理を開始します");
    setTimeout(() => {
        console.log("5秒後にこのメッセージが表示されます");
    }, 5000);
    console.log("処理を続けます");

    普通の関数であれば、表示される順番は、
    「処理を開始します」→「5秒後にこのメッセージが表示されます」→「処理を続けます」
    となる。
    最後のconsole.log("処理を続けます")の実行は、「5秒後にこのメッセージが表示されます」の表示後に実行される。

    しかし、setTimeoutは待っている間に他のコードを実行できるから、実際の表示される順番は、
    「処理を開始します」→「処理を続けます」→「5秒後にこのメッセージが表示されます」
    となる。setTimeoutは待っている間に他のコードを実行しているのである。

    これが非同期関数で、非同期処理を利用することで、プログラムの効率を高めることができる。

    普通の非同期関数は、「async」という言葉を頭につけて作り、「await」という言葉を使うと「この作業が終わるまで待ってね」ということができる。

    // 普通の書き方だと、待っている間何もできない
    function 普通のケーキ作り() {
      材料を混ぜる();
      オーブンで焼く(); // ここで30分止まる
      デコレーションする();
    }
    
    // 非同期関数を使うと、待っている間に他のことができる
    async function 非同期ケーキ作り() {
      材料を混ぜる();
      await オーブンで焼く(); // ここで他の作業ができる
      デコレーションする();
    }
    スポンサーリンク

    setTimeoutの引数

    関数に引数を渡すには、setTimeoutの第3引数以降を使用する。

    function greet(name) {
      alert("こんにちは、" + name);
    }
    
    setTimeout(greet, 2000, "太郎"); // 2秒後に「こんにちは、太郎」と表示

    setTimeout の戻り値

    setTimeout は数値の「タイマーID」を返す。これは Promise オブジェクトではなく、単なる識別子。
    非同期関数(async function で宣言された関数)は自動的に Promise を返すが、setTimeout は Promise を返さないので直接awaitすることができない。

    なので、setTimeout を Promise ベースの処理に統合するには、Promise でラップする必要がある。

    function delay(ms) {
      return new Promise(resolve => {
        setTimeout(resolve, ms);
      });
    }
    
    // 使用例
    async function example() {
      console.log("開始");
      await delay(2000);
      console.log("2秒後");
    }
    
    example();

    setTimeoutのキャンセル方法

    返ってきた「タイマーID」を使い、clearTimeoutでタイマーをキャンセルできる。

    const timerId = setTimeout(function() {
      alert("この処理は実行されません");
    }, 2000);
    
    clearTimeout(timerId); // タイマーをキャンセル

    タイマーをキャンセルする点については、一見すると矛盾しているように感じられるが「何かが起きたら〜秒後に処理する」というsetTimeout設計において、「何か別のことが起きたらその処理をキャンセルする」という柔軟性を提供する重要な機能である。

    ユーザー操作に基づくキャンセル

    // 自動保存の例
    let saveTimerId;
    
    function startAutoSave() {
      // 5秒後に保存処理を実行するタイマーを設定
      saveTimerId = setTimeout(function() {
        saveDocument();
      }, 5000);
    }
    
    function cancelAutoSave() {
      // ユーザーが「キャンセル」ボタンを押した場合
      clearTimeout(saveTimerId);
    }
    
    // ユーザーが入力を開始したときに自動保存タイマーを開始
    document.getElementById('editor').addEventListener('input', function() {
      // 既存のタイマーがあればキャンセル(リセット)
      if (saveTimerId) {
        clearTimeout(saveTimerId);
      }
      // 新しいタイマーを開始
      startAutoSave();
    });

    デバウンス(連続した処理の制御)

    // 検索入力のデバウンス例
    let searchTimerId;
    
    document.getElementById('searchInput').addEventListener('input', function() {
      // 既存のタイマーをキャンセル
      if (searchTimerId) {
        clearTimeout(searchTimerId);
      }
      
      // 500ミリ秒の間入力がなければ検索を実行
      searchTimerId = setTimeout(function() {
        performSearch();
      }, 500);
    });

    条件に基づいたキャンセル

    // ログアウト警告の例
    let logoutWarningTimer;
    
    function startSession() {
      // 5分後にログアウト警告を表示
      logoutWarningTimer = setTimeout(function() {
        showLogoutWarning();
      }, 5 * 60 * 1000);
    }
    
    function userActivity() {
      // ユーザーが操作を行った場合、既存の警告タイマーをキャンセル
      clearTimeout(logoutWarningTimer);
      // 新しい警告タイマーを設定
      startSession();
    }
    
    // ユーザーの操作(クリック、キー入力など)を検知
    document.addEventListener('click', userActivity);
    document.addEventListener('keypress', userActivity);

    エラー処理やタイムアウト制御

    // APIリクエストのタイムアウト例
    function fetchData() {
      let timeoutId;
      
      // タイムアウト処理
      const timeoutPromise = new Promise((_, reject) => {
        timeoutId = setTimeout(() => {
          reject(new Error('リクエストがタイムアウトしました'));
        }, 5000);
      });
      
      // 実際のデータ取得
      const fetchPromise = fetch('https://api.example.com/data')
        .then(response => {
          // 成功したらタイムアウトをキャンセル
          clearTimeout(timeoutId);
          return response.json();
        });
      
      // どちらか早い方を採用
      return Promise.race([fetchPromise, timeoutPromise]);
    }

    setTimeoutを繰り返し実行したい場合

    繰り返し処理にはsetIntervalを使うのが一般的だが、setTimeoutを使って再帰的に呼び出す方法もある。
    setTimeoutを使って処理を繰り返すには、関数の中で自分自身を再度呼び出す方法(再帰呼び出し)を使う。

    // 再帰的なsetTimeout
    function repeatMessage() {
      console.log("繰り返し表示");
      setTimeout(repeatMessage, 1000);
    }
    
    repeatMessage();

    この方法では、1回ずつ次のsetTimeoutを呼び出しているため、「前の処理が終わってから次を呼ぶ」という順序となる。つまり、前回の「完了時間」から次の間隔を計測している。

    一方のsetIntervalは、「前の処理が終わってから次を呼ぶ」のではなく、「開始時間」から計測しており、指定した間隔(この場合1000ミリ秒=1秒)で繰り返し処理を実行してくれるもの。

    // serInterval
    setInterval(() => {
      console.log("1秒ごとに表示されます");
    }, 1000);

    setTimeoutとsetInterval

    シンプルなのはsetInterval。より細かい制御が必要なときはsetTimeoutの再帰呼び出し。

    機能setTimeoutsetInterval
    実行回数1回複数回
    制御しやすさ高い(逐次処理可)低い(処理時間の影響あり)

    1. setInterval
    シンプルな定期実行に向いている。処理時間が短く、一定間隔で実行したい場合に最適。

    2. setTimeout
    以下の場合に向いている。
    ・処理時間にばらつきがある
    ・動的に間隔を変えたい
    ・前の処理の完了から次の開始までの時間を一定にしたい

    エラー処理の違い

    1. setInterval
    ・一回のエラーが発生しても繰り返しは続く

    2. setTimeout
    ・エラーが発生すると再帰呼び出しがなくなるため停止する
    ・エラー処理をしっかり行う必要がある

    // setTimeoutでのエラー処理例
    function safeRepeatWithTimeout() {
      try {
        // 何らかの処理(エラーが発生する可能性あり)
        riskyOperation();
        console.log("処理成功");
      } catch (error) {
        console.error("エラーが発生しましたが、続行します:", error);
      }
      
      // エラーがあっても次の実行をスケジュール
      setTimeout(safeRepeatWithTimeout, 1000);
    }

    動的な間隔変更

    1. setInterval
    ・一度設定した間隔は変更できない(停止して再設定が必要)

    2. setTimeout
    ・毎回異なる間隔を設定できるため、動的に変更可能

    // 動的な間隔でsetTimeoutを使う例
    function dynamicRepeat(count = 0) {
      console.log(`${count}回目の実行`);
      
      // 実行回数に応じて間隔を変える(徐々に遅くなる)
      const nextDelay = 1000 + (count * 500);
      console.log(`次は${nextDelay}ミリ秒後に実行します`);
      
      setTimeout(() => dynamicRepeat(count + 1), nextDelay);
    }
    
    // 開始
    dynamicRepeat();

    カウントダウンタイマー実装例

    1. setInterval

    function countdownWithInterval(seconds) {
      let remainingTime = seconds;
      
      // 表示を更新
      console.log(`残り時間: ${remainingTime}秒`);
      
      const timerId = setInterval(function() {
        // 1秒減らす
        remainingTime--;
        
        // 表示を更新
        console.log(`残り時間: ${remainingTime}秒`);
        
        // 終了条件
        if (remainingTime <= 0) {
          clearInterval(timerId);  // 繰り返しを停止
          console.log("カウントダウン終了!");
        }
      }, 1000);
    }
    
    // 10秒からカウントダウン開始
    countdownWithInterval(10);

    2. setTimeout

    function countdownWithTimeout(seconds) {
      let remainingTime = seconds;
      
      // 表示を更新
      console.log(`残り時間: ${remainingTime}秒`);
      
      function tick() {
        // 1秒減らす
        remainingTime--;
        
        // 表示を更新
        console.log(`残り時間: ${remainingTime}秒`);
        
        // 終了条件
        if (remainingTime > 0) {
          // まだ終わっていなければ次の呼び出しをスケジュール
          setTimeout(tick, 1000);
        } else {
          console.log("カウントダウン終了!");
        }
      }
      
      // 最初のtickを1秒後に実行
      setTimeout(tick, 1000);
    }
    
    // 10秒からカウントダウン開始
    countdownWithTimeout(10);

    setTimeoutの遅延時間0

    setTimeout(fn, 0)は関数をできるだけ早く、しかし現在の実行スタックの後に実行するためのテクニック。実際には最小遅延(通常4ms)が適用される。

    console.log("1");
    
    setTimeout(() => {
      console.log("4");
    }, 0);
    
    console.log("2");
    console.log("3");
    
    // 出力:
    // 1
    // 2
    // 3
    // 4

    setTimeout(fn, 0) は一見奇妙に見えるが、実際には多くの実用的なシナリオで活用されている。これは「現在の実行スタックの後に、できるだけ早く実行する」という意味を持つ。

    イベント発火後の状態リセット

    イベント処理後に状態をリセットしたい場合に使用する。

    const button = document.getElementById('submit-button');
    
    button.addEventListener('click', function() {
      // ボタンを無効化
      button.disabled = true;
      
      // フォーム送信など
      submitForm();
      
      // 少し経ってからボタンを再度有効化
      setTimeout(() => {
        button.disabled = false;
      }, 0);
    });

    無限ループの回避

    再帰的な処理で、スタックオーバーフローを防ぐために使う。

    function recursiveFunction(count) {
      console.log(count);
      
      if (count > 0) {
        // 直接再帰呼び出しの代わりに setTimeout を使用
        setTimeout(() => recursiveFunction(count - 1), 0);
      }
    }
    
    recursiveFunction(10000); // スタックオーバーフローを起こさない

    Promiseって何?

    Promiseは「約束」という意味の英語。JavaScriptでは、「今はわからないけど、あとで答えるね」という約束事。

    1. Promiseには結果が出たあとに「then」で成功した時の処理、「catch」で失敗した時の処理を書く。
    2. 非同期関数の中では「await」を使うと、Promiseの結果が出るまで待ってくれる。
    3. 非同期関数は自動的にこのPromiseを返す
    // お菓子屋さんにケーキがあるか聞く関数
    function ケーキを注文する() {
      return new Promise(function(成功したとき, 失敗したとき) {
        // お菓子屋さんが考えている...
        
        if (ケーキがある) {
          成功したとき("はい、ケーキありますよ!");
        } else {
          失敗したとき("すみません、ケーキ売り切れです");
        }
      });
    }
    
    // 使い方
    ケーキを注文する()
      .then(function(メッセージ) {
        console.log("やった!" + メッセージ);  // 成功したとき
      })
      .catch(function(メッセージ) {
        console.log("残念..." + メッセージ);  // 失敗したとき
      });