JavaScript | 非同期処理:非同期の基礎概念 – タスクキュー

JavaScript JavaScript
スポンサーリンク

まずタスクキューを一言でイメージする

タスクキューは、
「あとで実行する処理(コールバック)を順番に並べておく待ち行列」 です。

JavaScript はシングルスレッドなので、「今」実行できるのは 1 つだけ。
でも setTimeoutfetch、イベントリスナーなどで「あとで実行して」と予約された処理がたくさんあります。

それらを、
「終わった順」「発生した順」に並べて保管しておく場所が タスクキュー です。
そして、イベントループが「コールスタックが空いたタイミング」でタスクキューから 1 件ずつ取り出し、実行していきます。

ここが重要です。
タスクキューは「非同期処理のコールバックが“順番待ち”している場所」。
これを理解すると「なぜこの順番で実行されるの?」という疑問の多くがスッキリします。


タスクキューが使われる流れ(全体像)

登場人物のおさらい

非同期処理の世界には、ざっくりこういう役者がいます。

JavaScript エンジン(実際にコードを実行する)
コールスタック(今実行中の関数の積み重ね)
Web API(タイマー・通信・イベントなど「待つ仕事」を担当)
タスクキュー(「終わったら実行してね」という処理の待ち行列)
イベントループ(「スタックが空いたかな?」と見て、キューから次の処理を持ってくる係)

このうち、タスクキューは 「Web API が送り込んだコールバックを貯めておく場所」 です。

典型的な流れを一度言葉だけで追ってみる

  1. JavaScript が setTimeoutfetch を呼び出す
  2. Web API がその「待つ仕事」を引き受ける(時間を測る・ネットワークする・イベントを待つ)
  3. 「準備できたよ」「完了したよ」というタイミングで、Web API が「このコールバックをタスクキューに入れておいて」と依頼する
  4. イベントループが、コールスタックが空いたタイミングでタスクキューを見に行き、先頭のものを取り出して実行する

つまり、
「非同期処理が終わった瞬間に即実行」ではなく、
一度タスクキューを経由して「スタックが空いたタイミングで順番に実行」される

というのがポイントです。


具体例1:setTimeout とタスクキュー

コードと出力順

console.log("A");

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

console.log("C");
JavaScript

実行すると、必ず

A
C
B

の順に出力されます。
「0ミリ秒なのに、なぜ B が最後なの?」という疑問が、タスクキューを理解すると腑に落ちます。

裏側で何が起きているか

  1. コールスタックに「グローバルコード」が積まれ、console.log("A") が実行される
  2. setTimeout が呼ばれ、「0ミリ秒後にこのコールバックを実行して」と Web API に依頼する
    ここで setTimeout の処理は終わり、コールスタックから消える
  3. console.log("C") が実行される
  4. グローバルコードの実行が終わり、コールスタックが空になる
  5. 一方、Web API 側では「0ミリ秒」のタイマーが終わっており、「コールバックをタスクキューに投入」している
  6. イベントループが、コールスタックが空になったのを見て、タスクキューの先頭(このコールバック)を取り出し、コールスタックに積んで実行
  7. console.log("B") が実行される

ここが重要です。
setTimeout(..., 0) は「今すぐ実行」ではなく、
「今の処理(現在のコールスタック)が全部終わってから、タスクキュー経由で一番最初に実行される」 という意味だと理解すると、挙動がとても自然に見えてきます。


具体例2:イベントリスナーとタスクキュー

ボタンクリックのコード

const button = document.querySelector("#btn");

button.addEventListener("click", () => {
  console.log("クリックされた");
});

console.log("イベントリスナーが登録されました");
JavaScript

このコードでは、
ユーザーがボタンをクリックすると "クリックされた" が出力されます。

実際の流れ

  1. JavaScript が addEventListener を呼び、「クリックが起きたらこの関数を実行してね」と Web API に登録
  2. 一度登録してしまえば、JavaScript 側は何もしていない(ずっと待っているわけではない)
  3. ユーザーが実際にクリックすると、ブラウザのネイティブコードがそれを検知
  4. Web API が「クリックが起きたよ、このコールバックをタスクキューに入れて」とイベントループに渡す
  5. コールスタックが空いたタイミングで、イベントループがそのコールバックを実行
  6. "クリックされた" が出力される

重要なのは、クリックイベントのコールバックも「タスクキューに入って、順番待ちしてから実行される」 ということです。
たとえユーザーが爆速で連打しても、
メインスレッドが空いたタイミングで、キューから順にイベントが処理されていきます。


具体例3:複数のタスクがキューに並ぶとどうなるか

ちょっと複雑な例

console.log("1");

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

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

console.log("4");
JavaScript

このコードの出力順は、多くの環境で

1
4
2: timeout 0ms
3: timeout 10ms

のようになります(正確な順序はブラウザのタイマー実装に依存する部分もありますが、イメージとして)。

タスクキューの動きをイメージする

  1. "1" がすぐに出力される
  2. 0ms の setTimeout が Web API に登録される
  3. 10ms の setTimeout も登録される
  4. "4" が出力される
  5. グローバルコードが終わり、コールスタックが空になる
  6. その時点で、0ms のタイマーはすでに終了しており、「そのコールバック」がタスクキューの先頭に入っている
  7. イベントループがそれを取り出して "2" を実行
  8. 少し後で 10ms が経過し、「3 のコールバック」がタスクキューに追加される
  9. 次にコールスタックが空いたときに "3" が実行される

タスクキューは「先に入ったものから順に処理される FIFO の待ち行列」なので、
同じ種類のタスクなら、基本的には登録された順番で実行される と考えてよいです。


タスクキューとマイクロタスクキュー(軽く触れる)

実は、ブラウザの世界にはタスクキューが一種類だけではなく、

  • 通常のタスクキュー(macrotask queue と呼ばれたりもする)
  • マイクロタスクキュー(microtask queue)

という二つのレベルがあります。

ここではイメージだけ掴んでおけば十分です。

代表的な例

通常のタスクキューに入る代表
setTimeout のコールバック
setInterval
DOM イベントのコールバック

マイクロタスクキューに入る代表
Promise.then のコールバック
async/await の「await の先の処理」
queueMicrotask で登録された処理

イベントループは、
「1つの通常タスクを処理し終わるたびに、次の通常タスクに行く前にマイクロタスクキューを全部空にする」
という順番で動きます。

その結果、Promise の then や await の続きが「かなり優先的に処理される」ことになります。

簡単な挙動確認の例

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

になります。

理由は、

  1. A → D は同期処理なのでそのまま
  2. setTimeout のコールバックは「通常のタスクキュー」
  3. Promise.then のコールバックは「マイクロタスクキュー」
  4. グローバルコードが終わってスタックが空になったタイミングで
    まずマイクロタスク(C)を全部処理してから、
    次に通常のタスク(B)に移る

というルールのためです。

ここが重要です。
「タスクキュー」という言葉を聞いたら、
「非同期コールバックが待っている場所」+「Promise など優先度が少し違う別のキューもある」
くらいのイメージを持っておくと、今後の async/await 学習がスムーズになります。


なぜタスクキューを知る必要があるのか

実行順の「なぜ?」に説明がつくようになる

タスクキューを知らないと、

「0ms の setTimeout がなぜ後回しになるのか」
「then の中の処理が、なぜこんな順番で実行されるのか」

が、ただの「覚えるしかないルール」に見えてしまいます。

タスクキューとイベントループの存在を知っていると、

「今はまだグローバルの処理がスタックに乗っているから、タスクキューの中身は後回しだな」
「ここで Promise を解決したから、マイクロタスクキューに then が積まれたな」

というふうに、「構造」で理解できるようになります。

ブロッキングとの関係も見えてくる

タスクキューにいくらコールバックが溜まっていても、
コールスタックが「重い同期処理」に占拠されていたら、
イベントループはタスクキューから何も取り出せません。

その結果、

  • クリックイベントが反応しない
  • setTimeout の実行が遅れる
  • Promise の then がなかなか走らない

という症状になります。

つまり、
「タスクキューに入っているから安全」ではなく、「スタックを長時間塞がない書き方」がセットで重要 だと分かります。


まとめ

タスクキューの本質は、
「非同期処理のコールバックが、実行されるまで順番に並んで待っている場所」
です。

押さえておきたいポイントを整理すると、

非同期処理(タイマー・通信・イベントなど)が完了したとき、Web API はコールバックを直接実行せず、まずタスクキューに入れる
イベントループは、コールスタックが空になったタイミングでタスクキューの先頭を取り出して実行する
setTimeout(..., 0) は「今すぐ」ではなく、「現在の処理が全部終わったあと、タスクキューの先頭で処理される」という意味
Promise.then や await の続きは「マイクロタスクキュー」に入り、通常タスクより優先して処理される
重い同期処理でコールスタックを塞ぐと、タスクキューにいくら溜まっても実行されない(=UI が固まる)

まずは、

  • setTimeout
  • Promise.resolve().then(...)
  • 簡単なイベントリスナー

を組み合わせた小さなコードを書いて、
「どのタイミングでどのコールバックがタスクキューに積まれ、どの順番で実行されているか」を意識しながら実行結果を見てみてください。

タスクキューとイベントループのイメージが掴めると、
JavaScript の非同期処理は「暗記する謎のルール」から、
「構造として納得できる仕組み」に変わります。

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