JavaScriptで理解する非同期処理
初学者にとって、非同期処理
はひとつの難関ではないでしょうか?
JavaScriptは基本的にはシングルスレッド(ひとつひとつ処理を順番に実行する)
ですが、
非同期処理(複数の処理を並行して実行する仕組み)
を活用することで、マルチスレッド
のような振る舞いを実現しています。
この特性は、高い並行性を持つアプリケーションを開発する際に非常に有用です。
しかし、非同期処理
の本質的な動作や仕組みは初学者にとって混乱の元になることが多く、JavaScriptを使用する上で非同期処理
の理解が一つのハードルとなっていると思います。
結構、雰囲気で使っている人も多いと思います。
私たちがウェブサイトを使う際、裏側ではこの非同期処理
が頻繁に行われていて、そのおかげでスムーズに情報を受け取ったりページが素早く動作したりしています。
この記事では、そんな非同期処理
について少しでも理解を深められたらと思います。
また、Promise
やasync/await
など、非同期処理に関連するJavaScriptの便利な機能についても紹介します。これらの機能を使えば、非同期処理をさらにスマートに効率的に扱えます。
非同期処理
について学ぶことで、JavaScriptの世界が広がりスキルアップにもつながります。
それでは一緒に学んでいきましょう!
1. 非同期処理とは
非同期処理とは、プログラムの一部が実行されている間に他のタスクを実行することができる処理のことです。JavaScriptでは、特にウェブページとやりとりをする際に、ネットワーク通信の待ち時間などを利用して同時に他の処理を行うことができます。
2. 非同期処理が使用されるシーン
非同期処理は主に以下のようなシーンで使用されます。
- サーバーとの通信(データの送受信)
- ファイルやデータベースの読み込み・書き込み
- タイマー処理(setTimeoutやsetInterval)
これらの処理は待ち時間が長くなる可能性があるため、非同期処理を用いて他のタスクを同時に実行することで効率的に処理を行います。
非同期処理を体験する
以下のコードは2つの関数を並行に実行します。
- fiveSecondsFunc:5秒かかる処理
- twoSecondsFunc:2秒かかる処理
非同期処理のおかげで2つの関数の処理は非同期(並行して)実行されて、全体の完了時間は5秒となります。
では、ウェブブラウザのDeveloper Tools
を開いて、次に示すソースコードをコンソールに貼り付けて実行しましょう!
以下がソースコードです。
// 関数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. 非同期処理で使用するキーワード
非同期処理は主にPromise
、async
、await
を使って実装されます。
// 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
メソッドを用いて、解決・拒否それぞれに対応した処理を設定することが可能です。
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)となります。
以下にその例を示します。
const test = async () => {
return 1; // Promiseが解決(resolve)し、1の値が返る
}
test().then(alert); // 1
const test = async () => {
return Promise.resolve(1);
}
test().then(alert); // 1
asyncは関数がpromiseを返すことを保証し、非promiseをその中にラップするという事です。
await
awaitキーワード
は、asyncの解決(resolve)または拒否(reject)されるのを待ちます。
awaitが結果を受け取るまで、後続処理は発火しません。
(内部的には非同期に実行がされている)
※ await
は、asyncで宣言された関数内でのみ使用できます
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の初期状態です。
- Promiseオブジェクトが生成されると、まずはPending状態になります。
-
Fulfilled状態
- Promiseオブジェクトは、内部で管理するコールバック関数が resolve() を呼び出した時点でFulfilled状態に遷移します。
この状態はPromiseが正常に完了し、その結果が利用可能である状態です。
- Promiseオブジェクトは、内部で管理するコールバック関数が resolve() を呼び出した時点でFulfilled状態に遷移します。
-
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として返す関数です。
// 非同期処理を行う関数の定義
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ブロック
で処理することが可能です。
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
の方が直感的であることもあります。
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地獄
が生じます。
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の結果に基づいて順番に実行されます。
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のチェイン)を防ぐことができます。
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について詳しく解説しています。一読の価値アリです👍
参考: 非同期処理の実行順をビジュアル化して確認する
以下のウェブサイトでは、非同期処理がどのように実行されるかをビジュアルで確認することができます。
また、一般的な関数についても確認できますので参考にすると良いでしょう。
以上、おわりです。