讓JS array reduce與async/await共舞

MengWei Chen
7 min readSep 21, 2017

--

本文主要目的是介紹如何在javasctipt array的reduce裡面搭配async跟awit使用,並說明實作的原理。
文內會用到大量的async/await還有promise的技巧,若對這兩個東西概念還有點模糊的話,建議可以先去看一下這篇文章
http://huli.logdown.com/posts/292655-javascript-promise-generator-async-es6

會寫這篇文章的契機主要是前陣子FB讀書會社團有很有趣的議題,如何將一組用Array包起來的Promise function“依序執行”

例如

const delayPromise = data =>
new Promise((resolve, reject) => {
setTimeout(() => {
console.log(Date());
console.log(data);
resolve(data);
}, 1000);
});

const problem = [1, 2, 4, 5].map(delayPromise)

上面的程式是有問題的,原因是因為map的實作原理是”同步”的將數值”一個一個“帶入delayPromise當中。
為了方便理解,我們可以把map的簡易實作看成下列的程式碼

const mapFunc = (mapper, dataArray) => {
let res = [];
for(const i in dataArray) {
res[i] = mapper(dataArray[i]);
}
return res;
}
mapFunc(delayPromise, [1, 2, 4, 5]);

我們可以很清楚的發現,先前的[1, 2, 4, 5].map(delayPromise)其實是一個用迴圈同步呼叫promise的實作,因此結果並非依序執行promise

那到底要怎麼讓promise依序執行呢?

FB讀書會社團上提出了幾種不同的解法

1.用reduce將promise串接到Promise.resolve()後面

[1, 2, 3, 4].reduce(
(p, current) => p.then(() => delayPromise(current)),
Promise.resolve()
)
/* 等效於
Promise.resolve()
.then(()=>delayPromise(1))
.then(()=>delayPromise(2))
.then(()=>delayPromise(3))
.then(()=>delayPromise(4))
*/

2.用async/await去解決

const asyncWay = async(dataArray) => {
for(const i in dataArray) {
await delayPromise(dataArray[i]);
}
}
asyncWay([1, 2, 3, 4])

3.用ramda解決

const R require('ramda');const ramdaWay = R.pipe(R.map, R.apply(R.pipeP), R.call);ramdaWay((data) => () => delayPromise(data), [1, 2, 3, 4])/* 等效於
R.pipeP(
() => delayPromise(1),
() => delayPromise(2),
() => delayPromise(3),
() => delayPromise(4)
)
*/

後來我也跟一些網路上的朋友討論,發現了其實還有第四種方式,也就是今天的主題reduce 搭配async/await的解法

先來看一下這個解法長什麼樣子

[1, 2, 3, 4].reduce(async(acc, current) => {
if (acc) await acc;
return await delayPromise(current);
})

我第一次聽到這個解法的時候直覺應該會跟直接map一樣全部一起執行,但這個解法之所以可以正確work的關鍵其實是在await acc;上面,接下來我們就來一步一步拆解。

首先我們要先了解reduce到底是怎麼工作的,因此我參考了MDN的reduce polyfill寫了一個簡化版的reduce

const reduceFunc = (reducer, initVal, dataArray) => {
let val = initVal;
for(const i in dataArray) {
val = reducer(val, dataArray[i]);
}
return val;
}
reduceFunc((acc, curreny) => (acc + current), 0, [1, 2, 3, 4])
// 10

觀察這個reduceFunc後,可以發現reduce在實現上是有“遞迴”的概念在當中,在reducer function中的acc其實就是前一次執行reducer function的結果,我們可以把上面的範例結果看成

const res = 
((acc, curreny) => (acc + current))(
(acc, curreny) => (acc + current))(
(acc, curreny) => (acc + current))(1,2),
3
),
4
)
// 整理一下就等效於(((1+2)+3)+4)

因此我們可以把我們的async/await範例進行拆解

const reduceFunc = (reducer, initVal, dataArray) => {
let val = initVal;
for(const i in dataArray) {
val = reducer(val, dataArray[i]);
}
return val;
}
reduceFunc(
async(acc, current) => {
await acc;
return await delayPromise(current);
},
Promise.resolve,
[1, 2, 3, 4]
)
// 等效於const res =
(async () => {
await (async () => {
await (async () => {
await (async () => {
await Promise.resolve();
return await delayPromise(1);
})()
return await delayPromise(2);
})()
return await delayPromise(3);
})()
return await delayPromise(4);
})()

幫各位把重點跟迭代順序對應標起來

由上面的例子可以明顯的看出reducer function的第一個參數acc其實就是前一次遞迴的結果,因此我們在async reducer內使用await acc其實就是在等前一次的遞迴所產生的promise resolve結果!

相關的範例都放在code pen上
https://codepen.io/felixchen/pen/jGrVaX?editors=0001

延伸閱讀
JavaScript async/await 的奇淫技巧
http://fred-zone.blogspot.tw/2017/04/javascript-asyncawait.html

--

--