JavaScriptで理解する非同期処理
初学者にとって、非同期処理はひとつの関門ではないでしょうか?
その本質的な動作や仕組みについては少々混乱を招くことが多く、JavaScriptを使う上で非同期処理の理解が一つのハードルとなっていると思います。
結構、雰囲気で使っている人も多いと思います。
私たちがウェブサイトを使う際、裏側ではこの「非同期処理」が頻繁に行われていて、そのおかげでスムーズに情報を受け取ったり、ページが素早く動作したりしています。
この記事ではそんな「非同期処理」について理解を深められたらと思います。
また、Promiseやasync/awaitなど、非同期処理に関連するJavaScriptの便利な機能についても紹介します。これらの機能を使えば、非同期処理をさらにスマートに、効率的に扱えます。
「非同期処理」について学ぶことで、JavaScriptの世界が広がり、スキルアップにもつながります。それでは、一緒に学んでいきましょう!
1. 非同期処理とは
非同期処理とは、プログラムの一部が実行されている間に他のタスクを実行することができる処理のことです。JavaScriptでは、特にウェブページとやりとりをする際に、ネットワーク通信の待ち時間などを利用して、同時に他の処理を行うことができます。
2. 非同期処理が使用されるシーン
非同期処理は主に以下のようなシーンで使用されます。
- サーバーとの通信(データの送受信)
- ファイルの読み込み・書き込み
- タイマー処理(setTimeoutやsetInterval)
これらの処理は待ち時間が長くなる可能性があるため、非同期処理を用いて他のタスクを同時に実行することで、効率的に処理を行います。
非同期処理を体験する
以下のコードは2つの関数を並行に実行します。
- hogeFunction1:5秒かかる処理
- hogeFunction2:2秒かかる処理
非同期処理のおかげで2つの関数の処理は非同期(並行して)実行されて、全体の完了時間は5秒となります。
では、ウェブブラウザのDeveloper Toolsを表示して、以下のソースコードをコンソールに貼り付けて実行しましょう。
// 関数1
function hogeFunction1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Async Function1 完了 : 処理成功");
}, 5000);
});
}
// 関数2
function hogeFunction2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Async Function2 完了 : 処理成功");
}, 2000);
});
}
// 実行処理
function main() {
console.log("開始");
// Promise.allを使って、2つの非同期処理を並行して実行
Promise.all([hogeFunction1(), hogeFunction2()])
// 両方の非同期処理が完了したら.thenが実行される
.then(results => {
console.log(results[0]); // "Hoge Function1 完了 : 処理成功"
console.log(results[1]); // "Hoge Function2 完了 : 処理成功"
console.log("両方の非同期処理が終了");
})
.catch(error => {
console.log("エラーハンドリング : ", error);
});
}
// 実行
main();
2つの関数の処理が約5秒で完了した筈です。
3. 非同期処理で使用するキーワード
非同期処理は主にasync、await、Promiseを使って実装されます。
// 関数定義
function hogeFunction() {
// Promise関数を使い、結果(resolve, reject)を受け取る
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Hoge Function 完了");
}, 2000); // 2秒タイマー
});
}
// main関数の定義
async function main() {
console.log("開始");
// awaitを使ってhogeFunctionの処理完了まで待機
const result = await hogeFunction();
console.log(result);
console.log("終了");
}
main();
上記のコードでは、hogeFunctionという非同期処理を行う関数が定義されています。
この関数は、2秒後にresolveを呼び出し、"Hoge Function 完了"というメッセージを返します。
main関数では、このhogeFunctionをawaitを使って待機し、結果をresultに格納しています。
4. 非同期処理に使用する関数
async
asyncキーワードは、関数を非同期関数に変換します。非同期関数は、Promiseを返すようになります。これにより、awaitを使って、Promiseの結果を待機することができます。
await
awaitキーワードは、Promiseが解決(resolve)されるまで処理を一時停止します。awaitは、async関数の中でのみ使用できます。
Promise
Promiseは、非同期処理の成功・失敗を表現するオブジェクトです。
先ほどのコードの例でもありましたが、Promiseは主にresolve(成功時)とreject(失敗時)の2つの状態を持ちます。
try / catch、.then / .catchメソッドを用いて成功・失敗時の処理を追加することができます。
5. Promiseについて
非同期処理をより容易に扱う強力なツールであるPromiseについて深く掘り下げていきます。
PromiseはJavaScriptにおける組み込みオブジェクトの一つであり、非同期操作をより簡単に扱えるようにするために使われます。
非同期操作の結果を表す「Promise(約束)」を生成します。
Promiseの活用により、非同期処理の状態を効率的に管理することが可能になります。
これによって、読みやすくメンテナンス性の高いコードを書くことができます。
また、エラーハンドリングも容易になります。
Promiseオブジェクトとその3つの状態
Promiseオブジェクトは3つの状態を持っています。
これらの状態遷移をうまくハンドリングすることで非同期処理を効率的に制御することが可能です。
具体的には、new Promise()というコンストラクタを用いることでPromiseオブジェクトのインスタンスが生成されます。この生成されたPromiseオブジェクトが持つ3つの状態がPromiseの特徴となります。
3つの状態
-
Pending状態- Promiseオブジェクトが生成されると、まずはPending状態になります。
これはPromiseの初期状態で、まだFulfilledやRejectedになっていない状態です。
- 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として返す関数です。
// 非同期処理を行う関数の定義
async function hogeExample() {
// Fetch自体がPromiseを返す(Promiseの定義は不要)
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);
};
// 関数の実行
hogeExample();
上記のコードでは、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ブロックで処理することが可能です。
async function asyncTask() {
try {
let response = await fetch('https://api.github.com');
let data = await response.json();
console.log(data);
} catch (error) {
console.error('エラーを検知しました:', error);
} finally {
console.log('処理が終わりました');
}
}
上記のコードでは、非同期の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のチェイン)を防ぐことができます。
async function asyncFunc() {
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);
}
}
これらの手法を使用することで、Promise地獄を避け、非同期処理を扱いやすくすることができます。
このコードでは、tryブロック内の各行で非同期処理を順番に待っています。
エラーが発生した時点で残りの処理はスキップされ、エラーハンドリングに直行します。
これにより、コードはシンプルで可読性が高くなります。
結局どっちを使えばいいの?
言語やフレームワークによっては利用できない場合もあるので状況によります。
個人的には通常はtry/catchを使い、必要に応じて.then を使うなど臨機応変に使い分けるのが良いのかと思います。
以上、おわりです。