Câu 1Nếu sau delay(100, "A").then((v) => console.log(v)); là dòng console.log("B");, kết quả sẽ hiển thị theo thứ tự nào?
Promise — Nhận kết quả bất đồng bộ bằng then và catch
Nhận kết quả của một xử lý mất thời gian vào lúc sau bằng Promise: ba trạng thái, then / catch / finally, chuỗi then và Promise.all.
Có những xử lý mất một lúc mới có kết quả, như gửi đơn hàng hay kiểm tra tồn kho. Để trang không bị đứng trong lúc chờ, bạn viết những đoạn code này sao cho chương trình chạy ngay sang dòng tiếp theo mà không đợi, rồi nhận kết quả vào lúc sau. Cách viết này gọi là xử lý bất đồng bộ (asynchronous).
Bài này nói về Promise, thứ biểu thị một kết quả được xác định vào lúc sau, và Promise.all để chờ nhiều Promise cùng lúc.
Nhận kết quả vào lúc sau — Promise và then
Giả sử phải mất 100ms mới có câu trả lời về tình trạng còn chỗ ngồi. setTimeout (hàm gọi một hàm khác đúng một lần sau số mili giây chỉ định) trả về ngay lập tức và chương trình chạy tiếp sang dòng sau, nên bạn không thể return từ hàm của mình một câu trả lời đến sau 100ms.
Thay vào đó, bạn trả về một Promise (object biểu thị một kết quả được xác định vào lúc sau), và câu trả lời sẽ được đưa tới qua nó. Hàm bạn truyền cho new Promise((resolve) => { ... }) được gọi ngay lập tức, và khi gọi resolve(giá trị) bên trong, Promise chuyển từ pending (đang chờ kết quả) sang fulfilled (thành công). Hàm bạn truyền cho phương thức then sẽ được gọi với giá trị đó khi Promise đã fulfilled.
// Trả về một Promise sẽ fulfilled với value sau ms mili giây
function delay(ms, value) {
return new Promise((resolve) => {
setTimeout(() => resolve(value), ms);
});
}
// Bắt đầu kiểm tra (lúc này vẫn đang pending)
const seats = delay(100, "Còn 12 chỗ");
// Truyền trước hàm sẽ được gọi khi đã fulfilled
seats.then((message) => {
console.log(message); // Còn 12 chỗ (hiển thị thứ 2)
});
console.log("Đang kiểm tra tình trạng chỗ ngồi"); // Đang kiểm tra tình trạng chỗ ngồi (hiển thị thứ 1)
then chỉ giữ lấy hàm, không gọi nó ngay tại chỗ. Hàm bạn truyền vào chỉ chạy sau khi các dòng bên dưới đã chạy xong và Promise đã fulfilled.Nếu viết đoạn code dùng kết quả ở bên ngoài then, nó sẽ chạy trước khi kết quả được xác định, nên chưa có giá trị để dùng. Hãy gom các dòng dùng kết quả vào trong hàm truyền cho then, còn những việc muốn làm trong lúc chờ thì viết ở các dòng phía sau, bên ngoài then.
Thêm một dòng chờ ở cuối để thấy kết quả của then
Trình chạy code trên trang này chỉ thu những gì được in ra trước khi dòng cuối cùng chạy xong. Trong trình duyệt hay Node.js, hàm của then vẫn chạy dù không có gì chờ nó, nhưng ở đây bạn thêm await delay(1000); vào cuối để chờ 1 giây. await (cú pháp không chạy sang dòng tiếp theo cho đến khi kết quả của Promise được xác định) sẽ được nói ở bài tiếp theo.
Xử lý thất bại — reject / catch / finally
Việc xác minh mã PIN của thẻ sẽ thất bại nếu nhập sai số. Nếu chỉ có resolve, thất bại cũng tới dưới dạng một giá trị bình thường giống như thành công, nên hàm nhận kết quả lần nào cũng phải dùng if để phân biệt.
Hàm truyền cho new Promise còn nhận được reject ở tham số thứ hai. Khi gọi nó, như reject(new Error("nội dung")), Promise sẽ thành rejected (thất bại). Hàm truyền cho catch nhận Error đó khi Promise bị rejected, còn hàm truyền cho finally được gọi sau cùng, dù thành công hay thất bại.
function verifyPin(pin) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (pin === "4821") {
resolve("Đã xác minh mã PIN");
} else {
// Báo thất bại bằng cách truyền một Error cho reject
reject(new Error("Mã PIN không đúng"));
}
}, 100);
});
}
// then, catch và finally có thể nối tiếp nhau (cách hoạt động được giải thích ở phần sau)
verifyPin("0000")
.then((message) => console.log(message)) // không được gọi vì đã bị rejected
.catch((error) => console.log(error.message)) // Mã PIN không đúng
.finally(() => console.log("Đã xóa trống ô nhập"));
// Đã xóa trống ô nhập (luôn in ra sau cùng, dù thành công hay thất bại)
Khi Promise bị rejected, các hàm then nối trước catch sẽ bị bỏ qua, và hàm catch phía sau được gọi. catch này là phương thức của Promise, khác với câu lệnh try / catch. Nếu quên gọi cả resolve lẫn reject, code sẽ trông như bị treo, không có kết quả mà cũng không có lỗi.
Chạy các bước theo thứ tự — chuỗi then và return
Giả sử sau khi đặt hàng, bạn muốn gửi email xác nhận bằng mã đơn hàng nhận về. Nếu viết yêu cầu thứ hai bên trong then đầu tiên, rồi yêu cầu thứ ba lại nằm trong đó, thì mỗi bước thêm vào code lại thụt lề sâu hơn và càng khó đọc.
then trả về một Promise mới, nên bạn nối được các lời gọi như .then(...).then(...). Cách viết này gọi là chuỗi (chain — nối then tiếp theo vào Promise mà then trước trả về). Giá trị bạn return từ một hàm then sẽ được hàm then tiếp theo nhận.
// delay giống với code ở phần đầu
const placeOrder = () => delay(100, "A-2046");
const sendMail = (orderId) => delay(100, `Đã gửi email xác nhận cho ${orderId}`);
placeOrder()
.then((orderId) => {
console.log(`Mã đơn hàng: ${orderId}`); // Mã đơn hàng: A-2046
return sendMail(orderId); // chuỗi chờ đến khi email được gửi xong
})
.then((result) => {
console.log(result); // Đã gửi email xác nhận cho A-2046
return "Đã hoàn tất mọi bước"; // giá trị được chuyển thẳng cho hàm tiếp theo
})
.then((status) => console.log(status)); // Đã hoàn tất mọi bước
Không có return thì hàm trả về undefined, nên then tiếp theo chạy luôn mà không chờ email được gửi, và result là undefined. Không có lỗi nào hiện ra, chỉ có kết quả gửi bị mất đi, nên đây là lỗi rất dễ bỏ sót.
Đã thêm ngoặc nhọn thì không được bỏ return
Arrow function không có ngoặc nhọn như .then((id) => sendMail(id)) trả về nguyên Promise của sendMail, nên then tiếp theo sẽ chờ. Nếu bạn bọc thân hàm trong ngoặc nhọn để chèn thêm một dòng log, hãy viết lại return, như return sendMail(id);.
Chờ nhiều kết quả cùng lúc — Promise.all
Giả sử khi mở trang quản trị, bạn gửi riêng từng yêu cầu lấy doanh thu hôm nay, số đơn hàng và số yêu cầu hỗ trợ, rồi hiển thị bảng tổng hợp khi đã có đủ cả ba. Nếu gắn then riêng cho từng Promise, thì Promise nào về sau cùng mỗi lần một khác, nên không có một chỗ cố định để kiểm tra rằng cả ba đã về đủ.
Nếu gọi ba hàm rồi truyền các Promise nhận được cho Promise.all (phương thức nhận một mảng Promise và chờ đến khi có đủ mọi kết quả), bạn sẽ nhận cả ba kết quả cùng lúc dưới dạng một mảng. Chỉ cần một Promise bị rejected, chương trình sẽ đi vào catch thay vì then.
// delay giống với code ở phần đầu
const loadSales = () => delay(100, 184000);
const loadOrders = () => delay(50, 42);
const loadInquiries = () => delay(80, 3);
// Cả ba bắt đầu ngay khi các hàm được gọi. Promise.all chờ đến khi có đủ
Promise.all([loadSales(), loadOrders(), loadInquiries()])
// Nhận mảng kết quả bằng cách destructuring theo vị trí
.then(([sales, orders, inquiries]) => {
// Nhận theo thứ tự viết trong mảng, không phải thứ tự hoàn thành
console.log(`Doanh thu: ${sales} yên`); // Doanh thu: 184000 yên
console.log(`Đơn hàng: ${orders}`); // Đơn hàng: 42
console.log(`Yêu cầu hỗ trợ: ${inquiries}`); // Yêu cầu hỗ trợ: 3
})
.catch((error) => console.log(error.message));
Bản thân Promise.all không bắt đầu yêu cầu nào; nó chỉ chờ đến khi có đủ kết quả. Bảng dưới đây tóm tắt, với từng trạng thái của các Promise truyền vào, Promise mà Promise.all trả về và hàm nào được gọi.
| Trạng thái các Promise truyền vào | Promise do Promise.all trả về | Hàm được gọi và giá trị nhận được |
|---|---|---|
| Tất cả đều fulfilled | Fulfilled khi Promise cuối cùng fulfilled | then nhận một mảng theo thứ tự đã truyền vào |
| Một Promise bị rejected | Rejected ngay khi Promise đó bị rejected | catch nhận Error đó |
| Từ hai Promise trở lên bị rejected | Rejected ngay khi Promise đầu tiên bị rejected | catch chỉ nhận Error đầu tiên |
| Các Promise khác đã fulfilled, nhưng còn một Promise vẫn pending | Vẫn là pending | Cả then lẫn catch đều không được gọi |
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Nếu nối .then(A).catch(B).finally(C) vào một Promise bị rejected, những hàm nào được gọi?
Câu 3then của Promise.all([delay(100, "A"), delay(50, "B")]) nhận được mảng nào?