JavaScript | 非同期処理:コールバック – setTimeout の仕組み

JavaScript JavaScript
スポンサーリンク

まず setTimeout を一言でイメージする

setTimeout は、
「この処理を◯ミリ秒“後に”実行してね」とブラウザ(または Node)にお願いする関数
です。

ポイントはここです。

  • setTimeout を呼んだ瞬間に、コールバックの中身が実行されるわけではない
  • 「◯ミリ秒経ったら、このコールバックを“実行候補として予約”する」だけ
  • 実際に実行されるタイミングは、「現在の処理が落ち着いてから」

この「予約」と「実行」の間に、
Web API / タスクキュー / イベントループ が関わってきます。


setTimeout の基本的な使い方

一番シンプルな例

まずは動きだけ見てみます。

console.log("A: 開始");

setTimeout(() => {
  console.log("B: 1秒後の処理");
}, 1000);

console.log("C: 終了");
JavaScript

ほとんどの環境で、出力順はこうなります。

A: 開始
C: 終了
(1秒後)
B: 1秒後の処理

ここで重要なのは、setTimeout のすぐ下に書いた console.log("C") が先に実行される ことです。
「1秒待ってから次の行へ進む」のではありません。

第1引数と第2引数の意味

setTimeout のシグネチャは、ざっくりこうです。

setTimeout(コールバック関数, 待ち時間ミリ秒);
JavaScript

第1引数:あとで実行してほしい処理(コールバック)
第2引数:最低限待ってほしい時間(ミリ秒)

例えば 1000 なら「最低 1 秒後に」コールバックを実行候補として予約する、という意味になります。


setTimeout の裏側で何が起きているか

1. JavaScript は「お願いしたらすぐ次へ進む」

先ほどのコードに沿って、内部の流れを追ってみます。

console.log("A: 開始");

setTimeout(() => {
  console.log("B: 1秒後の処理");
}, 1000);

console.log("C: 終了");
JavaScript

流れはこうです。

  1. コールスタックに「グローバルコード」が乗り、console.log("A") が実行され、「A: 開始」が出る。
  2. setTimeout が呼ばれる。ここで JavaScript エンジンはブラウザの Web API に
    「1秒測って、その後この関数を呼ぶ準備をしておいて」と依頼する。
    setTimeout 自体の仕事はここで終わり。すぐにコールスタックから消える。
  3. すぐ次の行の console.log("C") が実行され、「C: 終了」が出る。
  4. グローバルコードの実行が終わり、コールスタックが空になる。

この時点で、JavaScript 本体は「待っていない」 ことに注目してください。
待っているのは「タイマーを管理している Web API 側」です。

2. 時間を数えるのは Web API の仕事

setTimeout による待ち時間の管理は、
JavaScript エンジンの中ではなく、ブラウザや Node 側のタイマー機構(Web API)が担当します。

Web API 側では、

  1. 指定されたミリ秒をカウントする
  2. 時間が来たら、「このコールバックはもう実行していいよ」とタスクキューに登録する

という仕事をします。

この時点でも、コールバックは「まだ実行されていません」。
ただタスクキューという待ち行列に「並んだだけ」です。

3. 実行するタイミングを決めるのはイベントループ

タスクキューにコールバックが入ると、
イベントループが「次の仕事」としてそれを拾い、コールスタックに乗せて実行します。

ただし、すぐにではなく、

  • コールスタックが空になっていること
  • 先に処理すべきマイクロタスク(Promise など)がないこと

が条件です。

そのため、

「指定の時間が過ぎた瞬間に必ず実行される」ではなく、
「指定時間が過ぎて、かつ JS 側が暇になったタイミングで実行される」
という動きになります。


よくある疑問:「0ミリ秒でも後ろに回る」のはなぜか

0 を指定しても「すぐ」ではない

次のコードを見てください。

console.log("A");

setTimeout(() => {
  console.log("B: timeout 0ms");
}, 0);

console.log("C");
JavaScript

結果は、

A
C
B: timeout 0ms

になります。「0ミリ秒なら A → B → C じゃないの?」と思うかもしれません。

これも仕組みから考えると納得できます。

0ms の意味は「最低でも今の処理が終わってから」

setTimeout(fn, 0) は、

  • 「0ミリ秒待つ」のではなく、「できるだけ早く実行したい」
  • ただし、「今動いているタスク(グローバルコード)が完全に終わったあと」という制約つき

という意味になります。

実際には、

  1. “A” を出力
  2. setTimeout が Web API に「0ms でコールバック登録して」と依頼
  3. “C” を出力
  4. グローバルコード終了 → コールスタックが空になる
  5. すでに 0ms は経過しているので、Web API がコールバックをタスクキューに入れている
  6. イベントループがタスクキューからコールバックを取り出して実行し、”B” を出力

という流れです。

ここが重要です。
setTimeout(..., 0) は「即実行」ではなく、「今の処理が全部終わった“あと”に、次のタスクとして実行」と理解するのが正しい です。


setTimeout と Promise の実行順比較(マイクロタスクとの関係)

どちらが先に実行されるか

次のコードを考えてみます。

console.log("A");

setTimeout(() => {
  console.log("B: timeout");
}, 0);

Promise.resolve().then(() => {
  console.log("C: promise");
});

console.log("D");
JavaScript

結果は、

A
D
C: promise
B: timeout

になることが多いです。

これは、「Promise の then はマイクロタスク」「setTimeout のコールバックは通常のタスク」という違いから来ています。

イベントループの具体的な順番

ざっくり流れを追うとこうです。

  1. “A” を出力
  2. setTimeout → Web API に依頼 → 0ms 後にコールバックをタスクキューに投入準備
  3. Promise.resolve().then(...) → then の中身(C)がマイクロタスクキューに積まれる
  4. “D” を出力
  5. グローバルコード終了 → コールスタックが空
  6. イベントループは、「次のタスクに行く前にマイクロタスクキューを全部処理する」ルールなので、まず C を実行
  7. その後でタスクキューから B を取り出して実行

この順序を支えているのも、setTimeout が「タスクキューにコールバックを渡す(=通常のタスク)」という仕組みで動いているからです。


setTimeout が「ブロッキングを避ける」ための道具になる理由

同じ「待つ」でも、同期的に待つとフリーズする

もし、待ち時間をこう書いてしまったらどうなるでしょう。

console.log("A: 開始");

const start = Date.now();
while (Date.now() - start < 3000) {
  // 3秒間ひたすらループ
}

console.log("B: 終了");
JavaScript

3 秒の間、ブラウザがほぼ固まります。
クリックもスクロールも、他の JavaScript も動きません。

これは、3 秒間ずっとコールスタックの一番上にこの while が乗りっぱなしになる からです。
イベントループは「次のタスクに行きたくても、今のタスクが終わってくれない」状態になります。

setTimeout なら「待っている間も他の処理ができる」

同じ 3 秒待つなら、こう書いた方がはるかに健全です。

console.log("A: 開始");

setTimeout(() => {
  console.log("B: 3秒後の処理");
}, 3000);

console.log("C: 他の処理を続ける");
JavaScript

3 秒間のあいだも、JavaScript は別のコードを実行できます。
UI も反応し続けます。

なぜかというと、

  • 「時間を測る」仕事は Web API 側で進行
  • JavaScript 本体は「コールバックがキューに入ってくるまで」別の処理をして良い
  • 3 秒経った時点で、イベントループが空きを見てそのコールバックを実行する

という分業ができているからです。

ここが重要です。
setTimeout は、シングルスレッドな JavaScript が「時間のかかる待ち」を外に追い出し、ブロッキングを避けるための基本ツール です。


setTimeout にまつわる細かいポイント

実際の実行時間は「指定時間以上」になる

setTimeout(() => {}, 1000) と書いても、
1 秒きっかりに実行されるとは限りません。

  • 指定時間が過ぎた
  • かつ、コールスタックが空いている
  • かつ、先に処理すべきマイクロタスクがない

という条件がそろったときに初めて実行されます。

なので、「最低 1 秒後に、都合がつきしだい実行」というくらいのイメージで捉えましょう。

setInterval と違って「連続実行のタイミング管理」は自前になる

setTimeout は「1 回だけ」の予約です。

setTimeout(() => {
  console.log("1回だけ");
}, 1000);
JavaScript

何度も繰り返したければ、自分の中で再び setTimeout を呼ぶ必要があります。

function repeat() {
  console.log("繰り返し");
  setTimeout(repeat, 1000);
}

repeat();
JavaScript

このように書くと、「前の処理が終わってから 1 秒後」に次が実行される形になり、
setInterval よりも「処理時間込みで間隔を調整しやすい」という利点があります。


初心者として「setTimeout の仕組みをどこまで理解しておくべきか」

最低限押さえておきたいこと

今の段階でしっかり理解しておいてほしいポイントは次のようなものです(文章で整理します)。

setTimeout の第1引数は「あとで実行されるコールバック関数」、第2引数は「最低限待つ時間(ms)」であること。

setTimeout を呼んだ瞬間にコールバックが実行されるわけではなく、「Web API に時間待ちを依頼し、時間が来たらタスクキューにコールバックを入れてもらう」仕組みであること。

実際にコールバックが実行されるのは、「現在のタスクが終わり、コールスタックが空き、マイクロタスクが片付いたあと」であること。

setTimeout(..., 0) は「今すぐ」ではなく、「今の処理が全部終わったあとに、次のタスクとして実行」と理解すること。

同期的な while ループなどで待つと、その間 UI が固まり続けてしまうが、setTimeout を使えば待ち時間を外に出し、メインスレッドを空けておけること。

余裕があれば試してほしい実験

いくつか簡単なコードを自分で書いて、順番を予想 → 実行 → 確認してみると理解が深まります。

console.logsetTimeout(..., 0) を混ぜて、「出力順」を当てる。
Promise.resolve().then(...)setTimeout(..., 0) を混ぜて、「どちらが先に出るか」を考える。
重い while ループと、setTimeout を使った待ち方を比べて、「UI の固まり方」の違いを感じる。

このあたりを一度体験しておくと、
「setTimeout は“時間をずらす”ための道具なんだ」という感覚がかなりクリアになります。


まとめ

setTimeout の本質を一言で言うなら、

「この関数を、◯ミリ秒“後に”実行候補としてタスクキューに登録しておいて、と Web API に頼むための仕組み」
です。

そこから実行に至るまでには、

JavaScript が setTimeout を呼ぶ(その場では何も待たない)
Web API が時間を測り、終わったらコールバックをタスクキューに入れる
イベントループが、コールスタックが空いたタイミングでそのコールバックを実行する

という段階を踏んでいます。

この仕組みを理解しておくと、

「なぜ 0ms でも後ろに回るのか」
「なぜ Promise.then のほうが先に実行されることがあるのか」
「なぜ同期の while ループが UI を殺すのか」

といった疑問が、全部一本の線でつながって見えるようになります。

これから先に学ぶ Promise や async/await も、
結局はこの「イベントループ+キュー+非同期コールバック」の上に成り立っています。

その土台として、setTimeout の仕組みをここまでのレベルで押さえておくのは、とても良い一歩です。

タイトルとURLをコピーしました