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
*/
const sequential = async (promises) => {
const first = promises.shift()
if (first == null) {
return []
}
const results = []
await promises
// 末尾に空のPromiseがないと、最後のPromiseの結果をresultsにpushできないため
.concat(() => Promise.resolve())
.reduce(async (prev, next) => {
const res = await prev
results.push(res)
return next()
}, Promise.resolve(first()))
return results
}
// 使う側
const main = async () => {
const promises = [
() => 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"),
]
const results = await sequential(promises)
console.log(results)
}
main()
- 以下、上記コードに至るまでの過程
実装過程
Lv.1 : べた書き
- まぁそりゃできるでしょうね
Lv.1
const main = async () => {
const results = []
results.push(await axios.get("https://api.github.com/search/users?q=siro"))
results.push(await axios.get("https://api.github.com/search/users?q=yamato"))
results.push(await axios.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
const main = async () => {
const promises = [
() => 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"),
]
const results = []
for (const p of promises) {
results.push(await p())
}
console.log(results)
}
main()
Lv.2- : ダメなパターン Array.prototype.forEach
- これは並列実行になってしまう
-
forEach
の処理自体はawait
されず実行され、results.push
だけがawait
後に実行される
Lv.2-
const main = async () => {
const promises = [
() => 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"),
]
const results = []
promises.forEach(async (p) => {
results.push(await p())
})
console.log(results)
}
main()
- ちょっと Babel(トランスパイラ) の気持ちになってみよう
-
async/await
をthen
に変換する場合、きっとこんなコードになるはずだ - これなら確かに並列実行になるだろうと予想できる
Lv.2-_kimoti
const main = async () => {
const promises = [
() => 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"),
]
const results = []
promises.forEach((p) => {
p().then((res) => {
results.push(res)
})
})
console.log(results)
}
main()
Lv.2-- : ダメなパターン Array.prototype.map
- そもそもこれでは
results
の型がPromise<AxiosResponse>[]
になるので、全然ダメ
Lv.2--
const main = async () => {
const promises = [
() => 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"),
]
const results = promises.map(async (p) => {
return await p()
})
// Babel の気持ち
// const results = promises.map(async (p) => {
// return p().then()
// })
console.log(results)
}
main()
Lv.3 : reduce パターン
- ググるとよく出てくるやつ
Lv.3
const main = async () => {
const promises = [
() => 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"),
]
const results = []
await promises.reduce((prev, next) => {
return prev.then((result) => {
results.push(result)
return next()
})
}, Promise.resolve())
console.log(results)
}
main()
- VSCodeを使っていれば、「This may be converted to an async function. ts(80006)」と指摘され、QuickFixで async/await に自動変換できるはず
-
then
が消えて少し読みやすくなる
Lv.3_async
const main = async () => {
const promises = [
() => 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"),
]
const results = []
await promises.reduce(async (prev, next) => {
const res = await prev
results.push(res)
return next()
}, 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+
const main = async () => {
const promises = [
() => 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"),
]
const results = []
await promises
.concat(() => Promise.resolve())
.reduce(async (prev, next) => {
const res = await prev
// reduce の initialValue の resolve 結果を、結果配列に push させないため undefined チェック
if (res != null) {
results.push(res)
}
return next()
}, Promise.resolve())
console.log(results)
}
main()
- うーん。汚い(白目)
-
// @ts-check
を使ってると型推論結果がうまく合わないのか、エラーが出て更にストレスマッハ -
reduce
使ってるのにresults.push
が必要で mutable になっているのもダサい
-
- そもそもなぜ「問題2」が
.concat(() => Promise.resolve())
で解決できるのか?- これを理解するため、ここで JavaScript の気持ちになって
reduce
をバラしてみる
- これを理解するため、ここで JavaScript の気持ちになって
Lv.3+_kimoti
const main = async () => {
const promises = [
() => 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())
const results = []
// 1ループ目
const res = await Promise.resolve()
if (res != null) {
results.push(res)
}
const next0 = promises[0]() // siro
// 2ループ目
const res0 = await next0
if (res0 != null) {
results.push(res0)
}
const next1 = promises[1]() // yamato
// 3ループ目
const res1 = await next1
if (res1 != null) {
results.push(res1)
}
const next2 = promises[2]() // kiso
// 4ループ目
const res2 = await next2
if (res2 != null) {
results.push(res2)
}
const next3 = 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++
const sequential = async (promises) => {
const results = []
await promises
.concat(() => Promise.resolve())
.reduce(async (prev, next) => {
const res = await prev
// reduce の initialValue の resolve 結果を、結果配列に push させないため undefined チェック
if (res != null) {
results.push(res)
}
return next()
}, Promise.resolve())
return results
}
const main = async () => {
const promises = [
() => 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"),
]
const results = await sequential(promises)
console.log(results)
}
main()
Lv.Final
- だいぶマシにはなったが、まだ null チェックが目障り
- 無駄な処理を省く・整理する
Lv.Final
const sequential = async (promises) => {
const first = promises.shift()
if (first == null) {
return []
}
const results = []
await promises
// 末尾に空のPromiseがないと、最後のPromiseの結果をresultsにpushできないため
.concat(() => Promise.resolve())
.reduce(async (prev, next) => {
const res = await prev
results.push(res)
return next()
}, Promise.resolve(first()))
return results
}
const main = async () => {
const promises = [
() => 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"),
]
const results = await sequential(promises)
console.log(results)
}
main()
- 改良の余地はあるかもしれないが、とりあえずこれで目的達成
Promise.all との比較
- 完全に同じシグネチャにはできなかった
- 宣言した時点 (
axios.get("...")
の時点) で各非同期処理が開始され、結果的に並列実行になるため - 直列にしたい場合は
function
で包んで、そのfunction
が実行されるまでPromise
が発火しないよう実装する必要があるため
- 宣言した時点 (
Promise.all
const main = async () => {
const promises = [
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"),
]
const results = await Promise.all(promises)
console.log(results)
}
main()
TypeScript + UnitTest
- 下記リポジトリを参照のこと
(TS + Jest の実行環境を簡単に用意するため、「create-react-app
で生成 → 不要なもの削除」で作っている。少し冗長だがご容赦を)
まとめ
- いちいち定義するのめんどくさいので標準化されないかなー
- 気が向いたらもうちょい改良して npm package 化してみようと思う