ECMAScript 2015 最大特色是將 Promise 定為語言標準,以 Promise 取代 Callback,當在 forEach 內使用 ECMAScript 2017 的 Asynchronous Function 時,會有意想不到結果。
Version
ECMAScript 2017
Asynchronous in For Loop
let data = [1, 2, 3]
let inc = async x => x + 1
let f = async a => {
let sum = 0
for (let x of a)
sum = sum + await inc (x)
return sum
}
f (data) // ?
第 3 行
let inc = async x => x + 1
ES2017 引進了 async await,只要在 function 名稱前加上 async,則回傳為 Promise,而非單純 Number。
第 8 行
for (let x of a)
sum = sum + await inc (x);
使用 for loop 調用 inc 累加至 sum。
因為 inc 為 async function 回傳 Promise,需使用 await 解開 Promise。
第 5 行
let f = async a => {
...
}
因為 f 內使用了 await 解開 Promise,ES6 規定在 function 前面要宣告為 async 成為 async function 再次包進 Promise 回傳。
儘管因為
inc宣告為async,f也成為 async function,但整體思維與寫法還是與 sync function 很像,這就是async await魅力。

Asynchronous in forEach
let data = [1, 2, 3]
let inc = async x => x + 1
let f = a => {
let sum = 0
a.forEach (async x => sum = sum + await inc (x))
return sum
}
f (data) // ?
第 7 行
a.forEach (async x => sum = sum + await inc (x))
隨著 Array.prototype 自帶 forEach 後,由於可搭配 ES6 的 arrow function,因此越來越多人使用 forEach 取代 for of loop。
由於 await 是寫在 arrow function 內,因此 arrow function 也要宣告為 async。

但實際執行卻為非預期的 0,why not ?
第 7 行
a.forEach (async x => sum = sum + await inc (x))
forEach 為 sync function,會一行一行在 stack 執行,這沒問題。
但其 callback 為 async function,根據 event loop model,該 callback 並不會立即執行,而是先擺到 callback queue 排隊,等全部 sync function 都執行完後,才會執行 callback queue 中的 async function 。
第 8 行
return sum
此為 synchronous 會先執行,但因為 callback quene 中所有的 async function 皆還未執行,因此 sum 仍然為初始值 0。
Q:所以
await沒有等待inc的 Promise 嗎 ?
await 仍然有等待 inc 的 Promise,但問題是在 callback queue 執行 async function 時才 await 等待 Promise,此時 return sum 早已執行過了,所以 sum 還是 0,因此 await 等待也沒用。
syncForEach
let data = [1, 2, 3]
let inc = async x => x + 1
Array.prototype.syncForEach = function (f) {
for (let i = 0; i < this.length; i++)
f (this [i], i, this)
}
let f = a => {
let sum = 0
a.syncForEach (async x => sum = sum + await inc (x))
return sum
}
f (data) // ?
Q:之前這樣解釋還是似懂非懂,可以用 code 解釋嗎 ?
第 5 行
Array.prototype.syncForEach = function (f) {
for (let i = 0; i < this.length; i++)
f (this [i], i, this)
}
forEach 其內部實作相當於 syncForEach,是封裝 for loop 的 higher order function。
但這裡有個問題,因為 inc 是 async function,導致傳入 f 成為 async function,但為什麼 f 之前沒加 await ? 這導致 syncForEach 成為在 synchronous 去執行 asynchronous f,所以結果是錯的。

asyncForEach
let data = [1, 2, 3]
let inc = async x => x + 1
Array.prototype.asyncForEach = async function (f) {
for (let i = 0; i < this.length; i++)
await f (this [i], i, this)
}
let f = async a => {
let sum = 0
await a.asyncForEach (async x => sum = sum + await inc (x))
return sum
}
f (data) // ?
第 5 行
Array.prototype.asyncForEach = async function (f) {
for (let i = 0; i < this.length; i++)
await f (this [i], i, this)
}
自行實作 asyncForEach,在 f 前加上 await,也在 function 前加上 async 成為 async function,這使得 asyncForEach 是在 asynchronous 去執行 asynchronous f。
10 行
let f = async a => {
let sum = 0
await a.asyncForEach (async x => sum = sum + await inc (x))
return sum
}
如此除了 asyncForEach 的 callback 要加上 async await 外,連 asyncForEach 前也要加上 await,最後使 f 成為 asynchronous function,如此所有的 function 都在 asynchronous 下,不再有 sync function 內執行 async function 問題。

Promise.all
let data = [1, 2, 3]
let inc = async x => x + 1
let f = a =>
Promise
.all (a.map (inc))
.then (x => x.reduce ((a, x) => a+=x, 0))
f (data) // ?
與其使用 for loop 或 asyncForEach,其實有更 FP 與 Promise 寫法。
改用 map 使用 inc,結果為 Array 中一堆 Pending Promise,再使用 Promise.all 使 Array 中的 Pending Promise 成為 Fulfilled Promise。
也由於 Promise.all 回傳為 Promise,可用 then 直接修改 Promise 內部資料並回傳新 Promise,在 then 使用 reduce 直接計算其 sum 回傳。
我們可發現整個過程都在 Promise 內處理,沒透過 await 轉成一般值,也沒透過 Imperative 的 for loop 處理,完全使用 FP 的 higher order function 與 pure function 處理,這才是 Promise 初衷。

Conclusion
- 實務上避免在
Array.prototype下的 method 使用async await,因為Array.prototype下的 method 皆為 sync function,也就是forEach其實就是syncForEach,會先執行完最後才執行 callback 內的 async function,導致結果不如預期,此時應該簡單使用for ofloop,就不會有 callback 為 async function 延後執行問題 - 結論是要避免在 sync function 內執行 async function,
forEach本質是syncForEach,因此執行 asyncinc會有問題,除非使用asyncForEach,這也是為什麼 ES6 規定只要使用到await,所有的 function 都要搭配async成為 async function,因為這樣才能避免在 sync function 內執行 async function - Promise 原本的設計是希望你用 higher order function + pure function 直接修改 Promise 內部值,如此可避開 ECMAScript 複雜的 asynchronous 機制,也不必在 Promise 與
await之中切換