Giới thiệu về Promise
Trong JavaScript, các tác vụ bất đồng bộ (như gọi API, đọc file, hẹn giờ) thường phức tạp để quản lý bằng các callback lồng nhau (callback hell). Để giải quyết vấn đề này, JavaScript đã giới thiệu khái niệm Promise. Một Promise đại diện cho kết quả cuối cùng của một thao tác bất đồng bộ, có thể là thành công hoặc thất bại.
Một Promise có ba trạng thái:
- Pending (Đang chờ): Trạng thái ban đầu, chưa hoàn thành cũng chưa thất bại.
- Fulfilled (Hoàn thành): Thao tác bất đồng bộ đã hoàn tất thành công.
- Rejected (Bị từ chối): Thao tác bất đồng bộ đã thất bại.
Quy tắc chuyển đổi trạng thái của Promise:
- Một Promise bắt đầu ở trạng thái
pending. - Từ
pending, nó có thể chuyển sangfulfilled(gọiresolve) hoặcrejected(gọireject). - Một khi đã chuyển sang
fulfilledhoặcrejected(gọi chung làsettled- đã quyết định), trạng thái của Promise không thể thay đổi nữa.
Cách tạo và sử dụng Promise cơ bản
Bạn tạo một Promise mới bằng cách sử dụng constructor new Promise(), truyền vào một hàm thực thi (executor function) có hai tham số: resolve và reject. Hàm resolve được gọi khi tác vụ thành công, và reject được gọi khi tác vụ thất bại.
const p = new Promise((resolve, reject) => {
console.log("Bắt đầu xử lý tác vụ...");
const thoiGianXuLy = Math.floor(Math.random() * 3000) + 1000; // 1 đến 4 giây
setTimeout(() => {
if (Math.random() > 0.6) {
resolve(`Tác vụ hoàn thành sau ${thoiGianXuLy}ms`); // Tác vụ thành công
} else {
reject("Tác vụ thất bại do lỗi hệ thống!"); // Tác vụ thất bại
}
}, thoiGianXuLy);
});
p.then(
(thongBaoThanhCong) => {
console.log("Thành công:", thongBaoThanhCong);
},
(thongBaoLoi) => {
console.log("Thất bại:", thongBaoLoi);
}
);
console.log("Promise đã được khởi tạo.");
Lưu ý quan trọng: Một Promise chỉ có thể thay đổi trạng thái một lần duy nhất. Các lệnh gọi resolve hoặc reject tiếp theo sẽ bị bỏ qua.
new Promise((resolve, reject) => {
resolve("Giá trị đầu tiên");
resolve("Giá trị thứ hai"); // Bị bỏ qua
reject("Lỗi!"); // Bị bỏ qua
}).then(data => console.log(data)); // Chỉ in ra "Giá trị đầu tiên"
Xử lý lỗi với .catch()
Phương thức .catch(onRejected) là một cách tiện lợi để xử lý lỗi (trạng thái rejected) của một Promise. Nó tương đương với việc gọi .then(null, onRejected).
const pLoi = new Promise((_, reject) => {
setTimeout(() => reject("Có lỗi xảy ra trong quá trình xử lý!"), 500);
});
pLoi.catch(error => {
console.error("Đã bắt lỗi:", error);
});
Chuỗi Promise (Promise Chaining)
Một trong những lợi ích mạnh mẽ nhất của Promise là khả năng xâu chuỗi các tác vụ bất đồng bộ. Phương thức .then() luôn trả về một Promise mới, cho phép bạn gọi .then() tiếp theo để xử lý kết quả của Promise trước đó.
Trạng thái của Promise mới được trả về bởi .then() phụ thuộc vào giá trị trả về của hàm callback bên trong .then():
- Nếu callback trả về một giá trị không phải Promise (hoặc không trả về gì), Promise mới sẽ được
resolvevới giá trị đó. - Nếu callback ném ra một ngoại lệ (
throw new Error(...)), Promise mới sẽ bịrejectvới ngoại lệ đó. - Nếu callback trả về một Promise khác, Promise mới sẽ có trạng thái và giá trị/lỗi tương tự như Promise được trả về đó.
Ví dụ về Chuỗi Promise
Ví dụ 1: Chuỗi tác vụ thành công
const p1 = new Promise((res, rej) => {
setTimeout(() => {
res(10); // Hoàn thành với giá trị 10
}, 1000);
});
const p2 = p1.then((giaTri) => {
console.log("p1 hoàn thành với:", giaTri); // In: "p1 hoàn thành với: 10"
return giaTri + 5; // Trả về 15, p2 sẽ resolve với 15
});
const p3 = p2.then((giaTriMoi) => {
console.log("p2 hoàn thành với:", giaTriMoi); // In: "p2 hoàn thành với: 15"
});
console.log("Trước setTimeout - p1, p2, p3:", p1, p2, p3); // p1, p2, p3 đều là pending
setTimeout(() => {
console.log("Sau setTimeout - p1, p2, p3:", p1, p2, p3);
// p1: fulfilled: 10
// p2: fulfilled: 15
// p3: fulfilled: undefined (vì callback cuối không trả về gì)
}, 2000);
Ví dụ 2: Chuỗi với .catch()
new Promise((res, rej) => {
res("Tác vụ A"); // Hoàn thành
})
.then((ketQua) => {
console.log(ketQua); // In: "Tác vụ A"
return "Tác vụ B"; // Trả về giá trị mới
})
.catch((loi) => {
console.error("Xảy ra lỗi:", loi); // Bỏ qua vì không có lỗi trước đó
return "Tác vụ C do lỗi";
})
.then((ketQuaMoi) => {
console.log(ketQuaMoi); // In: "Tác vụ B"
});
Ví dụ 3: Lỗi trong .then()
new Promise((res, rej) => {
res("một chuỗi");
})
.then((giaTri) => {
console.log(giaTri.toUpperCase()); // Tác vụ thành công, nhưng lỗi xảy ra ở đây (nếu giaTri là null/undefined)
throw new Error("Lỗi xử lý dữ liệu!"); // Giả lập lỗi
})
.catch((loi) => {
console.error("Đã bắt lỗi:", loi.message); // In: "Đã bắt lỗi: Lỗi xử lý dữ liệu!"
return "Dữ liệu dự phòng"; // Trả về giá trị, Promise tiếp theo sẽ resolve
})
.then((ketQuaPhucHoi) => {
console.log("Kết quả sau khi phục hồi lỗi:", ketQuaPhucHoi); // In: "Kết quả sau khi phục hồi lỗi: Dữ liệu dự phòng"
});
Ví dụ 4: Lỗi khởi tạo và lỗi trong .catch()
new Promise((res, rej) => {
throw new Error("Lỗi khởi tạo Promise!"); // Ném lỗi ngay lập tức, Promise bị reject
})
.then((duLieu) => {
console.log("Không được gọi"); // Bỏ qua
})
.catch((loiDauTien) => {
console.error("Lỗi 1:", loiDauTien.message); // In: "Lỗi 1: Lỗi khởi tạo Promise!"
throw new Error("Lỗi khi xử lý lỗi!"); // Ném lỗi mới từ catch, Promise tiếp theo bị reject
})
.then((ketQua) => {
console.log("Cũng không được gọi"); // Bỏ qua
})
.catch(loiCuoiCung => {
console.error("Lỗi cuối cùng:", loiCuoiCung.message); // In: "Lỗi cuối cùng: Lỗi khi xử lý lỗi!"
});
Ví dụ 5: Thứ tự thực thi với setTimeout và Promise
const congViecA = new Promise((resolve, reject) => {
console.log("1. Khởi tạo congViecA"); // Đồng bộ
setTimeout(() => {
reject("Tác vụ A bị hủy"); // Sau 1 giây, congViecA bị reject
}, 1000);
});
const congViecB = congViecA.catch((lyDo) => {
console.log("4. congViecB bắt được lỗi:", lyDo); // Được gọi sau khi congViecA reject
return "Giá trị phục hồi B"; // congViecB sẽ resolve với giá trị này
});
console.log("2. Ngay lập tức: congViecA:", congViecA); // Output: Promise {<pending>}
console.log("3. Ngay lập tức: congViecB:", congViecB); // Output: Promise {<pending>}
setTimeout(() => {
console.log("5. Sau 2 giây: congViecA:", congViecA); // Output: Promise {<rejected>: "Tác vụ A bị hủy"}
console.log("6. Sau 2 giây: congViecB:", congViecB); // Output: Promise {<fulfilled>: "Giá trị phục hồi B"}
}, 2000);
Ví dụ 6: Hàng đợi Microtask
const pThoiGian = new Promise((resolve, reject) => {
console.log("Bước 1: Khởi tạo Promise"); // Đồng bộ
resolve("Hoàn thành ngay"); // Thay đổi trạng thái Promise
console.log("Bước 2: Kết thúc constructor"); // Đồng bộ
});
pThoiGian.then(() => {
console.log("Bước 4: Callback của .then()"); // Microtask
});
console.log("Bước 3: Mã đồng bộ sau Promise"); // Đồng bộ
// Thứ tự xuất hiện:
// Bước 1: Khởi tạo Promise
// Bước 2: Kết thúc constructor
// Bước 3: Mã đồng bộ sau Promise
// Bước 4: Callback của .then()
Giải thích: Các tác vụ đồng bộ chạy trước. Callback của .then() được đưa vào hàng đợi microtask, sẽ được thực thi sau khi tất cả mã đồng bộ đã hoàn thành.
Ví dụ 7: Tương tác Microtask phức tạp
Promise.resolve().then(() => {
console.log("MT1 - 0");
return Promise.resolve("MT1 - 4"); // Trả về một Promise khác
}).then(res => {
console.log(res); // Nhận kết quả từ Promise trả về trước đó
}).then(() => {
console.log("MT1 - 7");
});
Promise.resolve().then(() => {
console.log("MT2 - 1");
}).then(() => {
console.log("MT2 - 2");
}).then(() => {
console.log("MT2 - 3");
}).then(() => {
console.log("MT2 - 5");
}).then(() => {
console.log("MT2 - 6");
});
// Thứ tự xuất hiện:
// MT1 - 0 (Microtask 1, step 1)
// MT2 - 1 (Microtask 2, step 1)
// MT1 - 4 (Microtask 1, step 2 - được trả về từ step 1)
// MT2 - 2 (Microtask 2, step 2)
// MT2 - 3 (Microtask 2, step 3)
// MT1 - 7 (Microtask 1, step 3)
// MT2 - 5 (Microtask 2, step 4)
// MT2 - 6 (Microtask 2, step 5)
Giải thích: Tất cả các callback của .then() đều được đưa vào hàng đợi microtask. JavaScript thực thi các microtask theo thứ tự chúng được thêm vào. Khi một callback trong .then() trả về một Promise, callback .then() tiếp theo của chuỗi đó sẽ chờ Promise được trả về hoàn thành trước khi được thêm vào hàng đợi microtask.
Các phương thức tĩnh của Promise
Promise.resolve(value): Trả về một Promise đã đượcfulfilledvớivalue.Promise.reject(reason): Trả về một Promise đã bịrejectedvớireason.Promise.all(iterable): Nhận một mảng các Promise. Trả về một Promise mới sẽfulfilledkhi tất cả các Promise trong mảng đềufulfilled, hoặcrejectedngay lập tức nếu bất kỳ Promise nào trong mảng bịrejected.Promise.any(iterable): Nhận một mảng các Promise. Trả về một Promise mới sẽfulfilledngay lập tức khi Promise đầu tiên trong mảngfulfilled, hoặcrejectednếu tất cả các Promise trong mảng đềurejected.Promise.allSettled(iterable): Nhận một mảng các Promise. Trả về một Promise mới sẽfulfilledkhi tất cả các Promise trong mảng đều đãsettled(dù làfulfilledhayrejected). Giá trịfulfilledlà một mảng các đối tượng mô tả trạng thái và giá trị/lý do của mỗi Promise.Promise.race(iterable): Nhận một mảng các Promise. Trả về một Promise mới sẽsettledvới trạng thái và kết quả của Promise đầu tiên trong mảngsettled(dù làfulfilledhayrejected).
Async/Await: Đơn giản hóa mã bất đồng bộ
async và await là cú pháp được giới thiệu trong ES2017 để làm cho code bất đồng bộ trông và hoạt động giống như code đồng bộ, giúp dễ đọc và dễ bảo trì hơn nhiều.
Từ khóa async
Hàm được đánh dấu với từ khóa async luôn trả về một Promise. Nếu hàm async trả về một giá trị, Promise sẽ được resolve với giá trị đó. Nếu hàm async ném ra một ngoại lệ, Promise sẽ bị reject với ngoại lệ đó.
Từ khóa await
Từ khóa await chỉ có thể được sử dụng bên trong một hàm async. Nó được đặt trước một Promise, khiến hàm async tạm dừng thực thi cho đến khi Promise đó settled. Khi Promise fulfilled, await trả về giá trị của Promise. Khi Promise rejected, await sẽ ném ra lỗi.
Để xử lý lỗi với await, bạn có thể sử dụng khối try...catch quen thuộc.
async function xuLyTacVu() {
try {
console.log("Bắt đầu chờ tác vụ...");
const ketQua = await Promise.reject("Không thể kết nối máy chủ!"); // Promise bị reject
console.log("Thành công:", ketQua); // Không chạy nếu có lỗi
} catch (loi) {
console.error("Đã bắt lỗi khi chờ:", loi); // Chạy khi Promise bị reject
}
}
xuLyTacVu();
Ví dụ chuyển đổi Promise chain sang Async/Await
Giả sử chúng ta có một hàm delay đơn giản:
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Với chuỗi .then():
delay(500)
.then(() => {
console.log("Tác vụ 1 hoàn thành!");
return delay(500);
})
.then(() => {
console.log("Tác vụ 2 hoàn thành!");
return delay(500);
})
.then(() => {
console.log("Tác vụ 3 hoàn thành!");
});
Với async/await:
async function thucHienChuoiTacVu() {
await delay(500);
console.log("Tác vụ 1 hoàn thành!");
await delay(500);
console.log("Tác vụ 2 hoàn thành!");
await delay(500);
console.log("Tác vụ 3 hoàn thành!");
}
thucHienChuoiTacVu();
Phiên bản async/await rõ ràng và dễ đọc hơn đáng kể, đặc biệt khi có nhiều bước bất đồng bộ nối tiếp nhau.