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

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

2022年2月21日 非同期処理:コールバック/Promise/Async - Promise.race (JavaScript Primer)

Promise.allメソッドは複数のPromiseが全て完了するまで待つ処理でした。 一方で複数のPromiseを受け取りますが、Promiseが1つでも完了した(Settled状態となった)時点で次の処理を実行する "Promise.race"を勉強しました。

Promise.race

Promise.raceメソッドはPromiseインスタンスの配列を受け取り、新しいPromiseインスタンスを返します。 この新しいPromiseインスタンスは、配列の中で一番最初にSettled状態となったPromiseインスタンスと同じ状態になります。 配列の中で一番最初にSettledとなったPromiseがFulfilledの場合は、新しいPromiseインスタンスもFulfilledになる 配列の中で一番最初にSettledとなったPromiseがRejectedの場合は、新しいPromiseインスタンスも Rejectedになる つまり、複数のPromiseによる非同期処理を同時に実行して競争(race)させて、一番最初に完了したPromiseインスタンスに対する次の処理を呼び出します。

// `timeoutMs`ミリ秒後にresolveする
function delay(timeoutMs) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(timeoutMs);
        }, timeoutMs);
    });
}
// 1つでもresolveまたはrejectした時点で次の処理を呼び出す
const racePromise = Promise.race([
    delay(1),
    delay(32),
    delay(64),
    delay(128)
]);
racePromise.then(value => {
    // もっとも早く完了するのは1ミリ秒後
    console.log(value); // => 1
});

上記のコードのまとめ

  • 引数の中で最初にSettledとなったPromiseインスタンスを返します。
  • Promise.raseのPromiseStateが決まった時点でSettledとなり、状態が不変になります。
  • Promise.raceの最初の処理結果が出た後も、残りの非同期処理は行われますが、返り値には影響しません。
  • Promise.raseの返り値は"Promiseインスタンスの1"となります。

Promise.allの返り値にPromiseResult内部プロパティに値が返ってきているのを検証から発見しました。

ここでは、PromiseResult内部プロパティにArray(2)が返ってきていて中身も参照した画像を載せます。 続いて、Promise.raseだと一番最初に実行された引数の値が入ります。 上記のコード例での検証結果も載せます。 Promise.raseの最初の引数の返り値はdalay(1)の結果なのでPromiseResult内部プロパティには 1が入っています。

Promise.raceの実用例

非同期処理のタイムアウトが実装できます。

以下は一定時間経過しても処理が終わっていない場合、エラーを返すという処理です。

function timeout(timeoutMs) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(new Error(`Timeout: ${timeoutMs}ミリ秒経過`));
        }, timeoutMs);
    });
}
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());
    });
}

Promise.race([
    dummyFetch("/resource/data"),
    timeout(500),
]).then(response => {
    console.log(response.body); // => "Response body of /resource/data"
}).catch(error => {
    console.log(error.message); // => "Timeout: 500ミリ秒経過"
});

上記のコードのまとめ

  • Promise.race()は配列内の2つのPromiseインスタンスの処理を同時に実行し先に返ってきた返り値が処理結果になります。
  • dummyFetchは0〜1秒未満のランダムなタイミングで実行しfulfilled状態のオブジェクトを返します。
  • timeoutは500ミリ秒経過したら例外を発生させrejected状態のインスタンスを返します。
  • dummyFetchのランダムの実行時間が500ミリ秒未満ならthenメソッドが実行され501ミリ秒以上ならcatchメソッドが実行されます。

[ES2017] Async Function

Promiseは構文ではなくただのオブジェクトであるため、それらをメソッドチェーンとして実現しないといけないといった制限があります。 この制限を解決するために導入されたのがAsync Functionです。

Async Functionは、Promiseインスタンスを返す関数を定義する構文です。

function doAsync() {
    return Promise.resolve("値");
}

上記のコードの糖衣構文が以下です。

async function doAsync() {
    return "値";
}

Async Functionの定義

// 関数宣言のAsync Function版
async function fn1() {}
// 関数式のAsync Function版
const fn2 = async function() {};
// Arrow FunctionのAsync Function版
const fn3 = async() => {};
// メソッドの短縮記法のAsync Function版
const obj = { async method() {} };

関数の定義にasyncキーワードをつけることで定義できます。

await式というPromiseの非同期処理が完了するまで待つ構文が利用できます。 非同期処理を同期処理のように扱えるため、Promiseチェーンで実現していた処理の流れを読みやすく書けます。

MDN await

参考

Promise.race