HomeLập trìnhJavaScriptJavaScript không đồng...

JavaScript không đồng bộ và chờ đợi trong các vòng lặp


Căn bản asyncawait 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 asyncawait. 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 awaitgetNumFruit để 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”);
};
n5Qv0F00AiCZqsRsu7NPh3S3HFp-ZWXYTwuH
Bảng điều khiển hiển thị ‘Bắt ​​đầu’. Một giây sau, nó ghi 27. Một giây sau, nó ghi 0. Một giây nữa, nó ghi 14 và ‘Kết thúc’

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à awaits trong vòng lặp for sẽ được thực thi nối tiếp.

Đọc thêm  Ví dụ về chuỗi con JavaScript - Phương thức Slice, Substr và Substring trong JS

Kết quả là những gì bạn mong đợi.

“Start”;
“Apple: 27”;
“Grape: 0”;
“Pear: 14”;
“End”;
sU0OzGuNuH5BwMoYmNcmNLfQzcexW0m7e08K
Bảng điều khiển hiển thị ‘Bắt ​​đầu’. Một giây sau, nó ghi 27. Một giây sau, nó ghi 0. Một giây nữa, nó ghi 14 và ‘Kết thúc’

Hành vi này hoạt động với hầu hết các vòng lặp (như whilefor-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, filterreduce. Chúng ta sẽ xem làm thế nào await ảnh hưởng đến forEach, mapfilter 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’
DwUgo9TAK8PXNLIAv3-27UZMSrEkfNx778hS
Bảng điều khiển ghi ‘Bắt ​​đầu’ và ‘Kết thúc’ ngay lập tức. Một giây sau, nó ghi lại 27, 0 và 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ợ asyncawait. 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”;
JD3o7BUxILoFP-hVwv5920JtHJPB8l6IrMNs
Nhật ký bảng điều khiển ‘Start‘, ‘[Promise, Promise, Promise]’ và ‘Kết thúc’ ngay lập tức

Từ 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”;
Clz579WsPZ0Tv4iiA5rN1960VP0xx4x66dAz
Nhật ký bảng điều khiển ‘Bắt ​​đầu’. Một giây sau, nó ghi ‘[27, 0, 14] Và kết thúc’

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.

Đọc thêm  Ví dụ đối sánh Regex JavaScript – Cách sử dụng JS Thay thế trên chuỗi

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”);
xI8y6n2kvda8pz9i7f5ffVu92gs7ISj7My9M
Nhật ký bảng điều khiển ‘Bắt ​​đầu’, ‘[‘apple’, ‘grape’, ‘pear’]’ và ‘Kết thúc’ ngay lập tức

Đâ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 awaitfilter đú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;
KbvkmmX4K77pSq29OVj8jhK0KNdyCYQsH1Sn
Bảng điều khiển hiển thị ‘Bắt ​​đầu’. Một giây sau, bảng điều khiển ghi lại ‘[‘apple’]’ Và kết thúc’

Đ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”;
1cHrIKZU2x4bt0Cl6NmnrNA9eYed0-3n4SIq
Bảng điều khiển ghi nhật ký ‘Bắt ​​đầu’, ’41’ và ‘Kết thúc’ ngay lập tức

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”;
vziO1ieaUzFZNE1p3FlaOBJLEQtAL21CYDKw
Nhật ký bảng điều khiển ‘Bắt ​​đầu’. Một giây sau, nó ghi ‘[object Promise]14’ và ‘Kết thúc’

Gì?! [object Promise]14?!

Mổ xẻ điều này thật thú vị.

  • Trong lần lặp đầu tiên, sum. 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 [object Promise]0
  • Trong lần lặp thứ ba, sum cũng là một lời hứa. numFruit14. [object Promise] + 14[object Promise]14.

Bí ẩn đã được giải quyết!

Đọc thêm  Giới thiệu về cú pháp Spread trong JavaScript

Đ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”;
a3ZEbODMdVmob-31Qh0qfFugbLJoNZudoPwM
Nhật ký bảng điều khiển ‘Bắt ​​đầu’. Ba giây sau, nó ghi ’41’ và ‘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 promisedSumcá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”);
};
Vm9olChpCbbEgBmub6OuOS2r0QD5SwwW9y9i
Nhật ký bảng điều khiển ‘Bắt ​​đầu’. Một giây sau, nó ghi ’41’ và ‘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ả.

rAN2FodB3Ff8FmQvMLy4R3mii1Kt41jRszkE
Nhật ký bảng điều khiển ‘Bắt ​​đầu’. Một giây sau, nó ghi ’41’ và ‘End’

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 filterreduce. Luôn await một loạt các lời hứa với mapsau đó 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.





Zik.vn – Biên dịch & Biên soạn Lại

spot_img

Create a website from scratch

Just drag and drop elements in a page to get started with Newspaper Theme.

Buy Now ⟶

Bài viết liên quang

DMCA.com Protection Status