プログラミング

【JavaScript】非同期処理を簡単な例を使って理解する

この記事は約11分で読めます。

「こう書けば動く」という状態でJavaScript/TypeScriptの非同期処理を実装してきました。

今回は「よく分からず使っている」「なんとなく分かる」にするため、できるだけ簡単な例を作成して基本を整理します。

本記事で掲載しているコードブロックは、そのままブラウザの開発者ツール(F12)のコンソールで実行できます。ぜひ確認してみてください。

同期処理と非同期処理

同期処理とは、最初のコードから次のコードへと順番に実行されていくことです。

料理人がラーメンとチャーハンを作る際に、ラーメンを作り終えてからチャーハンを作るイメージです。

JavaScript
const makeRamen = () => {
  console.log("ラーメンを作り始める");
  console.log("ラーメンを作り終えた");  
}

const makeFriedRice = () => {
  console.log("チャーハンを作り始める");
  console.log("チャーハンを作り終えた");  
}

makeRamen();
makeFriedRice();
出力結果
ラーメンを作り始める
ラーメンを作り終えた
チャーハンを作り始める
チャーハンを作り終えた

ですがラーメンを作ってる途中にチャーハンも作り始めた方が効率的です。

JavaScript
const makeRamen = () => {
  console.log("ラーメンを作り始める");
  setTimeout(() => {
    console.log("ラーメンを作り終えた");  
  }, 1000); // 1秒後にラーメンを作り終える
}

const makeFriedRice = () => {
  console.log("チャーハンを作り始める");
  setTimeout(() => {
    console.log("チャーハンを作り終えた");  
  }, 1000); // 1秒後にチャーハンを作り終える
}

makeRamen();
makeFriedRice();
出力結果
ラーメンを作り始める
チャーハンを作り始める
# 1秒待つ
ラーメンを作り終えた
チャーハンを作り終えた

このように、処理が終わるのを待たずに次の処理を行うこと非同期処理と言います。

非同期処理の利点

この「実行順序が縛られない」という特徴は、ユーザー体験を向上しつつ、通信などの重たい処理を行う上で非常に重要になります。

例えば、モーダルボタンをクリックした時に以下の2つの処理を行いたい場合を考えます。

  1. データの通信
  2. モーダル画面の表示

この時、同期処理の場合はデータの通信を終えてからでないとレンダリングされません。
しかし、非同期処理の場合、データの通信完了を待たずにレンダリングすることができます。

その結果、モーダル表示を高速に行うことができ、ユーザー体験を向上させることができます。

非同期処理を行う機能

同期処理と非同期処理の違いがなんとなく分かってきたところで、非同期処理を使うための代表的な機能を整理します。

  • setTimeout
  • Promise
  • async / await

setTimeout

setTimeoutは上記のラーメンとチャーハンの例でも出てきましたが、第2引数で受け取った時間だけ待った後に第1引数で受け取った処理を行う関数です。

処理を行わない間、別の処理を行うことができます。

JavaScript
const handleModalClick = () => {
  setTimeout(() => {
    console.log("データ通信完了");  
  },2000);
  console.log("モーダル表示処理");
}

handleModalClick();
console.log("再レンダリング");
出力結果
モーダル表示処理
再レンダリング
# 2秒待つ
データ通信完了

Promise

Promiseは非同期処理を簡単に記述できるよう用意されたオブジェクトです。

上記のモーダルボタンの例をPromiseで実装すると以下のようになります。

JavaScript
const handleModalClick = () => {
  new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('データ通信完了');
    }, 2000);
    resolve();
  });
  console.log("モーダル表示処理");
}

handleModalClick();
console.log("再レンダリング");
出力結果
モーダル表示処理
再レンダリング
# 2秒待つ
データ通信完了

一見コード量が増えただけのように見えますが、Promiseは複数の非同期処理をつなげて処理することを想定して作られています。これをPromiseチェーンと言います。

例えばデータを取得するのを待ってから変数にデータを格納したい場合を考えます。

JavaScript
const handleModalClick = () => {
  new Promise((resolve, reject) => {
    setTimeout(() => {
      const response = {
        id: 0, 
        data: "dummy data"
      }
      console.log('データ取得完了');
      resolve(response);
    }, 2000);
  }).then((response) => {
    const fetchedData = response.data;
    console.log("データ格納完了")
  });
  console.log("モーダル表示処理");
}

handleModalClick();
console.log("再レンダリング");
出力結果
モーダル表示処理
再レンダリング
# 2秒待つ
データ取得完了
データ格納完了

このようにresolveを使って正常終了したことを伝えつつ、データを次の非同期処理に渡すことができます。

thenの他にもerrorやfinallyがありますが今回は省略します。

async / await

async / awaitは、Promiseをさらに簡単に記述するために用意されたものです。

例えば、複数のPromiseチェーンを繋ぐ場合を考えます。

async/awaitを使わない場合(Promiseチェーン)です。

JavaScript
const fetchData1 = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("Data 1 fetched");
      resolve("inaka");
    }, 1000);
  });
};

const fetchData2 = (data1) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("Data 2 fetched");
      resolve(data1 + " de");
    }, 1000);
  });
};

const fetchData3 = (data2) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("Data 3 fetched");
      resolve(data2 + " Mac");
    }, 1000);
  });
};

fetchData1()
  .then((data1) => {
    console.log(data1);
    return fetchData2(data1);
  })
  .then((data2) => {
    console.log(data2);
    return fetchData3(data2);
  })
  .then((data3) => {
    console.log(data3);
    console.log("All data fetched");
  })
  .catch((error) => {
    console.error("Error fetching data:", error);
  });
出力結果
Data 1 fetched
inaka
# 1秒待つ
Data 2 fetched
inaka de
# 1秒待つ
Data 3 fetched
inaka de Mac
All data fetched

次にasync/awaitを使う場合です。

JavaScript
const fetchData1 = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("Data 1 fetched");
      resolve("inaka");
    }, 1000);
  });
};

const fetchData2 = (data1) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("Data 2 fetched");
      resolve(data1 + " de");
    }, 1000);
  });
};

const fetchData3 = (data2) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("Data 3 fetched");
      resolve(data2 + " Mac");
    }, 1000);
  });
};

const fetchAllData = async () => {
  try {
    const data1 = await fetchData1();
    console.log(data1);

    const data2 = await fetchData2(data1);
    console.log(data2);

    const data3 = await fetchData3(data2);
    console.log(data3);

    console.log("All data fetched");
  } catch (error) {
    console.error("Error fetching data:", error);
  }
};

fetchAllData();
出力結果
Data 1 fetched
inaka
# 1秒待つ
Data 2 fetched
inaka de
# 1秒待つ
Data 3 fetched
inaka de Mac
All data fetched

Promiseチェーンの場合、コードがネストしてしまい、処理の流れを追うのが難しくなります。特に複雑な非同期処理を扱うときには、thenの中にさらにthenが続くため、可読性が低下します。

async/awaitを使う場合、非同期処理が同期処理のように記述できるため、コードがシンプルで読みやすくなります。エラーハンドリングもtry/catchブロックを使って行うことができ、処理の流れを把握しやすくなります。

このように、async/awaitを使うことで、複雑な非同期処理をより簡潔で理解しやすいコードにすることができます。

まとめ

今回は、「よく分からず使っていた」非同期処理を「なんとなく分かる」にするために簡単に実行可能な例を作成してまとめました。

ユーザー体験を向上させるために必要なんだということが分かっただけでも収穫です。

下記の記事により詳しい(もっと分かりやすい)解説が載っているのでぜひ参考にしてみてください。

参考文献

【図解】1から学ぶ JavaScript の 非同期処理 - Qiita
はじめにJavaScriptで非同期処理を書くシーンは数多くあると思います。なのに、今までなんとなく使用してきました。これを機会にちゃんと勉強したいと思い体系化してまとめました。それだけだとタ…
setTimeout の真の力、あなたは知っていますか? - Qiita
こんにちは。ぬこすけ です。皆さんは「 setTimeout とはどんな関数でしょう?」と聞いたら、どう答えますか?おそらく、ほとんどの人が「指定した時間に処理が走るようにする関数」と答えるので…

コメント