點燈坊

失くすものさえない今が強くなるチャンスよ

在 forEach 內使用 Asynchronous Function

Sam Xiao's Avatar 2021-08-06

Array.prototype.forEach 只能接受傳入 Synchronous Function,若要傳入 Asynchronous Function,可依需求使用不同方式實現。

Version

ECMAScript 2017

Synchronous forEach

let data = [1, 2, 3]

data.forEach (x => console.log (x))

console.log ('Finished sync')

forEach 用來處理 side effect,可傳入 sync function,如預期先印出 data,最後印出 Finished sync

foreach000

Asynchronous forEach

let data = [1, 2, 3]

let sleep = ms => new Promise (resolve => setTimeout (resolve, ms))

data.forEach (async x => {
  await sleep (10 - x)
  console.log (x)
})

console.log ('Finished async')

若對 forEach 傳入 async function,會發現 Finished async 先印出,然後才印出 data,且會根據實際 delay 時間印出。

foreach001

Promise.all + map

let data = [1, 2, 3]

let sleep = ms => new Promise (resolve => setTimeout (resolve, ms))

await Promise.all (data.map (async x => {
  await sleep (10 - x)
  console.log (x)
}))

console.log ('Finished async')

若需求是 async function 先印出 data,最後才印出 Finished async,則必須改用 Promise.all + map 組合:

  • map:印出 data 內每個 element 並回傳 Array Promise
  • Promise.all:確保 Array Promise 內所有 Promise 都執行完,才回傳新 Promise Array
  • await Promise.all:由於 Promise.all 亦回傳 Promise,因次要加上 await 確保 async function 都執行完後才印出 Finished async

foreach002

reduce

let data = [1, 2, 3]

let sleep = ms => new Promise (resolve => setTimeout (resolve, ms))

await data.reduce (async (ac, x) => {
  await ac
  await sleep (10 - x)
  console.log (x)
}, 0)

console.log ('Finished async')

若需求是儘管 async function 先印出 data,也要根據 code 執行順序印出,而非根據實際 delay 時間印出,則必須改用 reduce 實現:

  • await ac:無論 async function 實際 delay 多久,都會被 await ac 擋住
  • await reduce:由於傳入 async function 回傳 Promise,經過 reduce 累積後亦回傳 Promise,因次要加上 await 確保 async function 都執行完後才印出 Finished async

foreach003

for Loop + await

let data = [1, 2, 3]

let sleep = ms => new Promise (resolve => setTimeout (resolve, ms))

for (let x of data) {
  await sleep (10 - x)
  console.log (x)
}

console.log ('Finished async')

若需求是儘管 async function 先印出 data,也要根據 code 執行順序印出,而非根據實際 delay 時間印出,除了使用 reduce 實現外,也可簡單使用 for loop + await 實現:

  • await sleep (10 - x):無論 sleep delay 多久,await 都會等待 sleep 執行完,因此不受實際 delay 影響

foreach004

Conclusion

  • mapreduce 原本不是用來處理 side effect,但由於 forEach 接受 async function 時有其限制,只好藉由 mapreduce 實現一些 forEach 做不到事情
  • 由於 forEach 本來就不是設計用來使用 async function,若需求就是不考慮實際 delay 影響,又覺得使用 reduce 很 tricky,可改用 for loop + await 較直覺,這也是少數 for loop 較適用場合

Reference

Tamas Sallai, How to use async functions with Array.forEach in JavaScript