やりたいこと
こんな場面を想定します。
エンドポイント/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を使わない解決策を提示します。
結論
こうします。
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秒待つ場合はこう。
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ともっと仲良くなりましょう!