JavaScript | 非同期処理:コールバック – 同期コールバックとの違い

JavaScript JavaScript
スポンサーリンク

いちばん大事な違いのイメージ

まず、ざっくりこう覚えてください。

同期コールバック
→ 「その場で、すぐに呼ばれるコールバック」

非同期コールバック
→ 「あとで、別のタイミングで呼ばれるコールバック」

どちらも「関数を引数として渡す」のは同じですが、
「渡された関数が、いつ実行されるか」が決定的に違います。

ここが重要です。
同期コールバックは「今この呼び出しの流れの中で実行される」。
非同期コールバックは「今の処理が終わった“あと”、イベントループ経由で実行される」。


同期コールバックの具体例(今すぐその場で呼ばれる)

同期コールバックのコードと流れ

まず同期コールバックの典型例から。

function runTwice(callback) {
  console.log("runTwice: start");
  callback();              // 1回目(ここで即実行)
  callback();              // 2回目(ここで即実行)
  console.log("runTwice: end");
}

runTwice(() => {
  console.log("コールバックの中");
});
JavaScript

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

runTwice: start
コールバックの中
コールバックの中
runTwice: end

流れとしては、

  1. runTwice が呼ばれる
  2. 関数内で callback() を呼んだ瞬間、コールバックが実行される
  3. もう一度 callback() を呼ぶと、またその場で実行される
  4. すべて終わったら runTwice が終わる

つまり、「runTwice の実行の一部」としてコールバックが動いています。

コールスタック的な見方

このときのコールスタックは、

  1. グローバル → runTwice
  2. runTwice の中から callback 呼び出し → グローバル → runTwice → callback
  3. callback が終わったら、スタックから callback が消えて runTwice に戻る
  4. もう一度 callback を呼ぶ → また同じように積まれてすぐ戻る

という動きです。

ポイントは、コールバックが「この関数呼び出しの流れの中で、すぐに実行されている」 こと。
これが「同期コールバック」です。


非同期コールバックの具体例(あとで呼ばれる)

setTimeout を使った非同期コールバック

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

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 に渡している関数が「非同期コールバック」です。

流れはこうです。

  1. “A: start” を出力
  2. setTimeout を呼ぶ
    → 「1秒後にこの関数を実行して」と Web API に渡すだけで、ここではコールバックは実行されない
  3. “C: end” を出力
  4. グローバルコードが終わる(コールスタックが空になる)
  5. 1秒経過したタイミングで、Web API がコールバックをタスクキューに入れる
  6. イベントループがそのコールバックをスタックに乗せて実行し、”B: timeout callback” が出る

コールバックが実際に実行されるのは、「setTimeout を呼んだとき」ではなく、「かなりあと、別のサイクル」 です。

イベントリスナーも非同期コールバック

DOM イベントでも同じです。

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

button.addEventListener("click", () => {
  console.log("クリックされたときのコールバック");
});

console.log("リスナー登録完了");
JavaScript

addEventListener に渡している関数も、登録時には呼ばれません。
ユーザーが「いつか」クリックしたとき、

  1. ブラウザがクリックを検知
  2. 登録されていたコールバックをタスクキューに入れる
  3. イベントループが、それをスタックに乗せて実行

という流れで、未来のある瞬間に「非同期コールバック」として実行されます。


並べて比較:同期コールバック VS 非同期コールバック

同じように見えるコードを書いてみる

同期コールバック:

function syncCaller(callback) {
  console.log("syncCaller: before");
  callback();
  console.log("syncCaller: after");
}

syncCaller(() => {
  console.log("同期コールバック");
});
JavaScript

出力順:

syncCaller: before
同期コールバック
syncCaller: after

非同期コールバック:

function asyncCaller(callback) {
  console.log("asyncCaller: before");
  setTimeout(callback, 0);
  console.log("asyncCaller: after");
}

asyncCaller(() => {
  console.log("非同期コールバック");
});
JavaScript

出力順:

asyncCaller: before
asyncCaller: after
非同期コールバック

どちらも「関数を引数に渡している」のは同じなのに、
「いつ実行されるか」が根本的に違う のがわかると思います。

コールスタックとキューの違い

同期コールバックは、

  • 呼び出し元関数の「中で」すぐにコールスタックに積まれ、その場で処理される

非同期コールバックは、

  • いったん Web API に登録され
  • 条件が揃ったときにタスクキューやマイクロタスクキューに入り
  • 「今実行中のタスク(スタック)の処理が終わってから」イベントループによってスタックに積まれる

という「一呼吸置いた」動きをします。

ここが重要です。
同期コールバックは「現在のスタックの一部」、非同期コールバックは「次かそのまた次のタスクとして実行される別枠の処理」 と考えると、頭が整理しやすくなります。


もう少しだけ深く:コードの読み方がどう変わるか

同期コールバックは「順番どおりに読めばいい」

同期コールバックは、基本的にこう読めます。

上から順に処理が進む
コールバック呼び出しの行に来たら、その関数の中身が即実行される
終われば元の関数に戻る

さっきの syncCaller の例では、

  1. “before”
  2. コールバック(”同期コールバック”)
  3. “after”

と、コードの見た目どおりの順番で動きます。
「その場で関数を呼び出しているだけ」と考えれば OK です。

非同期コールバックは「ここから先は“別タイミングの処理”」と意識する

非同期コールバックが出てくる行に来たら、頭の中でこう切り替えます。

「あ、ここから先(コールバックの中身)は、“今すぐ”ではなく“あとで”の処理なんだな」

例えば:

console.log("1");

setTimeout(() => {
  console.log("2: 非同期コールバック");
}, 0);

console.log("3");
JavaScript

読み方としては、

  1. “1” を出す
  2. 「2 の処理」を“あとで実行するために登録する”(今は実行しない)
  3. “3” を出す
  4. いまのタスクが全部終わって、キューから非同期コールバックが拾われたタイミングで “2” が出る

という二段階に分けて考える必要があります。

ここが重要です。
非同期コールバックの行に来たら、「ここから下の処理は、今この連続の世界から一旦切り離される」と意識すること。
これができると、非同期コードの見通しが一気によくなります。


どっちをいつ使うのか

同期コールバックが向いている場面

同期コールバックが似合うのは、

  • 配列の各要素に対して「すぐに」何かしたいとき(forEach, map など)
  • 自前の関数ライブラリで「処理の一部」を外から差し込みたいとき
  • 時間のかからない処理を順番通りに組み合わせたいとき

例えば:

[1, 2, 3].forEach((n) => {
  console.log(n * 2);
});
JavaScript

これは完全に「その場で」実行されるので、同期コールバックです。

非同期コールバックが必須になる場面

非同期コールバックは、次のようなときに必須です。

  • 一定時間待ってから処理したい(setTimeout, setInterval
  • ユーザー操作に応じて処理したい(クリック、キー入力などのイベント)
  • サーバー通信やファイル読み込みなど、いつ終わるか分からない処理の結果を扱いたい

これらは、「終わるまで待ち続ける」と UI が固まってしまうので、
「結果が出たときに呼ばれる非同期コールバック」という形にせざるを得ません。


まとめ:同期コールバックと非同期コールバックの違いを一文で

最後にギュッとまとめます。

同期コールバックは、
「呼び出し元の関数の中で、その場ですぐ実行されるコールバック」
実行タイミングは「今ここ」です。

非同期コールバックは、
「タイマー、イベント、通信などが完了した“あと”、イベントループを通じて別タイミングで実行されるコールバック」
実行タイミングは「今ここではなく、未来のどこか」です。

これからコードを読むとき、コールバックを見かけたら、まず自分にこう問いかけてみてください。

「これは同期か? 非同期か?」
「この関数は“今すぐ”実行されるのか? それとも“あとで”実行されるのか?」

この 2 つを意識できるようになると、
非同期処理のコードは、ぐっと怖くなくなります。

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