はじめに
初心者エンジニアにとって理解が難しいJavaScriptの「非同期処理」について引き続き解説していきます。
非同期処理はウェブアプリケーションを構築する上で必要不可欠な概念です。
非同期処理のない世界では、普段使っているような便利なアプリケーションは作られません。
※同期処理と非同期処理の違いについてはこちらの記事で紹介しています。
初心者エンジニアの鬼門「JavaScriptの非同期処理」について解説!①同期処理と非同期処理の違いとは?
今回は具体的にどのように非同期処理を実装すればいいのか、についてお伝えします。
非同期処理の歴史はPromise登場以前と以後に別れる
JavaScriptはシングルスレッドで動作する言語です。一度に一つのことしか実行できません。
しかし、ウェブアプリケーションは多くの場合、非同期的に動作する必要があります。
例えば、サーバーからデータを取得する場合、処理が完了するまでユーザーが待つ必要があったら、非常に非効率です。
Promiseが導入される前、非同期処理は主にコールバック関数を使って行われていました。
コールバック関数とは、ある処理が完了したときに呼び出される関数です。
コールバック関数を使った非同期処理
以下は、コールバック関数を使った典型的な非同期処理の例です。
function fetchData(callback) { setTimeout(() => { const data = "サーバーから取得したデータ" callback(data) }, 1000) fetchData((data) => { console.log(data) })
fetchData
関数が呼び出され、コールバック関数として(data) => { console.log(data) }
が渡されます。fetchData
関数内でsetTimeout
が呼び出され、1秒(1000ミリ秒)のタイマーが設定されます。- 1秒後に、タイマーが終了し、
setTimeout
のコールバック関数が実行されます。このコールバック関数内で、data
変数に”サーバーから取得したデータ”という文字列が代入されます。 - 次に、渡されたコールバック関数
callback(data)
が呼び出され、data
の値が”サーバーから取得したデータ”となります。 - 最後に、コールバック関数内で
console.log(data)
が実行され、コンソールに”サーバーから取得したデータ”が出力されます。
したがって、1秒後にコンソールに"サーバーから取得したデータ"
と出力されることになります。
このコールバック関数を使用することにより、非同期処理を実現していました。
しかし、非同期処理をこのコールバック関数で実行するには問題があります。
複数の非同期処理を連続して行う場合、コードが非常に読みづらくなります。これを「コールバック地獄」と呼びます。
コールバック地獄とは?
コールバック地獄(Callback Hell)は、JavaScriptなどの非同期プログラミングにおいて、コールバック関数が多重にネストされてしまい、コードが読みづらく、保守が困難になる状況を指します。
例えば下記のような処理になります。
function fetchData(callback) {
setTimeout(() => {
const data = "サーバーから取得したデータ"
callback(data)
}, 1000)}
functionprocessData(data, callback) {
setTimeout(() => {
const processedData = data + "を処理したデータ"
callback(processedData)}, 1000)}
functiondisplayData(data) {
console.log(data)
}
fetchData((data) => {
processData(data, (processedData) => {
displayData(processedData)
})
})
長い!よく分からん!って感じですがこんなことを実行しています。
- 0ミリ秒時点:
fetchData
のsetTimeout
がセットされる。 - 1000ミリ秒時点:
fetchData
のコールバックが実行され、processData
が呼び出される。 - 1000ミリ秒時点:
processData
のsetTimeout
がセットされる。 - 2000ミリ秒時点:
processData
のコールバックが実行され、displayData
が呼び出される。 - 2000ミリ秒時点:
displayData
が実行され、コンソールに結果が表示される。
したがって、最終的に2秒後にコンソールに "サーバーから取得したデータを処理したデータ"
と表示されます。
実際のアプリケーションではさらにもっと多くの処理が同時に実行されるため、こんなネストの数では済みません。
このコールバック地獄の問題点は主に下記の3点です。
- 読みづらい: コードが右にどんどんインデントされていくため、可読性が低下します。
- 保守が困難: ネストが深くなると、エラーのトラブルシューティングや新しい機能の追加が難しくなります。
- エラー処理が複雑: 各コールバック内でエラー処理を行う必要があるため、エラー処理が複雑になりがちです。
Promise登場以前の非同期処理はこのように、とても大変でした。
コールバック地獄を避けるための解決策は2つある!
1. Promise:非同期処理をチェーンすることができる
Promiseは非同期処理の結果を表すオブジェクトであり、最終的に成功(resolved)するか失敗(rejected)するかのいずれかの状態になります。
Promiseを作成するには、new Promise
コンストラクタを使用します。
このコンストラクタは、resolve
とreject
という2つの引数を取る関数を受け取ります。
以下はPromiseの基本的な構造です。
const myPromise = new Promise((resolve, reject) => {
// 非同期処理をここに記述 if (/* 成功条件 */) { resolve('成功結果') } else { reject('エラーメッセージ') } })
Promiseの状態が変わったときに呼び出されるコールバック関数を登録するために、then
メソッドとcatch
メソッドを使用します。
myPromise.then((result) => { console.log('成功:', result) }) .catch((error) => { console.error('失敗:', error) }) })
このコードは、myPromise
が解決(resolve)または拒否(reject)されるのを待ち、その結果に応じて異なる処理を行います。
myPromise
myPromise
は、Promiseオブジェクトです。これは非同期処理を表します。- このPromiseは、何らかの非同期操作(例:データのフェッチ、タイマー、ファイルの読み込みなど)を行い、その操作が成功(resolved)したか失敗(rejected)したかを表します。
.then((result) => { ... })
then
メソッドは、Promiseが解決されたときに呼び出されるコールバック関数を登録します。- Promiseが成功(resolved)すると、このコールバック関数が呼び出され、成功の結果が
result
として渡されます。 - この例では、成功時に
console.log('成功:', result)
が実行され、成功の結果がコンソールに表示されます。
.catch((error) => { ... })
catch
メソッドは、Promiseが拒否されたときに呼び出されるコールバック関数を登録します。- Promiseが失敗(rejected)すると、このコールバック関数が呼び出され、エラーメッセージやエラーオブジェクトが
error
として渡されます。 - この例では、失敗時に
console.error('失敗:', error)
が実行され、エラーメッセージがコンソールに表示されます。
Promiseは、非同期処理を扱うための強力で柔軟なツールです。
Promiseを使用することで、コールバック地獄を避け、コードの可読性と保守性を大幅に向上させることができます。
2. async/await: 非同期処理を同期的なコードのように書ける構文
JavaScriptのasync
/await
は、Promiseを扱うためのシンタックスシュガー(構文糖)※で、非同期処理をより直感的で読みやすいコードにするための機能です。
※シンタックスシュガーとはプログラミング用語で、「複雑な構文を読み書きしやくするために用意される飴」のようなものとご認識いただければOKです。
上述したPromiseを使用すれば非同期処理を簡単に実装できることは分かったけど、構文が少し難しいですよね?
非同期処理をさらにわかりやすく記述できるのがこのasync
/await
です。
実務でもこのasync
/await
を使用することが多いです。
「この処理を非同期にしたい!」と思ったらasync
キーワードを関数の前に付けることで、その関数は自動的にPromiseを返す非同期関数になります。
async
関数は、明示的にPromiseを返すことなく、非同期処理を簡潔に記述することができます。
async function fetchData() {
// 非同期処理
}
関数の中でawait
キーワードを使用することで、Promiseの結果を待つことができます。
await
キーワードは、Promiseが解決(resolve)されるまで非同期関数の実行を一時停止し、Promiseの結果を返します。
await
は必ずasync
関数の中で使用する必要があります。
async function fetchData() {
const result = await getData()
console.log(result)
}
fetchData()
実際にアプリケーションで使用する場合は、エラーハンドリングについても考慮する必要があります。
async
/await
を使ったエラーハンドリングには、いくつか方法がありますが分かりやすい方法を一つ紹介します。
下記は、Promiseのcatch
メソッドを使用しています。 await
で非同期処理を実行した後に、catch
メソッドを使ってエラーハンドリングを行う方法です。
async function fetchData() {
const result = await getData().catch(error => {
console.error("データの取得中にエラーが発生しました:", error)
})
console.log(result)
}
getData()
のPromiseが拒否された場合にcatch
ブロックが実行されます。
async
/await
を使用することで、JavaScriptの非同期処理はより直感的で読みやすくなります。
async
関数とawait
キーワードを活用することで、Promiseのチェーンを使わずに、同期的なコードのように非同期処理を記述できます。
複数の非同期処理を順番に実行したり、並列に実行したり、エラーを適切に処理するために、async
/await
を有効に活用しましょう。
まとめ
非同期処理は、JavaScriptにおいて重要な概念であり、ウェブアプリケーションを構築するプログラムで頻繁に使用されます。
非同期処理はアプリケーションのパフォーマンスやユーザーエクスペリエンスに大きな影響を与えるため、開発者がよく理解し、効果的に活用することが求められます。
今回は非同期処理について、プログラミング初学者でも理解しやすいように本当にさわりの部分を解説しました。
まずは概念の理解と非同期処理を実装することを目標に、アプリケーション開発に取り組んでみてください!