1
1

More than 3 years have passed since last update.

大量の非同期APIをawaitを使わずにシーケンシャルに呼び出す

Posted at

やりたいこと

こんな場面を想定します。
エンドポイント/m1scoreと、パラメータの配列がある。

const params = [
  { team: "madical", score: 649 },
  { team: "oide", score: 658 },
  { team: "mitori", score: 648 },
...
];

この配列の各要素を使って、POSTメソッドを全部シーケンシャルに(逐次処理で)呼び出したい。
下記のイメージ。

// まずは↓を実行
fetch("/m1score", {method: "POST", body: JSON.stringify({"team": "madical","score": 649})});
// ↑が終わったら↓を実行
fetch("/m1score", {method: "POST", body: JSON.stringify({"team": "oide","score": 658})});
// ↑が終わったら↓を実行
fetch("/m1score", {method: "POST", body: JSON.stringify({"team": "mitori","score": 648})});
// ↑が終わったら↓を実行
fetch("/m1score", {method: "POST", body: JSON.stringify({...

パラレルに呼び出せばよくね?

サーバーが死ぬ。
いやちゃんと作れば死なないかもしれませんが、素朴に作るとサーバーは大量アクセスに耐えられません。
我々はサーバーを落としたいわけではなく、リソースを正常にPOSTしたいのです。

さらに言うなら、たとえばDBにコネクションプールで接続しているようなAPIなんかは、
処理が終了しててもをDBコネクションが開放されておらず、即時に次のAPIを実行するとコネクションプールの開放のスピードが間に合わず枯渇する、なんてこともよくあります。
なのでAPIを一つ実行したらたとえば1秒待つようにしておきたい。

// まずは↓を実行
fetch("/m1score", {method: "POST", body: JSON.stringify({"team": "madical","score": 649})});
// ↑が終わったら1秒待ってから↓を実行
fetch("/m1score", {method: "POST", body: JSON.stringify({"team": "oide","score": 658})});
// ↑が終わったら1秒待ってから↓を実行
fetch("/m1score", {method: "POST", body: JSON.stringify({"team": "mitori","score": 648})});
// ↑が終わったら1秒待ってから↓を実行
fetch("/m1score", {method: "POST", body: JSON.stringify({...

awaitを使えばよくね?

一理ある。
だが、awaitはforEachの中では使えないため、ひと工夫が必要です。
参考: async/awaitを、Array.prototype.forEachで使う際の注意点、という話

ここではawaitを使わない解決策を提示します。

結論

こうします。

API逐次実行.js
const params = [
  { team: "madical", score: 649 },
  { team: "oide", score: 658 },
  { team: "mitori", score: 648 }
];
params
  .reverse()
  .reduce(
    (acc, cur) => () => {
      fetch("/m1score", { method: "POST", body: JSON.stringify(cur) })
        .then(acc());
    },
    () => {}
  )
  .call();

APIを呼び出した後1秒待つ場合はこう。

実行してから1秒待つ.js
const params = [
  { team: "madical", score: 649 },
  { team: "oide", score: 658 },
  { team: "mitori", score: 648 }
];
params
  .reverse()
  .reduce(
    (acc, cur) => () => {
      fetch("/m1score", { method: "POST", body: JSON.stringify(cur) })
        .then(setTimeout(acc, 1000));
    },
    () => {}
  )
  .call();

解説

reduce

今回の一番のポイントです。
reduceは配列を頭から順々に処理して一つの出力を得る関数です。これを使って入れ子関数を作ります。
accは作成途中の関数、curは次の配列要素(API実行のパラメータ)です。

内側からみていきましょう。

// 現在のパラメータを使ってfetchした後、1秒後にacc()を実行するようにしています。
fetch("/m1score", { method: "POST", body: JSON.stringify(cur) })
.then(setTimeout(acc, 1000));
// 即時実行しないように関数オブジェクトにしてあげます。
() => {fetch(...).then(...);}
// ()=>{}を初期値に、関数オブジェクトを次々に入れ子にしていっています。
// ()=>{}は何もしない関数です。(他にもっといい書き方があったら教えてください)
reduce( (acc, cur) => 関数オブジェクト , () => {} );

call()

reduceの結果が出来上がったものは関数オブジェクトなので、call()で呼び出してあげます。

reverse()

これはなくてもよいのですが、これがないと配列の後ろから順にAPI callされてしまいます。
というのもreduceは配列のはじめから処理し、入れ子関数を内側から作っていくため、
入れ子関数の一番外側(=一番最初に呼ばれるAPI)は配列の最後の要素が使われるのです。

直感的な呼び出し順にしたい場合は、上記のように、reverseを使って逆順にします。

まとめ

非同期処理とシーケンシャル処理は両立します。
皆さんもjavascriptともっと仲良くなりましょう!

1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1