Help us understand the problem. What is going on with this article?

Promiseのメソッドチェーンに潜む罠

More than 1 year has passed since last update.

引用元

Promise.prototype.then() メソッドと Promise.prototype.catch() メソッドもまた Promise を返すので、これらをチェーン (連鎖) させることができます。

なるほど確かにメソッドチェーンで書くことはできる。
じゃあメソッドチェーンをしなくても当然書けるよね、と思ってたら足元をすくわれた、という話。

おさらい

Promiseの基礎はこちら

やってみる

const t = Date.now();

function doneAsync(v) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const n = v + 1;
            console.log(`${Date.now() - t}: ${v}, ${n}`);
            resolve(n);
        }, 1000);
    });
}
  • 1秒後に
  • 経過時間・引数・引数に1足したものを表示して
  • 引数に1足したもので解決する

というPromiseを返す関数があったとして、これを使用した直列的な処理を考えてみる。

期待する結果は

1000: 0, 1
2000: 1, 2
3000: 2, 3

こんな感じ。1

①メソッドチェーン型

doneAsync(0)
.then(doneAsync)
.then(doneAsync);

②非メソッドチェーン型

const p = doneAsync(0);
p.then(doneAsync);
p.then(doneAsync);

実行結果

1005: 0, 1
2019: 1, 2
3021: 2, 3
1002: 0, 1
2019: 1, 2
2021: 1, 2

②のコードの3つ目のthenの実行タイミング・結果がおかしい。
一見等価に思える2つのコード、実は等価ではなかった

なんで?

thenの戻り値の認識ズレ。

引用元

プロミスに成功ハンドラと失敗ハンドラを付加します。呼ばれたハンドラの戻り値によって解決している新しいプロミスを返します。

この新しいプロミスを返しますというのが曲者で
メソッドチェーンをした場合、一般的にはメソッドを持つオブジェクト自体が返っているパターンが多く
thenでも実行したPromiseオブジェクトが返ってくると認識してしまいがちだが
実はthenが返すのは全く別の新しいPromiseオブジェクトになっている。

その前提で考えると、
①のコードでは、それぞれのthenの対象がそれぞれ別のPromiseオブジェクトなのに対し、
②のコードでは、全てのthenの対象が最初に作成したPromiseオブジェクトになっていた。

Promiseは、解決されたタイミングで自身に対してthenで登録されたcallbackを全て実行している2だけで
単一のPromiseオブジェクトそのものが直列的な処理フローを持っているわけではない。

解決した結果新たなPromiseオブジェクトの処理が動き出し、その結果また新たな・・・という実装をしていれば
それが結果的に直列的な処理になっているというだけの話。
この辺、概念としてはcallbackの入れ子と何ら変わらないように思う。

なので、②のような場合は

const p = doneAsync(0);
const p2 = p.then(doneAsync);
p2.then(doneAsync);

のように、前に実行したい処理を持っているPromiseオブジェクトに対してthenしなければならない。

どうしてもメソッドチェーンができない

宗教的な理由である処理を規定回数、for文で繰り返させたいなどのシーンで
メソッドチェーンができないということもあるでしょう。

そんな場合であっても、直前に返されたPromisethenすればいいということが分かっていれば

let p = doneAsync(0);
for (let i = 0; i < 2; i++) {
    p = p.then(doneAsync);
}

みたいな感じで書き換えは大丈夫。

悪い事ばかりでもない・・・か?

②のコード、やりたいこととは違っていたものの
逆にこれを並列処理的なものとして扱うこともできそう。

const p = doneAsync(0);
p.then(v => {
    console.log(v + 123); // 124
});
p.then(v => {
    console.log(v * 200); // 200
});

みたいな感じにすると、doneAsyncの結果(この場合は1)を用いて
それぞれ別々の処理を記述する事ができる。

ただ、並列処理とは言っても複数のブロックを同時に処理しているわけではなく
1つ目のcallback処理を同期的に終わらせたあと、次のcallback処理をする・・・ってだけなので
処理時間の短縮ができる、なんてことにはなりません。

独立した処理をthenブロックで分けると見やすいかもね~程度で。

まとめ

  • 単一のPromiseオブジェクトに複数thenしても、想像している直列処理にはならないよ
  • 基本的にはメソッドチェーンでthenした方がいいよ
  • どうしてもメソッドチェーンできない場合は、thenする対象をよく確認してね
  • catchthenと同じだよ

  1. 実行時間に数msの差は出る。 

  2. 厳密には、解決後に登録されたcallbackも実行するなどのいい感じの処理もしてくれる。 

tawatawa
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away