Mima Design MemottoWEB DESIGNER · mima

JavaScriptの非同期処理を理解するための例題集

Webアプリでは、通信、タイマー、ファイル読み込み、ユーザー操作など、結果がすぐに返らない処理を扱います。JavaScriptの非同期処理を理解すると、画面が固まる理由や、処理の順番が予想と違う原因を説明できるようになります。

この記事では、コールバック、Promise、async / await、イベントループを小さな例題で整理します。開発メモや制作事例をまとめたMimaの制作記録のように、実際の画面やアプリを作る場面を意識しながら、コードの動きを追っていきます。

非同期処理が必要な理由

次のコードでは、メッセージがどの順番で表示されるでしょうか。

console.log("開始");

setTimeout(() => {
  console.log("タイマー完了");
}, 1000);

console.log("終了");

実行結果は「開始」「終了」「タイマー完了」です。setTimeoutは指定時間後に処理するよう登録するだけで、完了までJavaScriptの実行を止めません。時間のかかる処理を待つ間も、画面操作や別の処理を進められます。

コールバックで流れを読む

非同期処理が終わった後に実行する関数を、コールバック関数と呼びます。例えば、画像の読み込み後に表示処理を行う場合は、onloadに関数を渡します。

const image = new Image();

image.onload = () => {
  console.log("画像を読み込みました");
};

image.src = "photo.jpg";

複数の処理を順番に実行すると、コールバックが深く入れ子になることがあります。いわゆるコールバック地獄は、成功時と失敗時の流れを追いにくくするため、後述するPromiseやasync / awaitで整理するのが一般的です。

Promiseで処理の状態をつなぐ

Promiseは、非同期処理の結果を表すオブジェクトです。状態には、処理中のpending、成功したfulfilled、失敗したrejectedがあります。thenで成功時、catchで失敗時の処理を指定できます。

fetch("/api/profile")
  .then((response) => {
    if (!response.ok) {
      throw new Error("通信に失敗しました");
    }
    return response.json();
  })
  .then((profile) => {
    console.log(profile.name);
  })
  .catch((error) => {
    console.error(error.message);
  });

ここで重要なのは、fetchがHTTPエラーを自動的にcatchへ送るわけではない点です。404や500を失敗として扱うには、response.okを確認して明示的にエラーを投げます。通信処理では、この確認を省略しないようにします。

asyncとawaitで順番を表現する

asyncを付けた関数は必ずPromiseを返し、awaitはPromiseの完了を待ちます。見た目は同期処理に近くなりますが、ブラウザ全体を停止させるわけではありません。

async function loadProfile() {
  try {
    const response = await fetch("/api/profile");

    if (!response.ok) {
      throw new Error("プロフィールを取得できません");
    }

    const profile = await response.json();
    console.log(profile.name);
  } catch (error) {
    console.error(error.message);
  }
}

awaitはasync関数の中で使い、失敗する可能性がある範囲をtry / catchで囲みます。エラー表示、再試行ボタン、読み込み中の表示まで考えると、ユーザー体験を保ちやすくなります。

イベントループを観察する

JavaScriptの実行順を理解するには、イベントループとタスクキューの関係が役立ちます。

console.log("A");

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

Promise.resolve().then(() => console.log("C"));

console.log("D");

結果は「A」「D」「C」「B」です。同期処理が終わった後、Promiseのコールバックであるマイクロタスクが先に処理され、その後にsetTimeoutのタスクが実行されます。0ミリ秒は即時実行ではなく、キューに追加されるまでの最短時間です。

実務に近い通信処理の例

検索画面では、入力のたびに通信するとリクエストが増えます。一定時間入力が止まってから検索するデバウンスを使うと、不要な通信を減らせます。さらに、古いリクエストの結果が新しい結果を上書きしないよう注意が必要です。

スマートフォン向けのUIでは、カメラや画像選択などの機能も非同期処理と関係します。実装の題材を探すときは、カメラアプリ比較のようなレビューを読み、利用者がどの場面で待たされるのかを考えると、設計上の課題を見つけやすくなります。

小さな検証で理解を深める実践ポイント

非同期処理は、最初から大きなアプリで試すより、実行順を表示する短いコードから始めると理解が安定します。開発者ツールのコンソールで結果を確認し、予想と違った箇所を一つずつ分解します。

  • console.logに番号を付け、処理の順番を記録する
  • 成功時だけでなく、通信失敗時の分岐も用意する
  • Promise.allで並列実行できる処理を見分ける
  • 読み込み中、成功、失敗の画面状態を設計する

Webページの表示速度を考える場合は、非同期処理だけでなく、フォントや画像の読み込み順も確認します。Webフォントの読み込みを調べると、見た目の変化と表示パフォーマンスを両立する考え方も学べます。

まずはsetTimeout、Promise、fetchを使った短い例題をブラウザで実行し、予想した順番と実際の結果をメモしてください。その積み重ねが、複雑なアプリの通信処理やUI状態を落ち着いて設計する力につながります。