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 化してみようと思う
