第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つの状態があります。

  1. pending (保留中): 初期状態。成功も失敗もしていない。
  2. fulfilled (成功): 非同期処理が成功して、結果が得られた状態。
  3. 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.allPromise.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つの非同期関数fetchData1fetchData2があります。これらの関数はそれぞれ異なる時間でデータを返します。両方の関数からデータを取得し、それらのデータを合体させて(例: data1 + " " + data2)コンソールに出力してください。ただし、両方のデータの取得は並行して行ってください。

function fetchData1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve("Hello,");
        }, 500);
    });
}

function fetchData2() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve("World!");
        }, 1000);
    });
}

// ここに並行してデータを取得し、コンソールに出力するコードを書いてください。