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

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

2022年2月17日 非同期処理:コールバック/Promise/Async - Promiseチェーン (JavaScript Primer)

Promiseチェーン

Promiseによる統一的な処理方法は複数の非同期処理を扱う場合に特に効力を発揮します。 非同期処理が終わったら次の非同期処理というように、複数の非同期処理を順番に扱いたい場合もあります。 Promiseではこのような複数の非同期処理からなる一連の非同期処理を簡単に書く方法が用意されています。

thenやcatchメソッドは常に新しいPromiseインスタンスを作成して返すという仕様があります。 つまり、メソッドチェーンと同じくPromiseもチェーンすることができます。 以下はthenメソッドの返り値がPromiseインスタンスのため、1つ目のthenが返された後に次のthenの処理を実行しているという例です。

Promise.resolve()
    // thenメソッドは新しい`Promise`インスタンスを返す
    .then(() => {
        console.log(1);
    })
    .then(() => {
        console.log(2);
    });

このPromiseチェーンは、次のコードのように毎回新しい変数に入れて処理をつなげるのと結果的には同じ意味となります。

厳密等価演算子を使うとfalseを返すので新しいインスタンスを作成していることがわかります。

// Promiseチェーンを変数に入れた場合
const firstPromise = Promise.resolve();
const secondPromise = firstPromise.then(() => {
    console.log(1);
});
const thirdPromise = secondPromise.then(() => {
    console.log(2);
});
// それぞれ新しいPromiseインスタンスが作成される
console.log(firstPromise === secondPromise); // => false
console.log(secondPromise === thirdPromise); // => false

さらに具体的なPromiseチェーンの例です。

asyncTask関数はランダムでFulfilledまたはRejected状態のPromiseインスタンスを返します。 この関数が返すPromiseインスタンスに対して、thenメソッドで成功時の処理を書いています。 thenメソッドの返り値は新しいPromiseインスタンスであるため、続けてcatchメソッドで失敗時の処理を書けます。

// ランダムでFulfilledまたはRejectedの`Promise`インスタンスを返す関数
function asyncTask() {
    return Math.random() > 0.5
        ? Promise.resolve("成功")
        : Promise.reject(new Error("失敗"));
}

// asyncTask関数は新しい`Promise`インスタンスを返す
asyncTask()
    // thenメソッドは新しい`Promise`インスタンスを返す
    .then(function onFulfilled(value) { 
        console.log(value); // => "成功"
    })
    // catchメソッドは新しい`Promise`インスタンスを返す
    .catch(function onRejected(error) {
        console.log(error.message); // => "失敗"
    });

Math.random は0以上1未満の範囲で浮動小数点の擬似乱数を返します。値を(0 は含むが、 1 は含まない)を返します。 最初の三項演算子で0.5とMath.randomで返した値を比べたときの真偽値で条件分岐をしています。 先述したように、Promiseチェーンはそれぞれ独立したインスタンスを作成するため、今回の場合に条件分岐でtrueのときとfalseのときで別のインスタンスを作成させることで呼び出すメソッドが決まります。

asyncTask関数が成功(resolve)した場合はthenメソッドで登録した成功時の処理だけが呼び出され、catchメソッドで登録した失敗時の処理は呼び出されません。 一方、asyncTask関数が失敗(reject)した場合はthenメソッドで登録した成功時の処理は呼び出されずに、catchメソッドで登録した失敗時の処理だけが呼び出されます。

例外に対してthenやcatchをチェーンした場合、最も近くにある失敗時の処理が呼び出されます。 以下は1つ目のthenで例外を返しているため、rejectedな状態のPromiseインスタンスを返しており、2つ目のthenは無視されて3つ目のcatchの処理が呼び出されている例です。

Promise.resolve().then(() => {
    // 例外が発生すると、thenメソッドはRejectedなPromiseを返す
    throw new Error("例外");
}).then(() => {
    // このthenのコールバック関数は呼び出されません
}).catch(error => {
    console.log(error.message); // => "例外"
});

Promiseチェーンで失敗をcatchメソッドなどで一度キャッチすると、次に呼ばれるのは成功時の処理です。 これは、thenやcatchメソッドはFulfilled状態のPromiseインスタンスを作成して返すためです。 そのため、一度キャッチするとそこからは元のthenで登録した処理が呼ばれるPromiseチェーンに戻ります。

Promise.reject(new Error("エラー")).catch(error => {
    console.log(error); // Error: エラー
}).then(() => {
    console.log("thenのコールバック関数が呼び出される");
});

上記の説明を読んで本当にfulfilledを返すのか気になったので検証しました。

Promise.reject(new Error("エラー")).catch(error => {
    console.log(error); // Error: エラー
});

[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: undefined

内部プロパティのPromiseStatefulfilledになっていました。

まとめ

  • Promiseはメソッドチェーンと同じく、処理の返り値を新しいPromiseインスタンスを返して次のthenやcatchの処理へ繋ぐことが出来ます。
  • 返り値の状態(rejectedやfulfilled)がチェーンしているメソッド(catchやthen)にマッチしない場合は無視されて次のチェーンしているメソッドに移行します。
  • catchはエラーを受け取るが、受け取った後に返すPromiseState内部プロパティはfulfilledです。 (catchの中でさらにエラーを投げた場合は別です。)

    参考

    Promiseチェーン