LoginSignup
6
5

JavaScriptの"非同期処理"を理解する(Promise/async/await)

Last updated at Posted at 2023-05-18

JavaScriptで理解する非同期処理

初学者にとって、非同期処理はひとつの難関ではないでしょうか?

JavaScriptは基本的にはシングルスレッド(ひとつひとつ処理を順番に実行する)ですが、
非同期処理(複数の処理を並行して実行する仕組み)を活用することで、マルチスレッドのような振る舞いを実現しています。
この特性は、高い並行性を持つアプリケーションを開発する際に非常に有用です。

しかし、非同期処理の本質的な動作や仕組みは初学者にとって混乱の元になることが多く、JavaScriptを使用する上で非同期処理の理解が一つのハードルとなっていると思います。
結構、雰囲気で使っている人も多いと思います。

私たちがウェブサイトを使う際、裏側ではこの非同期処理が頻繁に行われていて、そのおかげでスムーズに情報を受け取ったりページが素早く動作したりしています。

この記事では、そんな非同期処理について少しでも理解を深められたらと思います。

また、Promiseasync/awaitなど、非同期処理に関連するJavaScriptの便利な機能についても紹介します。これらの機能を使えば、非同期処理をさらにスマートに効率的に扱えます。

非同期処理について学ぶことで、JavaScriptの世界が広がりスキルアップにもつながります。
それでは一緒に学んでいきましょう!

1. 非同期処理とは

非同期処理とは、プログラムの一部が実行されている間に他のタスクを実行することができる処理のことです。JavaScriptでは、特にウェブページとやりとりをする際に、ネットワーク通信の待ち時間などを利用して同時に他の処理を行うことができます。

2. 非同期処理が使用されるシーン

非同期処理は主に以下のようなシーンで使用されます。

  • サーバーとの通信(データの送受信)
  • ファイルやデータベースの読み込み・書き込み
  • タイマー処理(setTimeoutやsetInterval)

これらの処理は待ち時間が長くなる可能性があるため、非同期処理を用いて他のタスクを同時に実行することで効率的に処理を行います。

非同期処理を体験する

以下のコードは2つの関数を並行に実行します。

  • fiveSecondsFunc:5秒かかる処理
  • twoSecondsFunc:2秒かかる処理
    非同期処理のおかげで2つの関数の処理は非同期(並行して)実行されて、全体の完了時間は5秒となります。

では、ウェブブラウザのDeveloper Toolsを開いて、次に示すソースコードをコンソールに貼り付けて実行しましょう!

以下がソースコードです。

非同期処理(Promise)で関数を並行処理する例
// 関数1
const fiveSecondsFunc = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Async fiveSecondsFunc 完了 : 処理完了");
    }, 5000);
  });
}

// 関数2
const twoSecondsFunc = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Async twoSecondsFunc 完了 : 処理完了");
    }, 2000);
  });
}

// 実行処理
const main = () => {
  console.log("-----開始-----");
  // Promise.allを使って、2つの非同期処理を並行して実行
  Promise.all([fiveSecondsFunc(), twoSecondsFunc()])
    // 両方の非同期処理が完了したら.thenが実行される
    .then(results => {
      console.log(results[0]); // "fiveSecondsFunc 完了 : 処理完了"
      console.log(results[1]); // "twoSecondsFunc 完了 : 処理完了"
      console.log("両方の非同期処理が終了");
    })
    .catch(error => {
      console.log("エラーハンドリング : ", error);
    }
  )
}

// 実行
main();

※ 手元の環境で試すのが難しい場合は
リアルタイムにJavaScriptのコードを実行して結果を反映するWebMakerというサービスもあります。こちらも非常に便利なので活用してください。

3. 非同期処理で使用するキーワード

非同期処理は主にPromiseasyncawaitを使って実装されます。

非同期処理の実装例
// 2秒後に処理を実行する関数
const twoSecondsFunc = () => {
  // Promise関数を使うと、結果(resolve, reject)を受け取れる
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("2: twoSecondsFunc 終了"); // resolve(解決)に返す
    }, 2000); // 2秒タイマー
  });
}

// main関数
const main = async () => {
  console.log("1: main 開始");
  // awaitを設定(例では、twoSecondsFuncの処理完了まで待機)
  const result = await twoSecondsFunc();
  // awaitの処理完了後に実行される
  console.log(result);
  console.log("3: main 終了");
}
main();
結果
=> "1: main 開始"
=> "2: twoSecondsFunc 終了"
=> "3: main 終了"

上記のコードでは、
Promise関数を用いてfuncという非同期処理を行う関数が定義されています。この関数は、2秒後にresolveを呼び出し、"2: twoSecondsFunc 終了"というメッセージを返します。

一方、main関数では、このtwoSecondsFunc関数の処理終了をasyncで宣言された関数のawaitを使って待機し、その結果をresultに格納しています。

4. 非同期処理に使用する関数

Promise

Promiseは、非同期処理の完了結果を受け取るためのオブジェクトです。
非同期処理の結果は、基本的に解決(resolve)、拒否(reject)の2つの状態としてコードで表現されます。

これらの状態は、try / catch.then / .catchメソッドを用いて、解決・拒否それぞれに対応した処理を設定することが可能です。

Promiseの使用例
const myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        // 2秒後に解決(resolve)を返し
        // 'Hello, Promise!'をパラメータとして渡す
        resolve("Hello, Promise!");
    }, 2000);
});

// Promiseが解決したらthenのコールバック関数が実行され、
// パラメータとしてresolveに渡された値が引数に入る
myPromise.then((successMessage) => {
    console.log("Yay! " + successMessage);
});
結果
▶️ Promise{<pending>} # Promiseは初期状態なので<pending>の状態
Yay! Hello, Promise! ## resolve後は裏で<fullfilled>へ状態変化

async

asyncキーワードを関数に付けると、その関数は非同期関数になります。

この非同期関数は、自動的にPromiseを返します。
つまり、その関数が値を返すとその値はPromiseの解決値(resolve,reject)となります。
以下にその例を示します。

asyncは暗黙的にPromiseを返す
const test = async () => {
  return 1; // Promiseが解決(resolve)し、1の値が返る
}

test().then(alert); // 1
明示的にPromiseを返すことも可能
const test = async () => {
  return Promise.resolve(1);
}

test().then(alert); // 1

asyncは関数がpromiseを返すことを保証し、非promiseをその中にラップするという事です。

await

awaitキーワードは、asyncの解決(resolve)または拒否(reject)されるのを待ちます。
awaitが結果を受け取るまで、後続処理は発火しません。
(内部的には非同期に実行がされている)
awaitは、asyncで宣言された関数内でのみ使用できます

awaitを使うことで、処理の発火を制御することができるため、
非同期コードを同期的に書くことができます。

awaitの使用例
// 時間のかかる非同期処理を模擬する関数
const sleep = (ms) => {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// asyncを付けた関数内でawaitを使うことができる
const main = async () => {
  console.log('処理を開始します...');

  // sleep関数が終了するまで待つ
  await sleep(5000); // ここで2秒間処理が停止します

  console.log('5秒待ちました!');
}

main();
結果
処理を開始します...
▶️ Promise{<pending>}
5秒待ちました!

5. Promiseについて

非同期処理をより容易に扱う強力なツールであるPromiseについて深く掘り下げていきます。

PromiseはJavaScriptにおける組み込みオブジェクトの一つであり、非同期操作をより簡単に扱えるようにするために使われます。
非同期操作の結果を表す「Promise(約束)」を生成します。

Promiseの活用により、非同期処理の状態を効率的に管理することが可能になります。
これによって、読みやすくメンテナンス性の高いコードを書くことができます。
また、エラーハンドリングも容易になります。

Promiseオブジェクトとその3つの状態(PromiseState)

Promiseオブジェクトは3つの状態を持っています。
これらの状態遷移をうまくハンドリングすることで非同期処理を効率的に制御することが可能です。

具体的には、new Promise()というコンストラクタを用いることでPromiseオブジェクトのインスタンスが生成されます。この生成されたPromiseオブジェクトが持つ3つの状態がPromiseの特徴となります。

3つの状態(PromiseState)

  • Pending状態

    • Promiseオブジェクトが生成されると、まずはPending状態になります。
      これはPromiseの初期状態です。
  • Fulfilled状態

    • Promiseオブジェクトは、内部で管理するコールバック関数が resolve() を呼び出した時点でFulfilled状態に遷移します。
      この状態はPromiseが正常に完了し、その結果が利用可能である状態です。
  • Rejected状態

    • コールバック関数が reject() を呼び出すと、PromiseオブジェクトはRejected状態に遷移します。これはPromiseが何らかの理由で完了できずエラーが発生した状態です。

以上が、Promiseオブジェクトの3つの状態です。

Promiseを返す関数について

Promiseコンストラクタを利用しなくても、Promiseを返す関数を使用することが可能です。
これは、数多くのJavaScriptの標準ライブラリやフレームワークでも提供されています。

この種類の関数は、一般的に何らかの非同期タスク(例えば、ネットワークリクエストやデータベースのクエリなど)を実行し、その結果を表すPromiseを返します。

Promiseを返す関数は、JavaScriptにおける非同期処理の強力なツール
こういったライブラリやフレームワークを使うことで、自分でnew Promiseを使ってPromiseを作る手間を省き、非同期処理を簡単にかつ安全に扱うことができます。
それを理解し、うまく使うことで、より高品質なコードを書くことができるようになります!

Promiseを返す関数の例:Fetch API

JavaScriptの標準ライブラリの一部であるFetch APIは、HTTP通信を行うためのAPIで、Promiseを返す関数です。
Fetchは、URLを引数として受け取り、そのURLへのHTTPリクエストを非同期に行い、結果を`Promiseとして返す関数です。

Fetch APIの使用例
// 非同期処理を行う関数の定義
const testExample = async () => {
  // Fetch自体がPromiseを返す(Promiseの定義は不要)
  const response = await fetch("https://api.github.com", { cache: 'no-store' });
  const data = await response.json();
  console.log(data);
};

// 関数の実行
testExample();

上記のコードでは、fetch関数を使って非同期にデータを取得し、その結果をログに出力しています。fetch関数Promiseを返すため、awaitを使って結果を待機しています。

Promiseを返す関数(まとめ)

既にJavaScriptの標準ライブラリや第三者ライブラリには、デフォルトでPromiseを返す処理が書かれた関数が豊富に提供されているのでPromiseの定義を書く必要があるか?ないか?は公式リファレンスで確認しましょう。

6. 非同期処理のエラーハンドリング

JavaScriptにおけるPromiseは非同期処理の結果を表現するためのオブジェクトで、成功した結果や失敗した理由(エラー)を表現することができます。

非同期処理のエラーハンドリングについて理解するためには、主にtry/catch.then/.catchの二つの方法があります。

try / catchの使い所

try / catchは、必ずしもPromise関数を必要としない一般的なエラーハンドリングです。しかし、async/awaitを使う場合、try/catchブロックは非常に直感的な方法でエラーハンドリングを行うことができます。
ここではPromiseが返す非同期処理を待つことができ、エラーが発生した場合は直ちにcatchブロックで処理することが可能です。

try / catch
const asyncTask = async () => {
    try {
        const response = await fetch("https://api.github.com", { cache: 'no-store' });
        if (!response.ok) {
            // HTTPステータスコードがエラーの場合
            throw new Error('HTTPエラー、ステータスコード: ' + response.status);
        }
        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.error('エラーを検知しました:', error);
    } finally {
        console.log('処理が終わりました');
    }
}
asyncTask();

上記のコードでは、非同期のfetch関数をawaitで待ち、エラーが発生した場合にはcatchブロックでそれを捉えています。

finallyは必ず処理されるブロックです。

.then / .catchの使い所

Promise関数を使用する時に限り、.then / .catchを使用する事ができます。
Promiseの基本的な形式である.then/.catchは、Promiseが解決したときに実行されるコールバック関数を指定することができます。
この形式を使うと、非同期処理の結果に基づいて連鎖的な処理を行うことが可能になります。

非同期処理を明示的に表現できるメリットがあります。また、複数の非同期処理を並列に実行する場合( Promise.all や Promise.race を使う場合など)には、 .then の方が直感的であることもあります。

.then .catchメソッドの使用例
fetch('https://api.example.com/data') // fetchはPromiseを返すのでPromiseの定義は不要
  .then(response => response.json())  // 取得成功した場合、結果をJSON形式に変換
  .then(data => console.log(data))    // JSONに変換後のデータを表示
  .catch(error => console.error('エラー:', error)); // エラー発生時の処理

thenメソッドを呼び出すことで、リクエストが成功した場合の処理を追加することができます。
また、catchメソッドを使うことで、何らかのエラーが発生した場合の処理も追加することができます。

Promise地獄とは?

Promise地獄とは、Promiseのチェーンが長く複雑になってしまう状況を指します。これはコードの可読性を下げ、デバッグを困難にする可能性があります。

複数の非同期処理を連鎖的に書くと、次のようなPromise地獄が生じます。

Promise地獄の例(.then/.catchがネストされ複雑に...)
doSomething()
    .then(result => {
        return doSomethingElse(result)
            .then(newResult => {
                return doAnotherThing(newResult)
                    .then(finalResult => {
                        console.log(`Got the final result: ${finalResult}`);
                    })
                    .catch(error => console.error(error));
            })
            .catch(error => console.error(error));
    })
    .catch(error => console.error(error));

ここでは、各処理のたびに.then().catch()をネストしています。この構造は読みにくく、エラーハンドリングも複雑になります。

Promise地獄の解消

この問題が発生するのは、try/catch、.then/.catchの扱い方次第です。
次ではそれぞれの地獄の起こらない書き方を提示します。

1. .then / .catchをうまく使う

Promise地獄を防ぐためには、これらのメソッドをネストせずに連鎖的に書くことがポイントです。
このコードでは、.thenがPromiseの結果に基づいて順番に実行されます。

Promiseを.then/catch
doSomething()
    .then(result => doSomethingElse(result))
    .then(newResult => doAnotherThing(newResult))
    .then(finalResult => console.log(`Final result: ${finalResult}`))
    .catch(error => console.error(error));
2. try / catchを使う

async/awaitとtry/catchを適切に使用すれば、通常はPromise地獄(ネストが深く複雑化したPromiseのチェイン)を防ぐことができます。

Promiseをtry/catch
const asyncFunc = async () => {
    try {
        const result = await doSomething();
        const newResult = await doSomethingElse(result);
        const finalResult = await doAnotherThing(newResult);
        console.log(`Got the final result: ${finalResult}`);
    } catch (error) {
        console.error(error);
    }
}

asyncFunc();

これらの手法を使用することで、Promise地獄を避け、非同期処理を扱いやすくすることができます。
このコードでは、tryブロック内の各行で非同期処理を順番に待っています。
エラーが発生した時点で残りの処理はスキップされ、エラーハンドリングに直行します。
これにより、コードはシンプルで可読性が高くなります。

結局どっちを使えばいいの?

言語やフレームワークによっては利用できない場合もあるので状況によります。
個人的には通常はtry/catchを使い、必要に応じて.then を使うなど臨機応変に使い分けるのが良いのかと思います。

参考: Microtasks Queue(内部キュー)

JavaScriptをより効果的に活用するためには、内部キューであるMicrotasks Queueの存在と処理順序に注意する必要があります。
正確な非同期処理の制御や相互作用を実現する為に、適切なキューの管理が欠かせません。

以下のウェブサイトは、Promiseの実行順序やMicrotasks Queueについて詳しく解説しています。一読の価値アリです👍

参考: 非同期処理の実行順をビジュアル化して確認する

以下のウェブサイトでは、非同期処理がどのように実行されるかをビジュアルで確認することができます。
また、一般的な関数についても確認できますので参考にすると良いでしょう。

以上、おわりです。

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5