4.1 JavaScript Asynchronous Quest: Sứ Mệnh Chinh Phục Bất Đồng Bộ
Như chúng ta đã tìm hiểu ở bài 1.99 - Event Loop, JavaScript là ngôn ngữ đơn luồng. Vì vậy nó áp dụng thiết kế xử lý Asynchronous (Bất đồng bộ) để thoát khỏi việc bị treo đứng khi gánh vác các tác vụ nặng (như chọc vào Database, gọi API, đọc file hệ thống).
Khả năng sử dụng điêu luyện các kỹ thuật Bất đồng bộ chính là ranh giới rạch ròi nhất giữa một lập trình viên Junior và Senior. Trong bài học này, chúng ta sẽ theo dấu toàn bộ chặng đường tiến hoá của việc kiểm soát JS Async.
🏛️ Thời kỳ đồ đá: Callbacks và Địa ngục tháp lồng
Cách thô sơ nhất để báo cho JavaScript biết "Làm xong việc này rồi hãy báo lại tôi nhé" chính là nhét một hàm (gọi là callback) vào bên trong lời gọi đó.
// Function bất đồng bộ cũ kỹ
function fetchUserData(id, callback) {
setTimeout(() => {
callback({ userId: id, username: "QuanBM" }); // Trả kết quả thông qua gọi lại hàm
}, 1000);
}
// Cách gọi
fetchUserData(1, function(user) {
console.log("Xong user:", user);
});
Điều này hoạt động tuyệt vời nếu bạn chỉ làm một việc duy nhất. Nhưng chuyện gì sẽ xảy ra nếu để in ra dữ liệu, bạn cần tuần tự 3 tiến trình phụ thuộc lẫn nhau: Lấy User -> Lấy Posts của User đó -> Lấy Comments của Post đó?
// ⚡ CALLBACK HELL (Pyramid of Doom - Kim tự tháp diệt vong)
fetchUser(1, function(user) {
fetchPosts(user.id, function(posts) {
fetchComments(posts[0].id, function(comments) {
console.log("Lấy bình luận thành công:", comments);
});
});
});
Đây chính là lúc Callback Hell xuất hiện. Code thụt lề dạt hết sang bên phải, khó theo dõi lồng ghép luồng, và cực kỳ tồi tệ trong việc kiểm soát dòng bắt lỗi (Error handling).
⚔️ Kỷ nguyên Phục hưng: Quyền năng của Promises
Năm 2015, ES6 giới thiệu Promise (Lời hứa). Đây là một Object đại diện cho sự kiện hoàn tất (hoặc thất bại) của một thao tác bất đồng bộ trong tương lai.
Một Promise luôn chỉ nằm ở 3 trạng thái:
- Pending (Đang bay lơ lửng, chưa có kết quả)
- Fulfilled / Resolved (Đã có data, trút vào .then())
- Rejected (Lỗi rồi, bị từ chối, trút vào .catch())
Khắc phục kim tự tháp, Promise chuỗi hóa code sang dạng phẳng dẹt (Flat chaining):
// 💡 CỨU BINH DÀN HÀNG NGANG
fetchUser(1)
.then(user => {
return fetchPosts(user.id);
})
.then(posts => {
return fetchComments(posts[0].id);
})
.then(comments => {
console.log("Tuyệt vời, bình luận đây: ", comments);
})
.catch(error => {
// Chỉ cần MỘT TÊN CẢNH SÁT cho 3 vụ án
console.error("Luồng lấy dữ liệu tan tành vì: ", error);
});
Lợi thế tuyệt đối: Không những trình bày đẹp mắt hơn, .catch() duy nhất nằm ở đáy sẽ tóm gọn tất tay bất kể lỗi đó xảy ra ở lời gội fetchUser, fetchPosts hay fetchComments!
🚀 Kỷ nguyên Hiện đại: async / await
Mặc dù Promise là một phát minh xuất chúng, việc phải liên tục nối dây .then().then() vẫn mang lại cảm giác lạ lẫm so với tư duy rẽ nhánh của các ngôn ngữ backend.
Vậy là giới công nghệ đón nhận cập nhật ES8 với 2 từ khóa: async / await (thực chất nó bên dưới vẫn chính là Promise, nhưng được khoác "bộ áo cú pháp" đẹp đẽ hơn - Syntatic Sugar).
Công thức thần tốc: Viết code bất đồng bộ mà nhìn cứ phẳng phiu như code đồng bộ bình thường!
// 🌟 MAGIC ASYNC / AWAIT
async function getFullData() {
try {
const user = await fetchUser(1); // Block "giả lập" dòng này tới khi có kết quả
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
console.log("Top notch!", comments);
} catch (error) {
// Khởi đầu tư duy bắt lỗi Try Catch truyền thống
console.error("Vỡ mộng: ", error);
}
}
getFullData();
[!TIP] Lưu ý quan trọng Keyword
awaitCHỈ ĐƯỢC PHÉP đứng trong một function đã được đính mácasync(Hoặc ở Top-Level module trong ES_modules sau này). Nhưng khi bạn chạy nó, nó không xả đứng nguyên cái Main Thread trình duyệt của máy đâu (Nhờ Event Loop làm nhiệm vụ bảo trợ), nó chỉ dập đứng context của cái hàm ấy mà thôi. Cực kì sảng khoái và an toàn!
💣 Bẫy ngầm chết ngọc: Lạm dụng await chạy tuần tự (Serial)
Một khi xài rành xài sướng await, các dev JS lại rơi vào một cái hố sâu mới: Lười biếng ghép các await liền kề nhau vô tội vạ.
Cùng xem đoạn mã tai hoạ dưới đây:
async function loadDashboard() {
const weather = await fetchWeather(); // Nghỉ mất 2 giây
const stocks = await fetchStockPrices(); // Lại nghỉ mất 3 giây
const news = await fetchNewsToday(); // Nghỉ mất thêm 1 giây
console.log("Tổng mất: 6 GIÂY! Quá chậm!");
}
Stock, thời tiết hay tin tức là 3 bảng tin hoàn toàn độc lập, chả liên quan gì đến nhau. Sao bảng tin chứng khoán phải chờ đến lúc tải xong cục thời tiết mới được đi vào ống khói chạy API? Chúng ta đã tự bóp méo lợi thế Concurrency (Hỗ trợ gọi đồng thời).
Hãy kích hoạt Promise.all làm quản đốc!
// 🔥 TỐI ƯU CONCURRENCY (Gọi song song)
async function loadDashboard() {
// Bắn 3 súng đi tải cùng 1 tít tắc
const [weather, stocks, news] = await Promise.all([
fetchWeather(),
fetchStockPrices(),
fetchNewsToday()
]);
console.log("Tổng mất: 3 GIÂY (Chỉ đợi đúng cái nào tốn lâu nhất). Tốc độ bàn thờ!");
}
Tóm gọn hành trình
- Callbacks: Truyền thống, nguyên thủ. Dẫn tới đau đớn nếu hàm gọi hàm lồng nhau (Callback hell).
- Promise: Trừu tượng hóa trạng thái tương lai, sử dụng chuỗi
.then()và bắt trọn mạng lưới lỗi ở.catch(). Giải quyết được rào cản lồng cung bậc. - Async/Await: Cú pháp đỉnh cao, dễ viết dễ đọc như đi bộ dạo phố.
- Đoạn đường cuối cùng của một Master Async là biết khi nào cần dùng Bất đồng bộ song song (
Promise.all) chứ không xài rập khuôn 1 luồng ngớ ngẩn (Serial await).
Giờ thì bạn đã có nền móng đỉnh cao. Các bài tiếp theo trong Chương 4 sẽ đẩy giới hạn độ khó của Asynchronous lên đỉnh kỉ lục nhờ vào binh đoàn Reactive Programming: RxJS.