第12章 非同期処理
非同期処理の基礎
JavaScriptはシングルスレッドの言語で、一度に1つのタスクしか実行できません。しかし、非同期処理を用いることで、長時間かかるタスクをバックグラウンドで実行し、完了時に結果を取得することができます。
コールバック関数
コールバック関数とは?
コールバック関数とは、関数の引数として渡される関数のことを指します。この関数は、ある関数の処理が終了した後、または特定のタイミングで呼び出されます。主に非同期処理やイベントリスナーなどの処理でよく使用されます。
基本的な使用方法
以下は、コールバック関数の基本的な使用方法を示す例です。
// コールバック関数を引数として受け取る関数
function greeting(name, callback) {
console.log('こんにちは ' + name);
callback();
}
// 使用例
greeting('太郎', function() {
console.log('コールバックが実行されました。');
});
// 出力結果:
// こんにちは 太郎
// コールバックが実行されました。
非同期処理での使用例
非同期処理、特にsetTimeoutや外部APIの通信などでのレスポンス待ちの際にコールバック関数がよく使用されます。
function doSomethingAsync(callback) {
setTimeout(function() {
console.log("非同期処理が完了しました。");
callback();
}, 1000);
}
doSomethingAsync(function() {
console.log("コールバック関数が実行されました。");
});
// 出力結果(1秒後に表示):
// 非同期処理が完了しました。
// コールバック関数が実行されました。
コールバック地獄
コールバック関数を多用することで、入れ子になった関数が多くなり、読みにくくなることを「コールバック地獄」と呼びます。
step1(function(value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(result) {
// 最終的な処理
});
});
});
});
このようなコードは読みにくく、エラーハンドリングも難しいため、Promiseやasync/awaitなどの新しい機能が導入され、この問題の解決が求められています。
Promise
Promiseとは?
Promiseは、非同期処理の成功・失敗をより良く扱うためのオブジェクトです。コールバック関数のネスト(コールバック地獄)を防ぐことができるため、非同期処理のコードが読みやすくなります。
Promiseには主に3つの状態があります。
- pending (保留中): 初期状態。成功も失敗もしていない。
- fulfilled (成功): 非同期処理が成功して、結果が得られた状態。
- rejected (失敗): 非同期処理が失敗して、エラーが発生した状態。
Promiseの基本的な使い方
// Promiseの作成
const promise = new Promise((resolve, reject) => {
// 何らかの非同期処理
setTimeout(() => {
if (true) { // 成功した場合
resolve('成功!');
} else { // 失敗した場合
reject('エラー発生');
}
}, 1000);
});
// Promiseの使用
promise
.then((message) => {
console.log(message); // 成功!
})
.catch((error) => {
console.error(error); // エラー発生
});
.then() と .catch()
- .then() : Promiseが成功(fulfilled)状態になったときに実行される関数を登録します。.then()は新しいPromiseを返すため、連鎖的に呼び出すことができます。
- .catch() : Promiseが失敗(rejected)状態になったときに実行される関数を登録します。
.finally()
Promiseが成功・失敗に関わらず、最後に必ず実行したい処理がある場合、.finally()を使用します。
promise
.then((message) => {
console.log(message);
})
.catch((error) => {
console.error(error);
})
.finally(() => {
console.log("非同期処理終了");
});
Promiseチェーン
複数の非同期処理を順番に実行したいとき、Promiseをチェーンでつなげることができます。
function asyncProcess(value) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (value) {
resolve(`入力値: ${value}`);
} else {
reject('入力は空です');
}
}, 1000);
});
}
// チェーンの使用
asyncProcess('トリガー1')
.then(
(response) => {
console.log(response);
return asyncProcess('トリガー2');
}
)
.then(
(response) => {
console.log(response);
}
)
.catch((error) => {
console.error(error);
});
Promiseは、非同期処理の可読性とエラーハンドリングを大幅に向上させる便利なツールです。しかし、さらにシンプルに非同期処理を書くための新しい機能、async/awaitもES2017から導入されており、それと併せて学習すると効果的です。
async/await
async/awaitとは?
async/awaitは、JavaScriptの非同期処理をシンプルに書くための構文です。Promiseのチェーンを使わずに、まるで同期的なコードのように非同期処理を記述することができます。
基本的な使い方
- async: asyncキーワードは関数の前に配置し、その関数が非同期関数であることを示します。この関数は必ずPromiseを返します。
- await: awaitキーワードは非同期関数内で使われ、Promiseの結果を待つために使用します。awaitを使用すると、Promiseが完了するまで(fulfilledかrejectedの状態になるまで)コードの実行を一時停止します。
async function fetchData() {
// 非同期処理を模倣するための関数
function getSampleData() {
return new Promise((resolve) => {
setTimeout(() => resolve('データ取得完了!'), 1000);
});
}
let data = await getSampleData(); // awaitでPromiseの完了を待つ
console.log(data); // データ取得完了!
}
fetchData();
このコードでは、getSampleData関数でデータを非同期に取得し、取得が完了したらログに出力しています。
エラーハンドリング
async/awaitを使用する場合、エラーハンドリングには通常のtry/catch構文を使用します。
async function fetchDataWithErrorHandling() {
try {
let data = await someAsyncFunction();
console.log(data);
} catch (error) {
console.error("エラーが発生しました:", error);
}
}
fetchDataWithErrorHandling();
並行処理
複数の非同期処理を並行して行いたい場合、Promise.allやPromise.raceといった関数とasync/awaitを組み合わせることができます。
async function multipleRequests() {
let [result1, result2] = await Promise.all([asyncFunc1(), asyncFunc2()]);
console.log(result1, result2);
}
async/awaitを使うと、非同期コードが読みやすくなり、複雑なPromiseのチェーンやコールバックのネストを避けることができます。ただし、背後にあるのはPromiseの仕組みなので、Promiseの基本も理解しておくとより効果的にasync/awaitを使用できます。
練習問題1.
次の非同期関数delayedHelloは、1秒後に'Hello, World!'という文字列を返すPromiseを返すものとします。この関数を完成させ、1秒後にコンソールに'Hello, World!'を出力してください
function delayedHello() {
// この関数を完成させてください。
}
// 使用例
delayedHello().then(message => {
console.log(message);
});
練習問題2.
以下の非同期関数fetchDataは50%の確率でエラーをスローする関数です。この関数を使い、エラーがスローされた場合は"データの取得に失敗しました。"、成功した場合は取得したデータをコンソールに出力するコードをasync/awaitを使って書いてください。
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve("データ取得成功!");
} else {
reject("エラーが発生しました。");
}
}, 1000);
});
}
// ここに上記の非同期関数を使ったコードを書いてください。
練習問題3.
以下の2つの非同期関数fetchData1とfetchData2があります。これらの関数はそれぞれ異なる時間でデータを返します。両方の関数からデータを取得し、それらのデータを合体させて(例: data1 + " " + data2)コンソールに出力してください。ただし、両方のデータの取得は並行して行ってください。
function fetchData1() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Hello,");
}, 500);
});
}
function fetchData2() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("World!");
}, 1000);
});
}
// ここに並行してデータを取得し、コンソールに出力するコードを書いてください。