Skip to content

Latest commit

 

History

History
336 lines (297 loc) · 16 KB

promise-chain-to-async-function.adoc

File metadata and controls

336 lines (297 loc) · 16 KB

Async Functionと配列

Promiseと配列のように、配列を元にした複数の非同期処理を扱う場合のAsync Functionについて見ていきます。

例として、複数のリソースを順番に取得する処理をPromiseで書いていきます。

まずは、Promiseを使った非同期処理を行う関数として、リソースを擬似的に取得するdummyFetchという関数を実装していきます。 dummyFetch関数は擬似的にデータ取得する関数で、1000ミリ秒未満のランダムなタイミングでレスポンスを返す非同期的な処理です。 パスが/resourceから始まるリソースを取得した場合は、そのレスポンスをもったResolved状態のPromiseオブジェクトを返します。 それ以外の場合は、リソースの取得に失敗したとしてRejected状態のPromiseオブジェクトを返します。

/**
 * 1000ミリ秒未満のランダムなタイミングでレスポンスを擬似的にデータ取得する関数
 * 指定した`path`にデータがある場合、成功として**Resolved**状態のPromiseオブジェクトを返す
 * 指定した`path`にデータがない場合、失敗として**Rejected**状態のPromiseオブジェクトを返す
 */
function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}
// `then`メソッドで成功時と失敗時に呼ばれるコールバック関数を登録
// /resource/A のリソースは存在するので成功しonFulfilledが呼ばれる
dummyFetch("/resource/A").then((response) => {
    console.log(response); // => { body: "Response body of /resource/A" }
}, (error) => {
    // この行は実行されません
});
// /not_found のリソースは存在しないのでonRejectedが呼ばれる
dummyFetch("/not_found").then((response) => {
    // この行は実行されません
}, (error) => {
    console.log(error.message); // => "NOT FOUND"
});

このdymmyFetch関数を使い、複数のリソースを順番に取得するfetchResources関数を実装していきます。 fetchResources関数は、配列で複数のリソースへのパスを受け取り、取得したリソースの中身(body)を配列として返すことにします。

まずは、Promise APIのみを使ってfetchResources関数を実装してみましょう。 Promise APIでは、Array#reduceメソッドを使った逐次処理を実装することで、複数の非同期処理を順番に実行できます。 (詳細はPromiseによる逐次処理を参照)

function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}
// 複数のリソースを取得し、レスポンスボディの配列を返す
function fetchResources(resources) {
    const results = [];
    // 配列をpromise chainにして順番に処理する
    return resources.reduce((promise, resource) => {
        return promise.then(() => {
            return dummyFetch(resource).then((response) => {
                results.push(response.body);
                return results;
            });
        });
    }, Promise.resolve());
}
const resources = ["/resource/A", "/resource/B"];
// リソースを取得して出力する
fetchResources(resources).then((results) => {
    console.log(results); // => ["Response body of /resource/A", "Response body of /resource/B"]
});

次に、先ほどと同じ処理をするfetchResourcesをAsync Functionとawait式で書いてみます。 Async Functionとして定義したfetchResources関数では、forループの中でawait式を使うことで複数の非同期処理を順番に実行できます。

次のコードでは、リソースのパスをforループで順番に、dummyFetch関数を使ってリソースの中身を取得しています。 forループによる反復処理もawait式でdummyFetch関数の完了を待っているため、その非同期処理が終ってから次の反復処理を行います。 すべてのforループの処理が終わると、fetchResources関数が返したPromiseオブジェクトが変数resultsが参照する値でresolveされます。

function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}
// 複数のリソースを取得し、レスポンスボディの配列を返す
async function fetchResources(resources) {
    const results = [];
    for (let i = 0; i < resources.length; i++) {
        const resource = resources[i];
        const response = await dummyFetch(resource);
        results.push(response.body);
    }
    return results;
}
const resources = ["/resource/A", "/resource/B"];
// リソースを取得して出力する
fetchResources(resources).then((results) => {
    console.log(results); // => ["Response body of /resource/A", "Response body of /resource/B"]
});

Promise APIのみでfetchResources関数書いた場合は、コールバックの中で処理するためややこしい見た目になりがちです。 一方で、Async Functionとawait式を使った場合は、非同期処理での取得と追加を順番に行うだけとなりネストがなく見た目はシンプルです。

await式はAsync Functionの中でのみ利用可能

先ほどのfetchResources関数ではforループを利用していました。 このとき、配列の反復処理にArray#forEachメソッドを利用したくなるかもしれません。

しかし、次のようにforループをArray#forEachに変更するだけでは、構文エラー(Syntax Error)となってしまいます。

async function fetchResources(resources) {
    const results = [];
    // Syntax Errorとなる例
    resources.forEach(function(resources) {
        const resource = resources[i];
        // Async Functionではないスコープで`await`式を利用しているためSyntax Errorとなる
        const response = await dummyFetch(resource);
        results.push(response.body);
    });
    return results;
}

これは、await式はAsync Functionの直下でのみ利用できるからです。

Async Functionではない通常の関数でawait式を使うとSyntax Errorとなります。 これは間違ったawait式の使い方を防止するための仕様です。

function main(){
    // Syntax Error
    await Promise.resolve();
}

Async Function内でawait式を使って処理を待っている間も、関数の外側では通常とおり処理が進みます。 次のコードを実行してみると、Async Function内でawaitしても、Async Function外の処理は停止していないことがわかります。

async function asyncMain() {
    // 中でawaitしても、Async Functionの外側の処理まで止まるわけではない
    await new Promise((resolve) => {
        setTimeout(resolve, 16);
    });
}
console.log("1. asyncMain関数を呼び出します");
// Async Functionは外から見れば単なるPromiseを返す関数
asyncMain().then(() => {
    console.log("3. asyncMain関数が完了しました");
});
// Async Functionの外側の処理はそのまま進む
console.log("2. asyncMain関数外では、次の行が同期的に呼び出される");

このようにawait式で非同期処理を一時停止しても、Async Function外の処理が停止するわけではありません。 Async Function外の処理も停止できてしまうと、JavaScriptでは基本的にメインスレッドで多くの処理をするため、UIを含めた他の処理が止まってしまいます。 これがawait式がAsync Functionの外で利用できない理由の一つです。

await式はAsync Functionの中でのみ利用可能なため、コールバック関数もAsync Functionとして定義しないとawait式が利用できないことに注意してください。

そのため、fetchResources関数のArray#forEachメソッドのコールバック関数に対して、asyncキーワードをつけることで構文エラーは発生しなくなります。 この場合は、コールバック関数がAsync Functionとなるため、コールバック関数内でawait式が利用できます。 しかし、コールバック関数をAsync Functionに修正するだけでは、fetchResources関数が意図した結果を返しません。

次のようにArray#forEachメソッドのコールバック関数をAsync Functionにしてみます。 このコードを実行してみると、fetchResources関数の返したPromiseの結果は空の配列となり、意図した結果にならないことが分かります。

function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}
// リソースを順番に取得する
async function fetchResources(resources) {
    const results = [];
    resources.forEach(async(resource) => {
        const response = await dummyFetch(resource);
        results.push(response.body);
    });
    return results;
}
const resources = ["/resource/A", "/resource/B"];
// リソースを取得して出力する
fetchResources(resources).then((results) => {
    // resultsは空になってしまう
    console.log(results); // => []
});

forEachメソッドのコールバック関数としてAsync Functionを渡し、コールバック関数中でawait式を利用して非同期処理の完了を待っています。 しかし、この非同期処理の完了を待つのはコールバック関数Async Functionの中だけで、外側ではfetchResources関数の処理が進んでいます。 そのため、コールバック関数でresultsに結果を追加する前に、fetchResources関数はその時点の変数resultsの値でresolveしてしまいます。

次のようにfetchResources関数にコンソール出力を入れてみると動作が分かりやすいでしょう。 forEachメソッドのコールバック関数が完了するのは、fetchResources関数の呼び出しがすべて終わった後になります。 そのためfetchResources関数はその時点の変数resultsの値である空の配列でresolveします。

function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}
// リソースを順番に取得する
async function fetchResources(resources) {
    const results = [];
    console.log("1. fetchResourcesを開始");
    resources.forEach(async(resource) => {
        console.log(`2. ${resource}の取得開始`);
        const response = await dummyFetch(resource);
        console.log(`3. ${resource}の取得完了`);
        results.push(response.body);
    });
    console.log("4. fetchResourcesを終了");
    return results;
}
const resources = ["/resource/A", "/resource/B"];
// リソースを取得して出力する
fetchResources(resources).then((results) => {
    console.log(results); // => []
});

この問題を解決する方法として、先ほどのようにコールバック関数を使わずにforループを使う方法があります。 また、リソースを順番が重要ではない場合は、Promise.allメソッドを使い、複数の非同期処理を1つのPromiseとしてまとめる方法があります。

PromiseとAsync Functionを組み合わせる

Async Functionとawait式でも非同期処理を同期処理のような見た目で書けます。 しかし、非同期処理は必ずしも順番に処理することが重要ではない場合があります。 その際に、forループとawait式で書くと複数の非同期処理を順番に行ってしまい無駄な待ち時間を作ってしまうコードになってしまいます。

先ほどfetchResources関数ではリソースAを取得し終わってから、リソースBを取得していました。 このとき、取得順が変わっても問題無い場合は、リソースAとリソースBを同時に取得する方が効率的です。

Promise.allメソッドを使い、リソースAとリソースBを取得する非同期処理を1つのPromiseインスタンスにまとめることができます。 await式が評価するのはPromiseインスタンスであるため、await式はPromise.allメソッドなどPromiseインスタンスを返す処理と組み合わせて利用できます。

そのため、先ほどfetchResources関数でリソースを同時に取得する場合は、次のように書けます。 Promise.allメソッドは複数のPromiseを配列で受け取り、それを1つのPromiseとしてまとめたものを返す関数です。 Promise.allメソッドの返すPromiseインスタンスをawaitすることで、非同期処理の結果を配列としてまとめて取得できます。

function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/resource")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}

// 複数のリソースを取得しレスポンスボディの配列を返す
async function fetchResources(resources) {
    // リソースをまとめて取得する
    const promises = resources.map((resource) => {
        return dummyFetch(resource);
    });
    // すべてのリソースが取得できるまで待つ
    // Promise.allは [ResponseA, ResponseB] のように結果が配列となる
    const responses = await Promise.all(promises);
    // 取得した結果からレスポンスのボディだけを取り出す
    return responses.map((response) => {
        return response.body;
    });
}
const resources = ["/resource/A", "/resource/B"];
// リソースを取得して出力する
fetchResources(resources).then((results) => {
    console.log(results); // => ["Response body of /resource/A", "Response body of /resource/B"]
});

このようにAsync Functionやawait式は既存のPromise APIと組み合わせて利用できます。 Async Functionも内部的にPromiseの仕組みを利用した構文です。 そのため、Async FunctionとPromiseのAPIを組み合わせて考えることは重要です。