はじめに
この間ループの中で非同期関数を呼び出した際に期待される挙動をしてくれずどハマりしてしまいました。
非同期処理に関する理解を深めたかったため、実際に簡単なコードを書いて試行錯誤した末になんとか解決できたので、その知見を共有したいと思って記事にしてみました。(調べても解決方法が見つけられなかったのも理由のひとつです)
ループ内で非同期でない関数を使用する場合
下記のようなコードがあるとします。
let counter = 0
const countSync = () => ++counter
const makeLoopSync = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve()
})
})
}
makeLoopSync()
.then(() => console.log('Start'))
.then(() => {
const list = Array(3).fill(null)
list.forEach(() => {
const count = countSync()
console.log(count)
})
})
.then(() => console.log('Done'))
このコードの期待される実行結果は下記です。期待する挙動をしてくれていますね。
Start
1
2
3
Done
ループ内で非同期関数を使用する場合
期待する挙動をしてくれないコード
ところが、forEach()
内で使用されている countSync()
が非同期関数だった場合はどのような結果になるでしょうか。
countSync
を countAsync
に変更し中身も非同期にしてみました。
let counter = 0
const countAsync = () => new Promise(resolve => setTimeout(() => { resolve(++counter) }))
const makeLoopSync = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve()
})
})
}
makeLoopSync()
.then(() => console.log('Start'))
.then(() => {
const list = Array(3).fill(null)
list.forEach(() => {
countAsync()
.then(count => console.log(count))
})
})
.then(() => console.log('Done'))
実行結果はこうなります。
Start
Done
1
2
3
さて、上のコードの実行結果を期待されるものにするためにはどうしたらよいでしょうか。
筆者はまず async/await
を使う必要がありそうだなと思いました。(実際には必ずしも async/await
を使う必要はありませんでした)
そして async/await
を使って直感的に書くならこうかなと思い、下記のように書き換えてみました。
makeLoopSync()
.then(() => console.log('Start'))
.then(() => {
const list = Array(3).fill(null)
list.forEach(async () => {
const count = await countAsync()
console.log(count)
})
})
.then(() => console.log('Done'))
})
結果はこうでした。
Start
Done
1
2
3
挙動は上の async/await
を使用しないパターンと同じでした。内部的にも同じことが行われているのだと思われます。
期待する挙動をしてくれるコード
いろいろ試してみた結果、最終的には該当箇所を下記のように変更したら期待する挙動をしてくれました。
嘘でした。以下2つのコードは NG パターンです。countAsync()
の timer をいじるとうまく動かないことを確認しました。
(@u39ueda さん ご指摘ありがとうございます・・!)
makeLoopSync()
.then(() => console.log('Start'))
.then(() => {
return new Promise(resolve => {
const list = Array(3).fill(null)
list.forEach(async () => {
const count = await countAsync()
console.log(count)
resolve()
})
})
})
.then(() => console.log('Done'))
またはこうでしょうか。(async/await
を使わないパターン)
makeLoopSync()
.then(() => console.log('Start'))
.then(() => {
return new Promise(resolve => {
const list = Array(3).fill(null)
list.forEach(() => {
countAsync()
.then((count) => console.log(count))
.then(() => resolve())
})
})
})
.then(() => console.log('Done'))
いずれにせよ Promise を一枚かまさないといけないわけですね。
Promise.all を使用すると安定した挙動になります。(ただし並列処理)
(@u39ueda さん ありがとうございます。)
makeLoopSync()
.then(() => console.log('Start'))
.then(() => {
const list = Array(3).fill(null)
return Promise.all(list.map(async () => {
const count = await countAsync()
console.log(count)
}))
})
.then(() => console.log('Done'))
new Promise(resolve => ) ...
をかます必要がないのですっきりします。
※ たしかループの中も直列で処理できる方法があったと思うので、(Array#reduce を使ったやつ)いずれ追加します。
おわりに
resolve させるのが まだまだ修行が足りませんね・・・。forEach()
の外ではないのが個人的には解せなかったです。
ただ、やはりなにかの理解を深める(学ぶ)方法として、自分で手を動かして試行錯誤するというのは非常に効果的だなとあらためて感じました。スポーツと似ていますね。