echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【JavaScript】「とりあえずawait」は卒業しよう

概要

先日、非同期処理に関して、下記のようなツイートをしました。

このツイートに対して、思った以上に反響を頂いた(と思う)ので、今回もう一度async/awaitのことを記事にしました。

ちなみに前回書いたasync/await関連の記事はこちら。
makky12.hatenablog.com

なお今回の記事を書くにあたり、下記ブログを参考にしました。
(ブログを書こうと思ったきっかけもこれを見たから)

「メソッド実行とawaitの分離」の書き方など、役立つソースが多いです。

前提:async/awaitの基本

async/awaitですが、ざっくり書くと

Promiseによる非同期処理を、同期処理っぽく書ける仕組み

です。

これを利用することで、例えば下記のメリットがあります。

  • 同期処理の様にコードが書けるので、ソースが直感的にわかりやすい。
  • Promiseの記載が不要なので、ソースがシンプルになる
    • Promise.then()ネスト(≒コールバック地獄)が不要になる
    • returnが最後に来るので、処理の流れや戻り値が把握しやすい
// 例えば、Promiseだけの場合、非同期処理はこう書くけど...  
// ※axios.get()は、WEB APIなどにgetメソッドでリクエストを送る非同期処理  
function sample() {  
  
    return new Promise(resolve () => {
        axios.get()
        .then((a) => {
            axios.get(a)
            .then((b) => {
                axios.get(b)
                .then((c) => {
                    resolve(c);
                });
            });
        });
    });
}
  
// async/awaitだと、こんな感じでシンプルに書ける。
async function sample() {
  
    const a = await axios.get();
    const b = await axios.get(a);
    const c = await axios.get(b);
  
    return c;

}

本題

で、元の僕のツイートにある

  • 非同期処理A〜Cの3つ
  • 全て成功した場合のみ処理Dを実行
  • A〜Cは依存関係なし

という処理を書く場合、ついつい「とりあえず生中」ならぬ「とりあえずawait」な感じで、条件反射的に下記ソースを書いてしまいがちです。

//  ダメな例
async function sample() {
  
    const a = await axios.get("./a");
    const b = await axios.get("./b");
    const c = await axios.get("./c");
  
    const d = funcD(a, b, c);
    return d;
  
}

が、これだと処理の流れは

  1. 処理Aのリクエストを送り、レスポンス取得まで処理B以降は実施しない
  2. 処理Bのリクエストを送り、レスポンス取得まで処理C以降は実施しない
  3. 処理Cのリクエストを送り、レスポンス取得まで処理Dは実施しない
  4. 処理Dを実施

と直列処理になり結果として処理時間は「A + B + C + D」となってしまいます。

今回の場合、処理A~Cは非同期処理、かつ依存関係はないので、それならば下記の流れで、A~Cは並列に実行すべきです。

1-1. 処理Aのリクエストを実施し、レスポンスを待たずに処理2を実行
1-2. 処理Bのリクエストを実施し、レスポンスを待たずに処理2を実行
1-3. 処理Cのリクエストを実施し、レスポンスを待たずに処理2を実行
2. 処理A~Cのレスポンスをすべて取得するまで待つ
3. 処理Dを実施

これならば、処理時間は「MAX(A, B, C) + D」ですみますので、結果としてA~Cの処理時間に関しては、他の2処理の処理時間分、短い時間で実行可能です。
(例えばCが一番処理時間が長い場合、(A + B)時間、処理時間を短縮できます)

これをコードにすると、下記のようになります。(3パターンのソースを書きました)

なお、「ラーメンで理解するasync/await」にある通り、非同期処理の実行とawaitは、絶対にセットで書かなきゃいけない...なんてことはありません。
下記のように、別々に書いても問題なく動きます。

//  OKな例(async/await使用)
async function sample() {
  
    // とりあえず、リクエストだけは一括で投げておいて...
    const axiosA = axios.get("./a");
    const axiosA = axios.get("./b");
    const axiosC = axios.get("./c");
  
    // awaitするのは、レスポンスの受け取りのみにする。
    // これなら、どれが一番時間がかかっても、待ち時間はMAX(A, B, C)になる
    const a = await axiosA;
    const b = await axiosB;
    const c = await axiosC;
  
    const d = funcD(a, b, c);
    return d;
  
}
  
//  OKな例(Promise.all使用)
function sample() {
  
    return new Promise((resolve) => {
  
      // とりあえず、リクエストだけは一括で投げるのは同じ。
      const axiosA = axios.get("./a");
      const axiosA = axios.get("./b");
      const axiosC = axios.get("./c");
  
      // Promise.all([])は、配列内の全処理のレスポンスが返るまで、
      // then()内の処理の実行を待つメソッド。
      // 逆に、どれか一個でもレスポンスが返ってきたら  
      // then()内の処理を実行する「Promise.race([])」もある
      Promise.all([axiosA, axiosB, axiosC])
        .then((result) => {
          // resultには、Promise.allで指定した処理のレスポンスが配列で
          // 格納される
          const d = funcD(result[0], result[1], result[2]);
          resolve(d);
        });
    })
}
  
//  OKな例(async/awaitとPromise.all併用)
function sample() {
  
    // とりあえず、リクエストだけは一括で投げるのは同じ。
    const axiosA = axios.get("./a");
    const axiosA = axios.get("./b");
    const axiosC = axios.get("./c");
  
    // Promise.all([])とawaitを併用する
    const result = await Promise.all([axiosA, axiosB, axiosC]);
  
    const d = funcD(result[0], result[1], result[2]);
    return d;
  
}

結局、どれで書くべき?

「ラーメンで理解するasync/await」のコメントでも、

  • こう書けばPromise.all()はいらんのか
  • Promise.all()いらんけど、並列実行がわかりにくいから微妙。
  • awaitばっか羅列する書き方()だと、並列実行が分かりにくい

などなど、いろいろ書かれてました。

個人的には、それぞれのメリットとしては、例えば下記だと思います。

  • async/await

    • ソースがシンプルになり、分かりやすい(同期処理っぽくてわかりやすい)
    • 前の非同期処理の結果に依存する非同期処理を実施する場合でも利用可能
      • 「前提:async/awaitの基本」で書いたような、前の非同期処理のレスポンスを利用する、など
  • Promise.all

    • 並列処理ということが一目でわかりやすい
    • awaitの乱用を防げる

ただ、正直どれを使っても大差はない、と思います。
(好みやプロジェクトに応じて使い分ければいいかと)

※個人的には「async/awaitとPromise.all併用」が、一番分かりやすいかな...と思いますが。

まとめ

正直、「結局、どれで書くべき」については、好みやらこだわりやら宗教やらもあるので、どれでも構わないと思います。

ただ「とりあえずawait」で非同期処理を書いていると、今回書いたような「不要な時間をかけてる処理」が出来てしまうので注意しましょう、という話でした。

実装する機能の仕様や設計から、処理フローの構成をしっかり検討することが大切だと思います。