6時だョ!!全員集合!!

Rails・JavaScrictを中心にアウトプットします。

2022年2月26日 非同期処理:コールバック/Promise/Async - await式はAsync Functionの直下でのみ利用可能 (JavaScript Primer)

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

今回はAsync Functionとawait式の特徴についてもう少し深ぼる内容でした。

関数宣言でawait式を使用した場合

// asyncではない関数では`await`式は利用できない
function main(){
    // SyntaxError: await is only valid in async functions
    await Promise.resolve();
}
  • await式が利用できるのは Async function直下もしくはECMスクリプトモジュール直下です。
  • await式を利用する場合はasync宣言をしないとSyntaxErrorになります。

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関数外では、次の行が同期的に呼び出される");
  • Async Functionでawait式を使った場合、外側の処理にawaitが影響することはありません。

Async Function内でコールバック関数を使った場合

for文で繰り返し処理をしてきましたが、ここではArray#forEachに変更しています。この場合に先述したAsync Functionの外側の処理にawaitが影響しない事で望む挙動が得られなくなってきます。

async function fetchResources(resources) {
    const results = [];
    // Syntax Errorとなる例
    resources.forEach(function(resource) {
        // Async Functionではないスコープで`await`式を利用しているためSyntax Errorとなる
        const response = await dummyFetch(resource);
        results.push(response.body);
    });
    return results;
}
  • Async Function内でforEachなどでコールバック関数を書く場合、コールバック関数に対してもasync宣言を行わないとawait式を使った場合にSyntaxErrorとなります。
  • コールバック関数内のブロックのawaitはコールバック関数の外側を参照しないためです。

Async Function内のコールバック関数をAsync Functionにした場合

forEachメソッドのコールバック関数をAsync Functionにした場合の挙動をみていきます。 syntax errorにはなりませんが望んだ挙動は得られません。 以下、コード例です。問題点がわかるようにconsole.logで出力する順番をみていきます。

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 function(resource) {
        console.log(`2. ${resource}の取得開始`);
        const response = await dummyFetch(resource);
        // `dummyFetch`が完了するのは、`fetchResources`関数が返したPromiseが解決された後
        console.log(`5. ${resource}の取得完了`);
        results.push(response.body);
    });
    console.log("3. fetchResourcesを終了");
    return results;
}
const resources = ["/resource/A", "/resource/B"];
// リソースを取得して出力する
fetchResources(resources).then((results) => {
    console.log("4. fetchResourcesの結果を取得");
    console.log(results); // => []
});
  • fetchResources(resources)は非同期処理が行われています。
  • 非同期処理内ではコールバック関数がAsync Functionでawait式が使われていますが外側には反映されません。
  • awaitの処理を待っている間に非同期処理が進み、先に結果をreturnします。
  • Fulfilled状態の[]が返り値となりsettledとしてthenメソッドが実行され、4つ目のconsole.log();とresultsが出力されます。
  • resultsが出力された後にawaitの5つ目のconsole.log();が出力されます。
  • forEachのコールバック関数をArrowFunctionに変えても同様の結果になります。

まとめ

この問題を解決する方法として、最初のfetchResources関数のように、コールバック関数を使わずにすむforループとawait式を組み合わせる方法があります。 また、fetchAllResources関数のように、複数の非同期処理を1つのPromiseにまとめることでループ中にawait式を使わないようにする方法があります。

  • await式を利用する場合はasync宣言をしないとSyntaxErrorになります。
  • await式は定義されたスコープのAsyncFunction外には影響しません。
  • AsyncFunction内でコールバック関数を使う場合は注意しましょう。

参考

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