LoginSignup
3
1

More than 5 years have passed since last update.

ループの中で非同期関数を呼び出す際に期待する挙動をしてくれないとき

Last updated at Posted at 2018-08-01

はじめに

この間ループの中で非同期関数を呼び出した際に期待される挙動をしてくれずどハマりしてしまいました。

非同期処理に関する理解を深めたかったため、実際に簡単なコードを書いて試行錯誤した末になんとか解決できたので、その知見を共有したいと思って記事にしてみました。(調べても解決方法が見つけられなかったのも理由のひとつです)

ループ内で非同期でない関数を使用する場合

下記のようなコードがあるとします。

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() が非同期関数だった場合はどのような結果になるでしょうか。

countSynccountAsync に変更し中身も非同期にしてみました。

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() の外ではないのが個人的には解せなかったです。まだまだ修行が足りませんね・・・。

ただ、やはりなにかの理解を深める(学ぶ)方法として、自分で手を動かして試行錯誤するというのは非常に効果的だなとあらためて感じました。スポーツと似ていますね。

関連

3
1
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1