Web Developer's Struggle Memories

日々の業務から思ったこと、学んだことを書き連ねていきます。

Array.prototype.map 内で非同期関数を走らせて、配列の要素ごとに処理する +α

ちょっと Promiseasync/await に躓いたのでメモとして書き残しておく。

間違った処理

当初は以下のような処理を書いていた。ある配列の全要素に対し一つ一つ処理を実行し、その結果に対して判定処理をしたかった。

let arr = []

const timeout = (ms) => {
  return new Promise(resolve => setTimeout(() => {
    console.log("waiting...")
    resolve()
  }, ms))
}

// failed pattern
const exe = async () => {
  await [1,2,3].map(async item => {
    console.log(item)
    await timeout(100)
    item > 2 && arr.push(item)
  })
  console.log(arr)
  if (arr.length === 1) console.log('ok')
}

exe()

timeout 関数が実際には API コール(fetch() の実行など)だったりするが、今回は setTimeout するだけのもの。最初はこう書いていて、await timeout(100) のところで毎回止まってくれることを想定していたが、結果は以下。

1
2
3
[]
waiting...
waiting...
waiting...

arr.push(item) のところで配列に値がセットされているのだが、そもそも await timeout(100) のところで処理が止まっておらず、先に map() の処理からメインスレッドに戻ってしまっていた。ちなみに上記の mapforEach に変更しても同様。

修正版

これを以下のように修正したらうまく動作した。

const exe = async () => {
-  await [1,2,3].map(async item => {
+  await Promise.all([1,2,3].map(async item => {
    console.log(item)
    await timeout(100)
    item > 2 && arr.push(item)
-  })
+  }))
  console.log(arr)
  if (arr.length === 1) console.log('ok')
}

exe()

これの実行結果は

1
2
3
waiting...
waiting...
waiting...
[ 3 ]
ok

となり、期待通りの動作。元々の await [1,2,3].map() の処理の戻り値を変数に入れて console.log で見てみると

[ Promise { <pending> }, Promise { <pending> }, Promise { <pending> } ]

の様に Promise の配列になっている事がわかる。なので、これを Promise.all で単一の Promise として処理してしまえば良かった。

参考: Promise.all()

余談

Array.prototype.filter のコールバックに async を付けると本来の挙動をしなくなる。百聞は一見に如かないので、以下のコードを見てほしい。(最初のコードの mapfilter に変えただけ)

// ok
const arr = [1,2,3].filter(item => item > 2)
console.log(arr) // => [3]

// ng
const arr = [1,2,3].filter(async item => item > 2)
console.log(arr) // => [1,2,3]

勉強になった。ではでは(=゚ω゚)ノ