當 JavaScript 的 forEach 遇到 async 時該怎麼解決

前言

forEach 是我們非常常使用的語法,而有些情況下可能需要做一些同步的事情,例如同步請求 AJAX,而這一塊在 forEach 上就很容易踩雷了。

forEach

forEach 非常的好用,你只需要這樣子寫就可以輕鬆使用

1
2
3
4
var arr = [1, 2, 3];
arr.forEach((item) => {
console.log(item); // 1, 2, 3
})

基本上在這邊看起來是沒有什麼問題的,但是如果今天的需求是必須使用迴圈來跑一段 AJAX 三次甚至是十次不等,那這時候結果可能就不同,這邊我寫一段簡易的程式碼給大家玩玩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var arr1 = [1, 3, 5, 7, 9];
var arr2 = [2, 4, 6, 8, 10];

function ajax(status, data, max) {
return new Promise((resolve, reject) => {
const time = Math.floor(Math.random() * max);
setTimeout(() => {
if(status) {
resolve(`成功:${data}`);
} else {
reject(`失敗:${data}`);
}
}, time);
})
}

function fn() {
arr1.forEach((item) => {
ajax(true, item, 10).then((res) => {
console.log(res);
})
});
arr2.forEach((item) => {
ajax(true, item, 10).then((res) => {
console.log(res);
})
});
}
fn();

我們可以看到上面的 AJAX 的結果在輸出時是亂數輸出的,而不是 1, 3, 5, 7… 這種方式,但我們期望的是依照順序來執行,而本身 forEach 是採用併發的方式運作(意旨同時執行、同時發送),因此並不會等另一個結果回傳之後才往下跑,就算你這樣寫也是無效

1
2
3
4
5
6
7
8
9
10
11
async function fn() {
await arr1.forEach(async (item) => {
const res = await ajax(true, item, 10)
console.log(res);
});
await arr2.forEach(async (item) => {
const res = await ajax(true, item, 10)
console.log(res);
});
}
fn();

至於為什麼呢?我們可以拉到 MDN 底下有一段關於 forEach 在舊版本 polyfill 加入的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Array.prototype.forEach = function(callback) {
var T, k;
if (this == null) {
throw new TypeError('this is null or not defined');
}
var O = Object(this);
var len = O.length >>> 0;
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
if (arguments.length > 1) {
T = arguments[1];
}
k = 0;
while (k < len) {
var kValue;
if (k in O) {
kValue = O[k];
callback.call(T, kValue, k, O);
}
k++;
}
};

但是上面那一段確實很難懂,所以我傾向你把它想像成以下這樣,會比較好懂

1
2
3
4
5
Array.prototype.forEach = function(callback) {
for(let i = 0; i < this.length; i++) {
callback(this[i], i, this);
}
}

我們可以看到不論是 MDN 所提供的語法,還是底下簡化的版本,通通都沒有 await,而且我們是將東西丟到原型內再去 callback 的。

那麼該怎麼解決呢?讓我們往下看。

asyncForEach

其中一種方式就是自己包裝一個 asyncawaitforEach,然後掛在原型下:

1
2
3
4
5
Array.prototype.asyncForEach = function(callback) {
for(let i = 0; i < this.length; i++) {
await callback(this[i], i, this);
}
}

接下來讓我們看一下搭配上面修改之後的結果會是怎樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var arr1 = [1, 3, 5, 7, 9];
var arr2 = [2, 4, 6, 8, 10];

function ajax(status, data, max) {
return new Promise((resolve, reject) => {
const time = Math.floor(Math.random() * max);
setTimeout(() => {
if(status) {
resolve(`成功:${data}`);
} else {
reject(`失敗:${data}`);
}
}, time);
})
}

async function fn() {
await arr1.asyncForEach(async (item) => {
const res = await ajax(true, item, 10)
console.log(res);
});
await arr2.asyncForEach(async (item) => {
const res = await ajax(true, item, 10)
console.log(res);
});
}
fn();

你可以發現結果不管怎麼樣都是 1…3…5…7…9 依照順序執行後才往下

當然如果你不想要寫在原型下的話就只需要寫成函式來使用也可以:

1
2
3
4
5
async function asyncForEach(callback) {
await for(let i = 0; i < this.length; i++) {
await callback(this[i], i, this);
}
}

如果你覺得上面寫法太長的話,還可以將 asyncForEach 改成以下會更簡短:

1
2
3
Array.prototype.asyncForEach = async function (fn) {
for (let t of this) await fn(t)
}

for/for…in/for..of

當然你也可以乾脆就使用 for/for...in/for..of

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var arr1 = [1, 3, 5, 7, 9];

function ajax(status, data, max) {
return new Promise((resolve, reject) => {
const time = Math.floor(Math.random() * max);
setTimeout(() => {
if(status) {
resolve(`成功:${data}`);
} else {
reject(`失敗:${data}`);
}
}, time);
})
}

async function fn() {
for(let i = 0; i < arr1.length; i++) {
const res = await ajax(true, arr1[i], 10);
console.log(res);
}

for(let i in arr1) {
const res = await ajax(true, arr1[i], 10);
console.log(res);
}
for(let item of arr1) {
const res = await ajax(true, item, 10);
console.log(res);
}
}

fn();

那麼以上就是解決的方式囉~

參考文獻

Liker 讚賞 (拍手)

如果這一篇筆記文章對你有幫助,希望可以求點支持或 牡蠣 鼓勵 (ノД`)・゜・。

Liker 是一個按讚(拍手)的讚賞機制,每一篇文章最多可以按五下(拍手),按讚過程你是完全不用付費的(除非你想要每個月贊助我 :D),你只需要登入帳號就可以開始按讚。
而 Liker 會依據按讚數量分配獎金給創作者,所以如果你願意按個讚我會非常感謝你唷。

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ