25
Go to list of users who liked
24
Share on X(Twitter)
Share on Facebook
More than 5 years have passed since last update.
JavaScript/TypeScript で Promise を直列実行する
Last updated atPosted at 2019-05-07
2019/05/19 追記
目的
- 複数の
Promiseを直列実行したい- 同期的実行・シーケンシャル・シリアルなど呼び方色々あるが、とにかく Not 並列に
Promise[]を実行したい
- 同期的実行・シーケンシャル・シリアルなど呼び方色々あるが、とにかく Not 並列に
Promise.allのような書き心地がいい- 技術的な正しさを追求した記事ではなく、雰囲気で使いこなせることを優先した記事である
動作イメージ
この記事に出てくるサンプルコードについて
Promise,async/awaitはなんとなくわかってる人向けaxiosで書いている- 標準の
fetchは、ボイラープレートコードが読みづらい・この記事の本質から外れそうなのでやめた
- 標準の
- よって、試す場合はjsfiddle などで実行のこと
- やりすぎると API(GitHub) に対するちょっとしたDOS攻撃になりかねないため、節度を持ってご利用ください
- そうなる前に rate limit で弾かれるけども
- やりすぎると API(GitHub) に対するちょっとしたDOS攻撃になりかねないため、節度を持ってご利用ください
tl;dr
tl;dr
/** * @param {(() => Promise<T>)[]} promises * @returns {Promise<T[]>} * @template T */constsequential=async(promises)=>{constfirst=promises.shift()if(first==null){return[]}constresults=[]awaitpromises// 末尾に空のPromiseがないと、最後のPromiseの結果をresultsにpushできないため.concat(()=>Promise.resolve()).reduce(async(prev,next)=>{constres=awaitprevresults.push(res)returnnext()},Promise.resolve(first()))returnresults}// 使う側constmain=async()=>{constpromises=[()=>axios.get("https://api.github.com/search/users?q=siro"),()=>axios.get("https://api.github.com/search/users?q=yamato"),()=>axios.get("https://api.github.com/search/users?q=kiso"),]constresults=awaitsequential(promises)console.log(results)}main()- 以下、上記コードに至るまでの過程
実装過程
Lv.1 : べた書き
- まぁそりゃできるでしょうね
Lv.1
constmain=async()=>{constresults=[]results.push(awaitaxios.get("https://api.github.com/search/users?q=siro"))results.push(awaitaxios.get("https://api.github.com/search/users?q=yamato"))results.push(awaitaxios.get("https://api.github.com/search/users?q=kiso"))console.log(results)}main()Lv.2 : for-of パターン
eslint-config-airbnbに怒られるやつ- ちなみにそのメッセージは「iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations.」
- 要するに「変換に必要なruntimeが重い」らしい
results.pushで mutable なのが残念だが、割と誰でも読みやすい・直感的だと思うので嫌いじゃない
Lv.2
constmain=async()=>{constpromises=[()=>axios.get("https://api.github.com/search/users?q=siro"),()=>axios.get("https://api.github.com/search/users?q=yamato"),()=>axios.get("https://api.github.com/search/users?q=kiso"),]constresults=[]for(constpofpromises){results.push(awaitp())}console.log(results)}main()Lv.2- : ダメなパターン Array.prototype.forEach
- これは並列実行になってしまう
forEachの処理自体はawaitされず実行され、results.pushだけがawait後に実行される
Lv.2-
constmain=async()=>{constpromises=[()=>axios.get("https://api.github.com/search/users?q=siro"),()=>axios.get("https://api.github.com/search/users?q=yamato"),()=>axios.get("https://api.github.com/search/users?q=kiso"),]constresults=[]promises.forEach(async(p)=>{results.push(awaitp())})console.log(results)}main()- ちょっと Babel(トランスパイラ) の気持ちになってみよう
async/awaitをthenに変換する場合、きっとこんなコードになるはずだ- これなら確かに並列実行になるだろうと予想できる
Lv.2-_kimoti
constmain=async()=>{constpromises=[()=>axios.get("https://api.github.com/search/users?q=siro"),()=>axios.get("https://api.github.com/search/users?q=yamato"),()=>axios.get("https://api.github.com/search/users?q=kiso"),]constresults=[]promises.forEach((p)=>{p().then((res)=>{results.push(res)})})console.log(results)}main()Lv.2-- : ダメなパターン Array.prototype.map
- そもそもこれでは
resultsの型がPromise<AxiosResponse>[]になるので、全然ダメ
Lv.2--
constmain=async()=>{constpromises=[()=>axios.get("https://api.github.com/search/users?q=siro"),()=>axios.get("https://api.github.com/search/users?q=yamato"),()=>axios.get("https://api.github.com/search/users?q=kiso"),]constresults=promises.map(async(p)=>{returnawaitp()})// Babel の気持ち// const results = promises.map(async (p) => {// return p().then()// })console.log(results)}main()Lv.3 : reduce パターン
- ググるとよく出てくるやつ
Lv.3
constmain=async()=>{constpromises=[()=>axios.get("https://api.github.com/search/users?q=siro"),()=>axios.get("https://api.github.com/search/users?q=yamato"),()=>axios.get("https://api.github.com/search/users?q=kiso"),]constresults=[]awaitpromises.reduce((prev,next)=>{returnprev.then((result)=>{results.push(result)returnnext()})},Promise.resolve())console.log(results)}main()- VSCodeを使っていれば、「This may be converted to an async function. ts(80006)」と指摘され、QuickFixで async/await に自動変換できるはず
thenが消えて少し読みやすくなる
Lv.3_async
constmain=async()=>{constpromises=[()=>axios.get("https://api.github.com/search/users?q=siro"),()=>axios.get("https://api.github.com/search/users?q=yamato"),()=>axios.get("https://api.github.com/search/users?q=kiso"),]constresults=[]awaitpromises.reduce(async(prev,next)=>{constres=awaitprevresults.push(res)returnnext()},Promise.resolve())console.log(results)}main()- 写経すると
reduceの中のreturnや()を書き忘れてうまく動かなかったり、コードが理解できなくてハマる - 初めて見た時は「なんで reduce で直列実行になるんだ?」と理解に苦しんだ・・・
Lv.3+ : reduce パターン改
- 残念ながら、上記の「Lv.3」のコードには2点問題がある
reduceの initialValue の Promise のresolve結果が空(undefined)promises[2]の結果が push されない
- よって、
resultsの期待値と実際の結果が異なる- 期待値 :
[ AxiosResponse(siro), AxiosResponse(yamato), AxiosResponse(kiso) ] - 実結果 :
[ undefined, AxiosResponse(siro), AxiosResponse(yamato) ]
- 期待値 :
- 「3つの非同期処理を直列に実行する」だけならこれでもよいが、結果が受け取りたい場合はもう一工夫必要なようだ
- 上記問題を解消してみる
Lv.3+
constmain=async()=>{constpromises=[()=>axios.get("https://api.github.com/search/users?q=siro"),()=>axios.get("https://api.github.com/search/users?q=yamato"),()=>axios.get("https://api.github.com/search/users?q=kiso"),]constresults=[]awaitpromises.concat(()=>Promise.resolve()).reduce(async(prev,next)=>{constres=awaitprev// reduce の initialValue の resolve 結果を、結果配列に push させないため undefined チェックif(res!=null){results.push(res)}returnnext()},Promise.resolve())console.log(results)}main()- うーん。汚い(白目)
// @ts-checkを使ってると型推論結果がうまく合わないのか、エラーが出て更にストレスマッハreduce使ってるのにresults.pushが必要で mutable になっているのもダサい
- そもそもなぜ「問題2」が
.concat(() => Promise.resolve())で解決できるのか?- これを理解するため、ここで JavaScript の気持ちになって
reduceをバラしてみる
- これを理解するため、ここで JavaScript の気持ちになって
Lv.3+_kimoti
constmain=async()=>{constpromises=[()=>axios.get("https://api.github.com/search/users?q=siro"),()=>axios.get("https://api.github.com/search/users?q=yamato"),()=>axios.get("https://api.github.com/search/users?q=kiso"),].concat(()=>Promise.resolve())constresults=[]// 1ループ目constres=awaitPromise.resolve()if(res!=null){results.push(res)}constnext0=promises[0]()// siro// 2ループ目constres0=awaitnext0if(res0!=null){results.push(res0)}constnext1=promises[1]()// yamato// 3ループ目constres1=awaitnext1if(res1!=null){results.push(res1)}constnext2=promises[2]()// kiso// 4ループ目constres2=awaitnext2if(res2!=null){results.push(res2)}constnext3=promises[3]()// .concat(() => Promise.resolve())console.log(results)}main()- 「*ループ目」(
reduceの中身の処理) はpromises.length回分実行される - よって、下記のようになり「問題2」が解決できる
promises.length= 3 だったpromisesに.concat(() => Promise.resolve())で 1回分ループを足す- = 「4ループ目」が実行される
- =
const next2 = promises[2]() // kisoの await 後の結果のpushが実行される
- さらに Babel の気持ちになって async/await を then に・・・は面倒くさいので省略
Lv.3++ : reduce パターン改二
resultsを作ってる処理を functionsequentialに切り出す- 切り出すことで、 mutable な処理は
sequentialに押し込まれ、メイン処理から見ればresultsは immutable となった - immutable 万歳!
Lv.3++
constsequential=async(promises)=>{constresults=[]awaitpromises.concat(()=>Promise.resolve()).reduce(async(prev,next)=>{constres=awaitprev// reduce の initialValue の resolve 結果を、結果配列に push させないため undefined チェックif(res!=null){results.push(res)}returnnext()},Promise.resolve())returnresults}constmain=async()=>{constpromises=[()=>axios.get("https://api.github.com/search/users?q=siro"),()=>axios.get("https://api.github.com/search/users?q=yamato"),()=>axios.get("https://api.github.com/search/users?q=kiso"),]constresults=awaitsequential(promises)console.log(results)}main()Lv.Final
- だいぶマシにはなったが、まだ null チェックが目障り
- 無駄な処理を省く・整理する
Lv.Final
constsequential=async(promises)=>{constfirst=promises.shift()if(first==null){return[]}constresults=[]awaitpromises// 末尾に空のPromiseがないと、最後のPromiseの結果をresultsにpushできないため.concat(()=>Promise.resolve()).reduce(async(prev,next)=>{constres=awaitprevresults.push(res)returnnext()},Promise.resolve(first()))returnresults}constmain=async()=>{constpromises=[()=>axios.get("https://api.github.com/search/users?q=siro"),()=>axios.get("https://api.github.com/search/users?q=yamato"),()=>axios.get("https://api.github.com/search/users?q=kiso"),]constresults=awaitsequential(promises)console.log(results)}main()- 改良の余地はあるかもしれないが、とりあえずこれで目的達成
Promise.all との比較
- 完全に同じシグネチャにはできなかった
- 宣言した時点 (
axios.get("...")の時点) で各非同期処理が開始され、結果的に並列実行になるため - 直列にしたい場合は
functionで包んで、そのfunctionが実行されるまでPromiseが発火しないよう実装する必要があるため
- 宣言した時点 (
Promise.all
constmain=async()=>{constpromises=[axios.get("https://api.github.com/search/users?q=siro"),axios.get("https://api.github.com/search/users?q=yamato"),axios.get("https://api.github.com/search/users?q=kiso"),]constresults=awaitPromise.all(promises)console.log(results)}main()TypeScript + UnitTest
- 下記リポジトリを参照のこと
(TS + Jest の実行環境を簡単に用意するため、「create-react-app で生成 → 不要なもの削除」で作っている。少し冗長だがご容赦を)
まとめ
- いちいち定義するのめんどくさいので標準化されないかなー
- 気が向いたらもうちょい改良して npm package 化してみようと思う
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme
