Căn bản async
và await
Thì đơn giản. Mọi thứ trở nên phức tạp hơn một chút khi bạn cố gắng sử dụng await
trong các vòng lặp.
Trong bài viết này, tôi muốn chia sẻ một số vấn đề cần lưu ý nếu bạn có ý định sử dụng await
trong các vòng lặp.
Trước khi bắt đầu
Tôi sẽ cho rằng bạn biết cách sử dụng async
và await
. Nếu không, hãy đọc bài viết trước để làm quen trước khi tiếp tục.
Chuẩn bị một ví dụ
Đối với bài viết này, giả sử bạn muốn lấy số lượng trái cây từ một giỏ trái cây.
const fruitBasket = {
apple: 27,
grape: 0,
pear: 14
};
Bạn muốn lấy số lượng của mỗi loại trái cây từ fruitBasket. Để có được số lượng trái cây, bạn có thể sử dụng một getNumFruit
chức năng.
const getNumFruit = fruit => {
return fruitBasket[fruit];
};
const numApples = getNumFruit(“apple”);
console.log(numApples); // 27
Bây giờ, hãy nói fruitBasket
sống trên một máy chủ từ xa. Truy cập nó mất một giây. Chúng ta có thể giả định độ trễ một giây này bằng thời gian chờ. (Vui lòng tham khảo bài viết trước nếu bạn gặp khó khăn trong việc hiểu mã thời gian chờ).
const sleep = ms => {
return new Promise(resolve => setTimeout(resolve, ms));
};
const getNumFruit = fruit => {
return sleep(1000).then(v => fruitBasket[fruit]);
};
getNumFruit(“apple”).then(num => console.log(num)); // 27
Cuối cùng, giả sử bạn muốn sử dụng await
và getNumFruit
để lấy số lượng của từng loại trái cây trong hàm không đồng bộ.
const control = async _ => {
console.log(“Start”);
const numApples = await getNumFruit(“apple”);
console.log(numApples);
const numGrapes = await getNumFruit(“grape”);
console.log(numGrapes);
const numPears = await getNumFruit(“pear”);
console.log(numPears);
console.log(“End”);
};

Với điều này, chúng ta có thể bắt đầu xem xét await
trong các vòng lặp.
Chờ đợi trong vòng lặp for
Giả sử chúng ta có một loạt trái cây mà chúng ta muốn lấy từ giỏ trái cây.
const fruitsToGet = [“apple”, “grape”, “pear”];
Chúng ta sẽ lặp qua mảng này.
const forLoop = async _ => {
console.log(“Start”);
for (let index = 0; index < fruitsToGet.length; index++) {
// Get num of each fruit
}
console.log(“End”);
};
Trong vòng lặp for, chúng ta sẽ sử dụng getNumFruit
để lấy số lượng mỗi quả. Chúng tôi cũng sẽ đăng nhập số vào bảng điều khiển.
Từ getNumFruit
trả lại một lời hứa, chúng ta có thể await
giá trị đã giải quyết trước khi ghi nhật ký.
const forLoop = async _ => {
console.log(“Start”);
for (let index = 0; index < fruitsToGet.length; index++) {
const fruit = fruitsToGet[index];
const numFruit = await getNumFruit(fruit);
console.log(numFruit);
}
console.log(“End”);
};
Khi bạn sử dụng await
, bạn muốn JavaScript tạm dừng thực thi cho đến khi lời hứa được chờ đợi được giải quyết. Điều này có nghĩa là await
s trong vòng lặp for sẽ được thực thi nối tiếp.
Kết quả là những gì bạn mong đợi.
“Start”;
“Apple: 27”;
“Grape: 0”;
“Pear: 14”;
“End”;

Hành vi này hoạt động với hầu hết các vòng lặp (như while
và for-of
vòng lặp)…
Nhưng nó sẽ không hoạt động với các vòng lặp yêu cầu gọi lại. Ví dụ về các vòng lặp như vậy yêu cầu dự phòng bao gồm forEach
, map
, filter
và reduce
. Chúng ta sẽ xem làm thế nào await
ảnh hưởng đến forEach
, map
và filter
trong vài phần tiếp theo.
Đang chờ trong vòng lặp forEach
Chúng ta sẽ làm điều tương tự như đã làm trong ví dụ về vòng lặp for. Đầu tiên, hãy duyệt qua mảng trái cây.
const forEachLoop = _ => {
console.log(“Start”);
fruitsToGet.forEach(fruit => {
// Send a promise for each fruit
});
console.log(“End”);
};
Tiếp theo, chúng tôi sẽ cố gắng lấy số lượng trái cây với getNumFruit
. (Chú ý các async
từ khóa trong chức năng gọi lại. Chúng tôi cần cái này async
từ khóa vì await
nằm trong chức năng gọi lại).
const forEachLoop = _ => {
console.log(“Start”);
fruitsToGet.forEach(async fruit => {
const numFruit = await getNumFruit(fruit);
console.log(numFruit);
});
console.log(“End”);
};
Bạn có thể mong đợi giao diện điều khiển trông như thế này:
“Start”;
“27”;
“0”;
“14”;
“End”;
Nhưng kết quả thực tế lại khác. JavaScript tiếp tục gọi console.log('End')
trước khi các lời hứa trong vòng lặp forEach được giải quyết.
Bảng điều khiển đăng nhập theo thứ tự này:
‘Start’
‘End’
‘27’
‘0’
‘14’

JavaScript làm điều này bởi vì forEach
không nhận thức được lời hứa. Nó không thể hỗ trợ async
và await
. Bạn _không thể_ sử dụng await
Trong forEach
.
Chờ đợi với bản đồ
Nếu bạn dùng await
trong một map
, map
sẽ luôn trả về một loạt lời hứa. Điều này là do các chức năng không đồng bộ luôn trả về lời hứa.
const mapLoop = async _ => {
console.log(“Start”);
const numFruits = await fruitsToGet.map(async fruit => {
const numFruit = await getNumFruit(fruit);
return numFruit;
});
console.log(numFruits);
console.log(“End”);
};
“Start”;
“[Promise, Promise, Promise]”;
“End”;

Start
‘, ‘[Promise, Promise, Promise]’ và ‘Kết thúc’ ngay lập tứcTừ map
luôn trả lại lời hứa (nếu bạn sử dụng await
), bạn phải đợi một loạt các lời hứa được giải quyết. Bạn có thể làm điều này với await Promise.all(arrayOfPromises)
.
const mapLoop = async _ => {
console.log(“Start”);
const promises = fruitsToGet.map(async fruit => {
const numFruit = await getNumFruit(fruit);
return numFruit;
});
const numFruits = await Promise.all(promises);
console.log(numFruits);
console.log(“End”);
};
Đây là những gì bạn nhận được:
“Start”;
“[27, 0, 14]”;
“End”;

Bạn có thể thao túng giá trị bạn trả lại trong lời hứa của mình nếu muốn. Các giá trị được giải quyết sẽ là các giá trị bạn trả về.
const mapLoop = async _ => {
// …
const promises = fruitsToGet.map(async fruit => {
const numFruit = await getNumFruit(fruit);
// Adds onn fruits before returning
return numFruit + 100;
});
// …
};
“Start”;
“[127, 100, 114]”;
“End”;
Đang chờ với bộ lọc
Khi bạn sử dụng filter
, bạn muốn lọc một mảng với một kết quả cụ thể. Giả sử bạn muốn tạo một mảng có hơn 20 loại trái cây.
Nếu bạn dùng filter
thông thường (không chờ đợi), bạn sẽ sử dụng nó như thế này:
// Filter if there’s no await
const filterLoop = _ => {
console.log(‘Start’)
const moreThan20 = await fruitsToGet.filter(fruit => {
const numFruit = fruitBasket[fruit]
return numFruit > 20
})
console.log(moreThan20)
console.log(‘End’)
}
Bạn mong chờ moreThan20
chỉ chứa táo vì có 27 quả táo, nhưng có 0 quả nho và 14 quả lê.
“Start”[“apple”];
(“End”);
await
Trong filter
không hoạt động theo cùng một cách. Trên thực tế, nó không hoạt động chút nào. Bạn lấy lại mảng chưa được lọc …
const filterLoop = _ => {
console.log(‘Start’)
const moreThan20 = await fruitsToGet.filter(async fruit => {
const numFruit = getNumFruit(fruit)
return numFruit > 20
})
console.log(moreThan20)
console.log(‘End’)
}
“Start”[(“apple”, “grape”, “pear”)];
(“End”);

Đây là lý do tại sao nó xảy ra.
Khi bạn sử dụng await
trong một filter
gọi lại, gọi lại luôn là một lời hứa. Vì lời hứa luôn trung thực nên mọi mục trong mảng đều vượt qua bộ lọc. Viết await
trong một filter
giống như viết mã này:
// Everything passes the filter…
const filtered = array.filter(true);
Có ba bước để sử dụng await
và filter
đúng:
1. Sử dụng map
để trả lại một lời hứa mảng
2. await
hàng loạt lời hứa
3. filter
các giá trị giải quyết
const filterLoop = async _ => {
console.log(“Start”);
const promises = await fruitsToGet.map(fruit => getNumFruit(fruit));
const numFruits = await Promise.all(promises);
const moreThan20 = fruitsToGet.filter((fruit, index) => {
const numFruit = numFruits[index];
return numFruit > 20;
});
console.log(moreThan20);
console.log(“End”);
};
Start[“apple”];
End;

Đang chờ giảm
Trong trường hợp này, giả sử bạn muốn tìm tổng số trái cây trong fruitBastet. Thông thường, bạn có thể sử dụng reduce
để lặp qua một mảng và tính tổng số.
// Reduce if there’s no await
const reduceLoop = _ => {
console.log(“Start”);
const sum = fruitsToGet.reduce((sum, fruit) => {
const numFruit = fruitBasket[fruit];
return sum + numFruit;
}, 0);
console.log(sum);
console.log(“End”);
};
Bạn sẽ nhận được tổng cộng 41 loại trái cây. (27 + 0 + 14 = 41).
“Start”;
“41”;
“End”;

Khi bạn sử dụng await
với reduce, kết quả trở nên vô cùng lộn xộn.
// Reduce if we await getNumFruit
const reduceLoop = async _ => {
console.log(“Start”);
const sum = await fruitsToGet.reduce(async (sum, fruit) => {
const numFruit = await getNumFruit(fruit);
return sum + numFruit;
}, 0);
console.log(sum);
console.log(“End”);
};
“Start”;
“[object Promise]14”;
“End”;

Gì?! [object Promise]14
?!
Mổ xẻ điều này thật thú vị.
- Trong lần lặp đầu tiên,
sum
Là.
numFruit
là 27 (giá trị được phân giải từgetNumFruit(‘apple’)
).0 + 27
là 27. - Trong lần lặp thứ hai,
sum
là một lời hứa. (Tại sao? Bởi vì các hàm không đồng bộ luôn trả về lời hứa!)numFruit
là 0. Một lời hứa không thể được thêm vào một đối tượng thông thường, vì vậy JavaScript chuyển đổi nó thành[object Promise]
chuỗi.[object Promise] + 0
Là[object Promise]0
- Trong lần lặp thứ ba,
sum
cũng là một lời hứa.numFruit
Là14
.[object Promise] + 14
Là[object Promise]14
.
Bí ẩn đã được giải quyết!
Điều này có nghĩa là, bạn có thể sử dụng await
trong một reduce
gọi lại, nhưng bạn phải nhớ await
bộ tích lũy đầu tiên!
const reduceLoop = async _ => {
console.log(“Start”);
const sum = await fruitsToGet.reduce(async (promisedSum, fruit) => {
const sum = await promisedSum;
const numFruit = await getNumFruit(fruit);
return sum + numFruit;
}, 0);
console.log(sum);
console.log(“End”);
};
“Start”;
“41”;
“End”;

Nhưng… như bạn có thể thấy từ gif, phải mất khá nhiều thời gian để await
mọi thứ. Điều này xảy ra bởi vì reduceLoop
cần phải đợi cho promisedSum
được hoàn thành cho mỗi lần lặp.
Có một cách để tăng tốc vòng lặp giảm. (Tôi phát hiện ra điều này nhờ Tim Oxley. nếu bạn await getNumFruits(
) đầu tiên trước await promisedSum
các reduceLoop
chỉ mất một giây để hoàn thành:
const reduceLoop = async _ => {
console.log(“Start”);
const sum = await fruitsToGet.reduce(async (promisedSum, fruit) => {
// Heavy-lifting comes first.
// This triggers all three getNumFruit promises before waiting for the next iteration of the loop.
const numFruit = await getNumFruit(fruit);
const sum = await promisedSum;
return sum + numFruit;
}, 0);
console.log(sum);
console.log(“End”);
};

Điều này hoạt động bởi vì reduce
có thể bắn cả ba getNumFruit
lời hứa trước khi chờ đợi lần lặp tiếp theo của vòng lặp. Tuy nhiên, phương pháp này hơi khó hiểu vì bạn phải cẩn thận với thứ tự mà bạn await
nhiều thứ.
Cách đơn giản nhất (và hiệu quả nhất) để sử dụng await
trong giảm là:
1. Sử dụng map
để trả lại một lời hứa mảng
2. await
hàng loạt lời hứa
3. reduce
các giá trị giải quyết
const reduceLoop = async _ => {
console.log(“Start”);
const promises = fruitsToGet.map(getNumFruit);
const numFruits = await Promise.all(promises);
const sum = numFruits.reduce((sum, fruit) => sum + fruit);
console.log(sum);
console.log(“End”);
};
Phiên bản này rất dễ đọc và dễ hiểu, chỉ mất một giây để tính tổng số quả.

Chìa khóa rút ra
1. Nếu bạn muốn thực hiện await
cuộc gọi nối tiếp, hãy sử dụng một for-loop
(hoặc bất kỳ vòng lặp nào không có lệnh gọi lại).
2. Đừng bao giờ sử dụng await
với forEach
. Sử dụng một for-loop
(hoặc bất kỳ vòng lặp nào không có lệnh gọi lại) để thay thế.
3. Đừng await
phía trong filter
và reduce
. Luôn await
một loạt các lời hứa với map
sau đó filter
hoặc reduce
cho phù hợp.
Bài viết này ban đầu được đăng trên blog của tôi.
Đăng ký nhận bản tin của tôi nếu bạn muốn có nhiều bài viết hơn để giúp bạn trở thành nhà phát triển giao diện người dùng tốt hơn.