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 of
loop,就不會有 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
之中切換