なぜ非同期処理が必要か?
プログラムが実行されるとき、すべての処理が直列(つまり、一つずつ順番に)に実行されると、長時間かかるタスクがあると全体の処理が遅くなってしまいます。例えば、ウェブサイトのデータをインターネットから取得する場合、そのデータが帰ってくるまでの間、他の処理がすべて止まってしまうことになります。
そこで、JavaScript では「非同期処理」を使って、長時間かかるタスクをバックグラウンドで実行し、そのタスクが完了したら結果を受け取るという方式が取られています。
Promise とは?
JavaScript での非同期処理の基本は「Promise」というオブジェクトです。Promise は「未来のある時点で結果が手に入る」という約束を表すオブジェクトです。
Promise は、主に以下の3つの状態を持つ
- Pending(保留中): 非同期処理がまだ完了していない、または開始していない状態。
- Fulfilled(成功): 非同期処理が成功して、結果が利用可能な状態。
- Rejected(失敗): 非同期処理が何らかの理由で失敗した状態。
Promise の作成
新しい Promise オブジェクトは、以下のように Promise コンストラクタを使用して作成します:
const myPromise = new Promise((resolve, reject) => {
// 何らかの非同期処理
if (/* 成功条件 */) {
resolve("成功時の結果");
} else {
reject("エラーメッセージ");
}
});
- resolve 関数は、非同期処理が成功したときに呼び出され、Promise を成功状態にします。
- reject 関数は、非同期処理が失敗したときに呼び出され、Promise を失敗状態にします。
Promise の使用
Promise が成功または失敗したときの結果を取得するためには、then と catch メソッドを使用します。
myPromise
.then(result => {
// Promise が成功したときの処理
console.log(result); // "成功時の結果" が表示されます
})
.catch(error => {
// Promise が失敗したときの処理
console.error(error); // "エラーメッセージ" が表示されます
});
Promise の利点
- 非同期処理の整理: Promise は非同期処理の結果を整理し、成功と失敗の処理を明確に区別することができます。
- チェーン処理: 複数の Promise を順番にまたは並列に実行することができます。
- エラーハンドリング: catch メソッドを使用して、非同期処理中に発生したエラーを一箇所で処理することができます。
async/await とは?
async:
- async キーワードは、関数が非同期であることを示します。
- async が付いた関数は、必ず Promise を返します。もし関数が明示的な Promise を返さなくても、TypeScriptは自動的にそれを Promise でラップします。
await:
- await キーワードは、async 関数の中でのみ使用できます。
- Promise の完了を待ちます。つまり、Promise が resolve または reject されるのを待ってから次の行に進みます。
- await は、Promise が成功した場合にその結果を返します。失敗した場合にはエラーをスローします。
シンプルな例
考え方を簡単にするため、お友達から手紙をもらうという状況を想像してみましょう。
- お友達が手紙を書くのに時間がかかるので、「待ってね」と言われます。
- あなたは待つことにし、その間に他のことをします。
- 手紙が届いたら、それを読みます。
このシナリオをコードで表現すると、以下のようになります。
// 1. 「手紙を書く」のをシミュレートする非同期関数
async function writeLetter(): Promise<string> {
return new Promise(resolve => {
setTimeout(() => {
resolve("手紙の内容");
}, 2000); // 2秒後に「手紙の内容」という結果を返します。
});
}
// 2. 手紙が届くのを待って、それを読む関数
async function readLetter() {
console.log("手紙を待っています...");
let letter = await writeLetter(); // 手紙が届くのを待ちます。
console.log("手紙が届きました:", letter);
}
// この関数を実行してみる
readLetter();
このコードを実行すると、最初に "手紙を待っています..." と表示され、2秒後に "手紙が届きました: 手紙の内容" と表示されます。
この例でのポイントは、await を使用することで、手紙が届くのを「待って」いるように見える点です。しかし、実際にはプログラムは他のタスクを実行することができます。
この async/await の構文は、非同期処理を「順番に」行う際のコードを簡潔にし、読みやすくします。TypeScript でも、この構文はそのまま利用でき、さらに型の情報を付け加えることでコードの安全性も向上します。
async/await の導入による変化とそのメリット
1. 読みやすさと直感的なコード
Promiseのみの場合:
function fetchData() {
return fetch("https://api.example.com/data");
}
fetchData().then(response => {
return response.json();
}).then(data => {
console.log(data);
}).catch(error => {
console.error("Error:", error);
});
async/awaitを使用した場合:
async function fetchDataAndPrint() {
try {
let response = await fetch("https://api.example.com/data");
let data = await response.json();
console.log(data);
} catch (error) {
console.error("Error:", error);
}
}
fetchDataAndPrint();
async/await を使用すると、コードが縦に進むことで、読むのが自然になります。.then() や .catch() を連鎖的につなげるのではなく、エラーハンドリングも try/catch でシンプルに扱えます。
2. エラーハンドリングの改善
async/await を使うことで、try/catch 構文を使ってエラーハンドリングができるため、同期コードと非同期コードのエラーハンドリングを同じ方法で行うことができます。これにより、一貫性があり、読みやすいエラーハンドリングが可能になります。
3. 変数のスコープと取り扱いの簡略化
async/await を使用することで、非同期処理の結果を変数に直接代入できます。これにより、スコープの扱いが簡単になり、変数の再利用も容易になります。
要するに、async/await の導入により、非同期プログラミングが直感的で読みやすく、そして一貫性のあるものになりました。この構文は、特に複数の非同期処理を扱う際や、非同期処理の結果を変数に格納して再利用する必要がある場合に、非常に役立ちます。