【JavaScript】非同期処理(Promise, async/await)の基礎まとめ

JavaScript

JavaScriptで非同期処理を扱うことは多々あるかと思いますが、他のコードを見よう見まねで非同期処理を記述している方もいるのではないのでしょうか?

JavaScriptの非同期処理の記法であるPromise, async/awaitについて、初心者の方でもわかるようにまとめましたので、参考にしてみてください。

JavaScriptの処理はシングルスレッドで実行される

JavaScriptはシングルスレッドで処理が実行されます。シングルスレッドは単一のスレッドで処理が実行されるため、処理が順序通りに実行されます。

例えば、シングルスレッドで順序通りに処理が実行される場合(非同期処理がない場合)の例を以下の図で示します。

処理Aは外部リソースやサーバーなどにリクエストを投げて、そのレスポンス受ける処理です。リクエスト後は外部リソースからのレスポンス待ちとなり、何もしていない時間が発生しています。

そして、処理Aが完了した後、処理Bが実行されます。

一方、マルチスレッドの場合は処理Aと処理Bを以下の図のように並列で処理できます。

非同期処理

シングルスレッドでも非同期処理を用いると効率良く処理を実行することができます。

以下の図は非同期処理を用いた場合の一例です。

先ほどの例だと処理Aのリクエスト後に待ち時間が発生しました。非同期処理では、処理Aの待ち時間の間に別の処理が実行され、レスポンスが返ってきたときに処理Aの続きが実行されます。

以降では、JavaScriptでどのような記法で非同期処理を実行するのかを説明します。

コールバック関数による非同期処理

非同期処理では、関数自身の処理が終わったときに実行される関数を登録します。

先ほどの図で説明すると、外部リソースからのデータ取得処理関数(処理Aの上の部分)に、データ取得終了後の処理関数(処理Aの下の部分)を登録するようなイメージです。

非同期処理の一例としてsetTimeoutを使ったコード例を以下に示します。

setTimeout(function(){
  console.log("Hello, world!");
}, 2000); // 2秒後に"Hello, world!"を出力

setTimeoutは指定時間待機した後にコールバック関数を実行します。上記のコードでは、第二引数に指定した2000[ms]待機した後に第一引数で指定したHello, world!を出力する関数を実行します。

コールバック地獄

非同期処理を連続で実行する場合にネストが深くなる場合があります。例えば、ファイルを読み込んだ後にその内容を加工し、その結果を別のファイルに書き込む場合を考えてみましょう。

const fs = require('fs');

fs.readFile('file1.txt', function(err, data1) {
  if (err) throw err;
  fs.readFile('file2.txt', function(err, data2) {
    if (err) throw err;
    fs.writeFile('file3.txt', data1 + data2, function(err) {
      if (err) throw err;
      console.log('Done!');
    });
  });
});

上記のコードでは、まずfile1.txtを読み込み、その後にfile2.txtを読み込んで、それらの内容を結合して、file3.txtに書き込む処理を行っています。

このようにコールバック関数をネストしすぎて、コードが複雑で読みづらくなることをコールバック地獄といいます。

このような場合に、Promiseやasync/awaitを使用することにより、コードが簡潔になり、読みやすくなるため、保守性を向上させることができます。

Promiseとは?

Promiseは非同期処理の完了や失敗を表現するオブジェクトで、2つの引数を取る関数を渡します。

const promise = new Promise((resolve, reject) => {
  // 非同期処理を実行する
  // 処理が成功した場合、resolve()を呼び出す
  // 処理が失敗した場合、reject()を呼び出す
});

resolve()は、Promiseを完了状態にするために呼び出され、引数には処理の結果が渡されます。reject()は、Promiseを拒否状態にするために呼び出され、引数にはエラーの詳細が渡されます。

Promiseオブジェクトを使用すると、次のように非同期処理を簡単に処理できます。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('成功しました');
  }, 1000);
});

promise.then((result) => {
  console.log(result); // => '成功しました'
});

上記コードでは、Promiseのthen()メソッドを使用して、setTimeoutで1秒待機が成功した時(3行目)にコンソール出力する処理(8行目)を定義しています。

then()メソッドでは、非同期処理で完了状態(resolve()が呼び出された後)の処理を定義します。また、catch()メソッドでは、非同期処理が拒否状態(reject()が呼び出された後)の処理を定義します。

Promiseチェーン

Promiseは、チェーン化(chaining)ができるため、複数の非同期処理を順番に実行することができます。Promiseチェーンを使用すると、ネストされたコールバック関数よりもシンプルで読みやすいコードを記述することができます。

以下は、Promiseチェーンを使用して、複数の非同期処理を順番に実行する例です。

function asyncFunc1() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('asyncFunc1が完了しました');
      resolve();
    }, 1000);
  });
}

function asyncFunc2() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('asyncFunc2が完了しました');
      resolve();
    }, 1000);
  });
}

function asyncFunc3() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('asyncFunc3が完了しました');
      resolve();
    }, 1000);
  });
}

asyncFunc1()
  .then(() => asyncFunc2())
  .then(() => asyncFunc3())
  .then(() => {
    console.log('すべての処理が完了しました');
});

上記の例では、3つの非同期処理を順番に実行しています。asyncFunc1()が完了したら、次の非同期処理であるasyncFunc2()を実行し、その後、asyncFunc3()を実行しています。最後に、3つの非同期処理がすべて完了した後に、コンソールにメッセージを出力しています。

async/awaitとは?

async/awaitは、Promiseをより簡潔に扱うための構文です。asyncは関数が非同期処理を行うことを示すキーワードです。awaitはPromiseが完了するまで処理を待機するためのキーワードです。

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function asyncFunc() {
  console.log('処理を開始します');

  // sleep関数を呼び出して1秒待機する
  await sleep(1000);

  console.log('1秒後に処理が完了しました');
}

asyncFunc();

上記のコードでは、asyncキーワードを使用して非同期処理を行う関数を定義し(5行目)、その中で、awaitキーワードを使用してPromiseの完了を待った後(9行目)、コンソール出力処理を行います(11行目)。

async/awaitを使用すると、非同期処理のエラーハンドリングがより簡単になります。先ほどのPromiseの例では、then()/catch()を使用してエラーハンドリングを行いました。async/awaitを使用する場合は、try-catch文を使用してエラーハンドリングを行うことができます。

async function asyncFunc() {
  try {
    const result = await promise;
    // 非同期処理が成功した場合の処理
  } catch (error) {
    // 非同期処理が失敗した場合の処理
  }
}

async/awaitとPromiseチェーンの比較

async/awaitとPromiseチェーンは、非常に似た機能を持っています。どちらも、非同期処理を順番に実行することができます。しかし、それぞれのアプローチには異なる特徴があります。

async/awaitは、Promiseチェーンよりも直感的で読みやすいコードを記述することができます。また、エラーハンドリングが簡単になるため、コードの保守性が高くなります。

一方、Promiseチェーンは、async/awaitよりも柔軟性があります。複雑な非同期処理を実行する場合は、Promiseチェーンを使用した方が効率的な場合があります。

まとめ

JavaScriptにおいて、非同期処理を扱うための機能として、Promise、async/awaitについて説明しました。

Promiseを使用することで、コールバック地獄を回避し、非同期処理をよりシンプルに扱うことができます。また、async/awaitを使用することで、非同期処理を直感的に記述することができ、コードの保守性を保つことができます。

コメント

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