JavaScript | 非同期処理:コールバック – 非同期コールバック

JavaScript JavaScript
スポンサーリンク

まず「非同期コールバック」を一言でイメージする

非同期コールバックは、
「“今すぐ”ではなく、“あとで”呼び出してもらうために登録しておく関数」
のことです。

普通のコールバックも「あとで呼ばれる関数」ですが、
非同期コールバックは特に、

  • いつ呼ばれるかが「今この瞬間」ではない
  • しかも「今の処理が終わった“後のタイミング”で、イベントループによって呼ばれる」

という特徴があります。

ここが重要です。
単に「関数を引数に渡しているからコールバック」ではなく、
「イベントループによって“別のタイミングで”実行されるコールバック」が、非同期コールバック です。


まずは普通のコールバックと比べてみる

同期コールバック(今すぐ呼ばれるパターン)

これは「同期コールバック」の例です。

function runCallback(callback) {
  console.log("runCallback start");
  callback(); // ここで即呼ばれる
  console.log("runCallback end");
}

runCallback(() => {
  console.log("中で実行されるコールバック");
});
JavaScript

実行結果はこうなります。

runCallback start
中で実行されるコールバック
runCallback end

runCallback の中で callback() を呼んだ瞬間に実行されているので、
「呼び出しの流れの中で、その場で発動する」タイプです。
これは「同期コールバック」とよく呼ばれます。

非同期コールバック(“あとで”呼ばれるパターン)

次は非同期コールバックの例です。

console.log("A: start");

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

console.log("C: end");
JavaScript

実行結果は、

A: start
C: end
(1秒後)
B: timeout callback

です。

setTimeout に渡したコールバックは、setTimeout の中では「今すぐ実行されません」。
Web API に「あとでこの関数を実行して」と登録され、
時間が経ったあとにイベントループを通じて呼び出されます。

ここが重要です。
「関数を渡して、その場で呼ぶ」なら同期コールバック。
「関数を渡して、“あとで”呼んでもらう」なら非同期コールバック。

この違いをまずしっかり分けてイメージしてください。


非同期コールバックが出てくる典型パターン

setTimeout / setInterval

タイマー系は非同期コールバックの教科書のような例です。

setTimeout(() => {
  console.log("1秒後に実行される非同期コールバック");
}, 1000);
JavaScript

ここでは、

  • コールバック関数 () => { ... }setTimeout に渡す
  • Web API が 1000ms 計測する
  • 経過後、「このコールバックをタスクキューに入れて」とイベントループに渡す
  • イベントループがスタックが空いたタイミングでそれを実行する

という流れになります。

setInterval も同じで、「◯ミリ秒ごとにこの非同期コールバックをタスクキューに投げてください」という仕組みです。

DOM イベント(クリックなど)

ユーザーの操作も、非同期コールバックの典型です。

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

button.addEventListener("click", () => {
  console.log("クリック時の非同期コールバック");
});
JavaScript

ここで渡している関数も、非同期コールバックです。

  • コードを実行した時点では呼ばれない
  • ユーザーが「いつか」クリックした瞬間に
  • ブラウザ(Web API)がイベントを検知し、「登録されていたコールバック」をタスクキューに入れる
  • イベントループがそれを実行する

という流れなので、「いつ実行されるかは未来のどこか」です。

非同期通信(昔ながらのコールバックスタイル)

Promise や fetch が出る前は、通信も非同期コールバックで扱っていました。

function getData(callback) {
  const xhr = new XMLHttpRequest();
  xhr.open("GET", "/data.json");
  xhr.onload = function () {
    callback(xhr.responseText);
  };
  xhr.send();
}

console.log("リクエスト開始");

getData((data) => {
  console.log("非同期コールバックで受け取ったデータ:", data);
});

console.log("他の処理");
JavaScript

ここで getData に渡している匿名関数も非同期コールバックです。
実際に呼ばれるのは、

  • ネットワーク通信が完了し
  • onload イベントが発火され
  • ブラウザがそのイベントハンドラをタスクキューに入れ
  • イベントループがそれを実行したタイミング

です。


なぜ非同期コールバックが必要になるのか

「終わるまで待ち続けると、全部止まる」から

もし非同期処理を同期で書いてしまうと、
時間のかかる処理のあいだ、JavaScript のメインスレッドが完全に止まってしまいます。

例えば 3 秒待ちを同期でやると:

console.log("A: start");

const start = Date.now();
while (Date.now() - start < 3000) {
  // 3秒間止まり続ける
}

console.log("B: end");
JavaScript

この 3 秒間、UI は固まり、クリックもスクロールも反応しません。

非同期コールバックを使えば、同じ「3秒後に処理したい」をこう書けます。

console.log("A: start");

setTimeout(() => {
  console.log("B: 3秒後の非同期コールバック");
}, 3000);

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

3 秒待っている間も、他の処理やユーザー操作に反応できます。

ここが重要です。
「待っている間も別のことをできるようにする」ために、
「結果が出たときに呼ばれる非同期コールバック」というスタイルが必要になる。

JavaScript がシングルスレッドだからこそ

JavaScript は基本的に「1 本のメインスレッド」で動いています。

  • この 1 本が UI イベントも画面更新も全部担当している
  • そこを長時間占有する同期処理を置くと、全部止まる

という構造なので、「時間のかかる処理を外に追い出す」しかありません。

非同期コールバックは、

  • 待つ仕事(タイマー・通信・イベント監視)は Web API に任せる
  • 結果が出たときに実行したい処理だけ、コールバックとして登録しておく

という分業の真ん中にいる存在です。


非同期コールバックの「実行タイミング」をもう少し深掘りする

「終わった瞬間」ではなく「キューに入ってから」という一呼吸

非同期コールバックは、
「条件が満たされた瞬間に即実行」されるわけではありません。

流れとしては、

条件が満たされる(時間が経つ・クリックされる・通信が終わる)
→ Web API が「このコールバックをタスクキューに入れる」
→ イベントループが「コールスタックが空いたタイミングで」それを実行する

という三段階です。

例えば:

console.log("A");

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

console.log("C");
JavaScript

0 ミリ秒でも、

A
C
B: timeout

という順になります。

理由は、

  • グローバルコード(A → setTimeout → C)が終わるまでは、コールスタックが埋まっている
  • 0ms 経過後に、コールバックはタスクキューに「準備完了」として待機する
  • グローバルコードが終わってスタックが空になったあと、ようやくイベントループがそのコールバックを実行する

からです。

ここが重要です。
非同期コールバックは「条件成立 → 即実行」ではなく、「条件成立 → キューに積む → スタックが空いたら実行」というワンクッション付きで動いている。

Promise.then や async/await との関係(軽く触れる)

今の主流は Promise / async/await ですが、
中身ではやっぱり「非同期コールバック」が動いています。

Promise.resolve().then(() => {
  console.log("これはマイクロタスクの非同期コールバック");
});
JavaScript

この then の中身も、「Promise が解決されたときに実行される非同期コールバック」です。
違いは、

  • タスクキューではなく「マイクロタスクキュー」に積まれる
  • 通常タスクよりも優先度高く実行される

という点だけで、「あとで実行されるコールバックである」という本質は同じです。


非同期コールバックの書きにくさと「コールバック地獄」

ネストが深くなる問題

非同期処理をコールバックだけで書いていくと、すぐこうなります。

getData((data1) => {
  processData1(data1, (result1) => {
    getMoreData(result1, (data2) => {
      processData2(data2, (result2) => {
        console.log("全部終わった:", result2);
      });
    });
  });
});
JavaScript

どの ) がどこに対応してるか分かりにくく、
エラー処理や条件分岐が入るとすぐに崩壊します。
これが「コールバック地獄」と呼ばれる状態です。

この問題を解決するために、Promise や async/await が生まれました。
でも、非同期コールバックそのものが消えたわけではなく、
Promise も中で「非同期コールバック(then など)」を利用しています。

ここが重要です。
「書きやすさ」のために Promise や async/await を使うが、
頭の中では「結局、非同期コールバックがイベントループ経由で呼ばれている」と理解しておくと強い。


まとめ:非同期コールバックとは何かを言い直す

非同期コールバックを一言でまとめると、

「時間がかかったり、いつ起こるか分からない処理(タイマー・通信・イベントなど)が終わった“あとで”、イベントループによって呼び出されるよう登録された関数」
です。

押さえておきたいポイントを整理すると、次のようになります。

「場で即呼ばれる」ものではなく、「あとで呼び返してもらうために渡す関数」であること。
setTimeout / setInterval / addEventListener / 古い通信 API などで、第1引数・第2引数に渡している関数が典型的な非同期コールバックであること。
実際の呼び出しは、「条件が満たされた瞬間」ではなく、「タスクキューやマイクロタスクキューに積まれ、コールスタックが空いたタイミング」で行われること。
JavaScript がシングルスレッドである以上、「待ち時間のあいだも他の処理を続ける」ための基本的な仕組みとして非同期コールバックが必要であること。
Promise / async/await も、表現が変わっただけで中身には「非同期コールバック」の考え方が流れていること。

最初のステップとしては、

setTimeout
addEventListener
簡単な通信処理(ダミーでも OK)

あたりを使って、「これは非同期コールバックだ」と意識しながらコードを書いてみてください。

「どのタイミングで登録されて、どのタイミングで実行されるのか」を
イベントループやタスクキューのイメージと一緒に追いかけていくと、
非同期コールバックは「ただの難しい言葉」ではなく、
「JavaScript が止まらないための当たり前の道具」として感じられるようになります。

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