0

[JS Thực Chiến] Async/Await: Phép thuật cứu rỗi những linh hồn kẹt trong "Địa ngục Callback"

Chào anh em, lại là mình đây.

Nếu anh em làm JavaScript/Node.js, chắc chắn anh em đã từng nghe đến cái đặc sản mang tên "Bất đồng bộ" (Asynchronous). Vì JS chỉ có một luồng (single-thread), nó không thể đứng đợi một cục query database chạy mất 5 giây được, nó phải đẩy việc đó ra chỗ khác rồi chạy tiếp lệnh bên dưới.

image.png

Nhưng cuộc sống mà, đôi khi chúng ta bắt buộc phải đợi. Ví dụ: Phải lấy được thông tin User (bước 1), có ID rồi mới lấy được danh sách Bài viết của User đó (bước 2), có Bài viết rồi mới lấy được Comment (bước 3).

Xuyên suốt chiều dài lịch sử của JS, các pháp sư đã đẻ ra 3 thế hệ để xử lý cái luồng logic tuần tự này. Hôm nay chúng ta cùng ôn lại lịch sử để thấy Async/Await nó vĩ đại như thế nào nhé.

1. Thời kỳ đồ đá: Callback Hell (Địa ngục gọi lại)

Ngày xửa ngày xưa, cách duy nhất để chạy code sau khi một tác vụ bất đồng bộ hoàn thành là truyền vào một cái hàm callback.

Với cái ví dụ 3 bước ở trên, code của anh em trông sẽ tởm lợm thế này:

getUser(userId, function(err, user) {
    if (err) return console.error(err);

    getPosts(user.id, function(err, posts) {
        if (err) return console.error(err);

        getComments(posts[0].id, function(err, comments) {
            if (err) return console.error(err);

            console.log("Cuối cùng cũng lấy được comment: ", comments);
        });
    });
});

Anh em thấy cái hình thù của đoạn code không? Nó lùi dần vào trong tạo thành một hình tam giác (giới IT gọi là Callback Hell hay Hadouken Code). Code này đọc lé cả mắt, bảo trì thì cực hình, mà dính lỗi thì không biết đường nào mà debug.

2. Thời kỳ quá độ: Promise (.then() .catch())

image.png

Để dọn dẹp cái đống rác trên, ES6 (2015) đẻ ra Promise (Lời hứa). Thay vì lồng ghép, chúng ta nối chuỗi chúng nó lại với nhau.

getUser(userId)
    .then(user => getPosts(user.id))
    .then(posts => getComments(posts[0].id))
    .then(comments => {
        console.log("Lấy được comment rồi: ", comments);
    })
    .catch(err => {
        console.error("Có lỗi ở 1 trong 3 bước trên: ", err);
    });

Đẹp hơn rất nhiều rồi! Không còn lùi lề lộn xộn nữa. Nhưng anh em thấy đấy, nó vẫn còn hơi rườm rà với một đống chữ then, và đôi khi việc truyền biến từ cái then đầu tiên xuống cái then thứ ba lại khá là đau đầu.

3. Kỷ nguyên ánh sáng: Async / Await

Đến bản ES8 (2017), JS tung ra đòn sát thủ: Async/Await. Bản chất của nó chỉ là "Syntactic sugar" (cú pháp bọc đường) viết đè lên Promise, nhưng nó làm thay đổi hoàn toàn cách chúng ta tư duy. Nó giúp code bất đồng bộ nhìn y hệt như code đồng bộ (tuần tự từ trên xuống dưới).

image.png

Mời anh em chiêm ngưỡng:

// Phải có chữ async trước function thì mới xài được chữ await ở trong
async function fetchUserComments(userId) {
    try {
        // Đứng đây đợi lấy xong user rồi mới chạy tiếp
        const user = await getUser(userId); 
        
        // Đợi lấy xong posts
        const posts = await getPosts(user.id); 
        
        // Đợi lấy xong comments
        const comments = await getComments(posts[0].id); 
        
        console.log("Xong xuôi sạch sẽ: ", comments);
    } catch (err) {
        // Catch lỗi gom vào một chỗ
        console.error("Lỗi rồi người anh em: ", err);
    }
}

Mịn màng, sạch sẽ, đọc từ trên xuống dưới ai cũng hiểu! Biến user, posts dùng chung một scope nên thích gọi ở đâu trong hàm cũng được. Quá đỉnh!

4. Những cú "Bóp team" kinh điển khi xài Async/Await

Tuy dễ xài, nhưng có 2 cái bẫy chết người mà 90% anh em mới học đều dẫm phải làm cho hệ thống chậm rì:

Cú lừa số 1: Quên chữ await

Nếu gọi hàm bất đồng bộ mà quên chữ await, cái anh em nhận được không phải là Data, mà là một cục object Promise { <pending> }. Đem cái này đi xử lý thì kiểu gì cũng Undefined hoặc vỡ mặt.

Cú lừa số 2: Lạm dụng await tuần tự làm nghẽn cổ chai

Hãy xem đoạn code sau, giả sử anh em cần gọi 2 API: Lấy thời tiết Hà Nội và lấy thời tiết Sài Gòn (2 việc này KHÔNG liên quan gì đến nhau):

async function getWeather() {
    // ❌ Code dại dột: Đợi Hà Nội xong (mất 2 giây) rồi mới đi hỏi Sài Gòn (mất thêm 2 giây)
    // Tổng thời gian: 4 giây
    const hn = await fetchWeather('Hanoi'); 
    const sg = await fetchWeather('Saigon'); 
    console.log(hn, sg);
}

Rõ ràng là 2 cái API này chạy độc lập, tại sao phải bắt thằng Sài Gòn đứng đợi thằng Hà Nội chạy xong? Cách fix của Kỹ sư thứ thiệt: Dùng Promise.all() để cho chúng nó chạy song song (Parallel) cùng một lúc.

async function getWeatherPro() {
    // ✅ Code tay to: Cho 2 thằng xuất phát cùng lúc.
    // Tổng thời gian: Bằng thời gian của thằng chạy chậm nhất (VD: 2 giây)
    const [hn, sg] = await Promise.all([
        fetchWeather('Hanoi'),
        fetchWeather('Saigon')
    ]);
    console.log(hn, sg);
}

Tương tự, anh em tuyệt đối không được nhét await vào trong vòng lặp for...of hoặc forEach trừ khi các tác vụ đó bắt buộc phải chờ nhau. Nếu không, anh em đang biến hệ thống của mình thành một con rùa bò lùi.

Chốt hạ

Async/Await là một trong những phát minh vĩ đại nhất của hệ sinh thái JavaScript, nó dọn dẹp hàng tấn code rác rưởi của kỷ nguyên Callback. Nhưng nhớ nhé anh em, sức mạnh lớn đi kèm trách nhiệm lớn. Đặt chữ await vào đâu thì phải tự hỏi: "Cái lệnh bên dưới có thực sự cần dữ liệu của cái lệnh này không? Nếu không thì cho chúng nó chạy song song đi!".

Anh em đã từng viết cái hàm lồng 5-6 cái callback bao giờ chưa? Hay đã từng làm hệ thống đơ 5 phút chỉ vì nhét await vào cái vòng lặp for 1000 phần tử? Tự thú dưới comment cho nhẹ lòng nhé!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.