Hàm bất đồng bộ Async Functions trong JavaScript

Javascript nâng cao | by Học Javascript

Trong JavaScript, bất đồng bộ là một khái niệm quan trọng giúp chương trình có thể thực thi các tác vụ mà không làm gián đoạn luồng xử lý chính. Trước đây, lập trình viên thường sử dụng callback hoặc Promise để làm việc với bất đồng bộ, nhưng cả hai phương pháp này đều có nhược điểm nhất định, đặc biệt là khi xử lý nhiều tác vụ liên tiếp.

Để khắc phục vấn đề này, async functions (hàm bất đồng bộ) ra đời, cung cấp cú pháp rõ ràng và dễ đọc hơn bằng cách sử dụng từ khóa asyncawait. Với async/await, việc viết code bất đồng bộ trở nên trực quan hơn, giống như code đồng bộ nhưng vẫn giữ được hiệu suất cao. Trong bài viết này, chúng ta sẽ tìm hiểu chi tiết về async functions, cách sử dụng chúng, xử lý lỗi, cũng như so sánh với các phương pháp bất đồng bộ khác trong JavaScript.

Hàm async là gì?

Một async function trong JavaScript là một hàm bất đồng bộ, được khai báo bằng từ khóa async trước từ khóa function. Khi một hàm được khai báo là async, nó luôn trả về một Promise. Điều này có nghĩa là bạn có thể sử dụng await bên trong hàm để tạm dừng thực thi cho đến khi một Promise được hoàn thành.

Cách hoạt động của một async function

  • Khi gọi một async function, nó luôn trả về một Promise, dù bạn có explicitly trả về một Promise hay không.

  • Nếu hàm async trả về một giá trị, giá trị đó sẽ được tự động bọc trong một Promise.

  • Nếu bên trong async function có một lỗi (throw error), Promise sẽ bị từ chối (rejected).

  • await có thể được sử dụng bên trong async function để đợi một Promise hoàn thành trước khi tiếp tục thực thi đoạn mã tiếp theo.

Ví dụ đơn giản về async function

Dưới đây là một ví dụ cơ bản về cách sử dụng async để khai báo một hàm bất đồng bộ:

// Khai báo một async function
async function sayHello() {
    return "Xin chào!";  // Trả về một chuỗi
}

// Gọi async function và xử lý kết quả
sayHello().then(result => console.log(result)); 
// Output: "Xin chào!"

Giải thích:

  • Hàm sayHello() được khai báo với từ khóa async.

  • Nó trả về một chuỗi "Xin chào!", nhưng do đây là một async function, giá trị thực tế mà nó trả về là một Promise.

  • Khi gọi sayHello(), ta có thể dùng .then() để nhận kết quả.

Bây giờ, hãy xem cách sử dụng await để đợi một Promise bên trong async function:

async function fetchData() {
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => resolve("Dữ liệu đã tải xong!"), 2000);
    });

    let result = await promise; // Chờ promise hoàn thành
    console.log(result);
}

fetchData(); 
// Sau 2 giây: Output: "Dữ liệu đã tải xong!"

Giải thích:

  • Hàm fetchData() là một async function.

  • Bên trong, nó tạo một Promise giả lập một tác vụ mất 2 giây để hoàn thành.

  • await promise giúp đợi Promise hoàn thành trước khi tiếp tục thực thi lệnh tiếp theo (console.log(result)).

Như vậy, async functions giúp code trở nên dễ đọc hơn khi làm việc với các tác vụ bất đồng bộ, thay vì sử dụng callback hoặc Promise chaining rườm rà.

Cách khai báo async function trong JavaScript

Sử dụng từ khóa async trước một hàm

Trong JavaScript, một hàm bất đồng bộ (async function) được khai báo bằng cách thêm từ khóa async trước từ khóa function. Khi một hàm được khai báo là async, nó sẽ luôn trả về một Promise, ngay cả khi bên trong hàm không có một Promise nào.

Cú pháp khai báo một async function:

async function tenHam() {
    // Nội dung hàm
}

Hoặc với arrow function:

const tenHam = async () => {
    // Nội dung hàm
};

Sử dụng await để đợi kết quả của một Promise

  • await chỉ có thể được sử dụng bên trong async function.

  • Khi sử dụng await trước một Promise, JavaScript sẽ tạm dừng thực thi cho đến khi Promise đó được resolve (hoàn thành).

  • Điều này giúp tránh sử dụng .then() hoặc callback lồng nhau, làm cho code dễ đọc hơn.

Ví dụ về await trong async function

async function getData() {
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => resolve("Dữ liệu đã tải xong!"), 3000);
    });

    console.log("Đang tải dữ liệu...");

    let result = await promise; // Chờ promise hoàn thành
    console.log(result); // Output sau 3 giây: "Dữ liệu đã tải xong!"
}

getData();

Giải thích:

  • Hàm getData() là một async function.

  • Bên trong, nó tạo một Promise giả lập quá trình tải dữ liệu mất 3 giây.

  • Dòng console.log("Đang tải dữ liệu..."); chạy ngay lập tức.

  • await promise giúp tạm dừng cho đến khi Promise được resolve, sau đó mới thực hiện console.log(result).

  • Nhờ await, chúng ta tránh được việc sử dụng .then() hoặc callback lồng nhau.

Ví dụ về async function cơ bản

Dưới đây là một async function đơn giản trả về một giá trị:

async function sayHello() {
    return "Xin chào!";
}

// Gọi hàm và xử lý kết quả
sayHello().then(console.log); // Output: "Xin chào!"

Giải thích:

  • sayHello() là một async function, nên nó luôn trả về một Promise.

  • Khi gọi sayHello(), chúng ta có thể sử dụng .then() để nhận kết quả.

Lưu ý:

  • Nếu bạn không sử dụng await, hàm async sẽ không chờ và tiếp tục thực thi ngay lập tức.

  • await chỉ có thể được sử dụng bên trong một async function, nếu sử dụng bên ngoài sẽ gây lỗi.

Cách sử dụng await trong async function trong JavaScript

await giúp xử lý Promise theo cách đồng bộ hơn

  • await là một từ khóa được sử dụng để tạm dừng việc thực thi của một async function cho đến khi Promise được resolve.

  • Điều này giúp code dễ đọc hơn vì nó giống như xử lý đồng bộ, nhưng thực tế vẫn là bất đồng bộ.

Ví dụ về cách xử lý Promise bằng await

Trước khi có await, chúng ta thường xử lý Promise bằng .then():

function fetchData() {
    return new Promise((resolve) => {
        setTimeout(() => resolve("Dữ liệu đã tải!"), 2000);
    });
}

fetchData().then((data) => {
    console.log(data); // Output sau 2 giây: "Dữ liệu đã tải!"
});

Với await, code trở nên dễ hiểu hơn:

async function getData() {
    console.log("Bắt đầu tải dữ liệu...");
    let data = await fetchData();
    console.log(data); // Output sau 2 giây: "Dữ liệu đã tải!"
}

getData();

Lợi ích của await:

Không cần .then(), giúp code gọn gàng hơn.
Giúp tránh callback hell khi có nhiều tác vụ bất đồng bộ liên tiếp.

await chỉ có thể được sử dụng bên trong async function

Nếu sử dụng await bên ngoài một async function, JavaScript sẽ báo lỗi:

let data = await fetchData(); //  Lỗi: await is only valid in async function

Cách đúng là bọc nó trong một async function:

async function main() {
    let data = await fetchData();
    console.log(data);
}

main();

Xử lý lỗi trong async function trong JavaScript

Dùng try...catch để bắt lỗi trong async function

Trong một async function, nếu một Promise bị reject, nó sẽ gây lỗi và có thể bị bắt bằng try...catch.

Ví dụ về try...catch với async function

async function fetchDataWithError() {
    try {
        let response = await fetch("https://invalid-url.com"); // URL sai
        let data = await response.json();
        console.log(data);
    } catch (error) {
        console.error("Lỗi xảy ra:", error.message);
    }
}

fetchDataWithError();

Lợi ích của try...catch:

Giúp bắt lỗi một cách rõ ràng.
Cho phép tiếp tục thực thi sau khi lỗi xảy ra.

Khi nào nên dùng .catch() thay vì try...catch?

  • .catch() thường được dùng khi bạn gọi một async function và muốn xử lý lỗi bên ngoài.

  • try...catch thường được dùng bên trong async function để xử lý lỗi từng bước.

Ví dụ dùng .catch() bên ngoài async function

async function fetchData() {
    let response = await fetch("https://invalid-url.com");
    return response.json();
}

fetchData()
    .then((data) => console.log(data))
    .catch((error) => console.error("Lỗi xảy ra:", error.message));

Kết hợp nhiều async function trong JavaScript

Chạy nhiều async functions theo tuần tự

Khi gọi nhiều async functions liên tiếp, ta có thể sử dụng await để đảm bảo từng function hoàn thành trước khi function tiếp theo chạy.

Ví dụ chạy tuần tự

async function task1() {
    console.log("Bắt đầu task 1...");
    await new Promise(resolve => setTimeout(resolve, 2000)); // Giả lập tác vụ mất 2 giây
    console.log("Task 1 hoàn thành!");
    return "Kết quả từ task 1";
}

async function task2() {
    console.log("Bắt đầu task 2...");
    await new Promise(resolve => setTimeout(resolve, 1000)); // Giả lập tác vụ mất 1 giây
    console.log("Task 2 hoàn thành!");
    return "Kết quả từ task 2";
}

async function runTasksSequentially() {
    let result1 = await task1();
    console.log(result1);

    let result2 = await task2();
    console.log(result2);
}

runTasksSequentially();

Kết quả chạy tuần tự:

  • task1 chạy trước và hoàn thành sau 2 giây.

  • task2 chạy sau khi task1 hoàn thành, mất 1 giây.

  • Tổng thời gian: 3 giây.

Chạy nhiều async functions song song với Promise.all()

Khi các async functions không phụ thuộc lẫn nhau, ta có thể chạy đồng thời để tiết kiệm thời gian.

Ví dụ chạy song song

async function runTasksInParallel() {
    console.log("Bắt đầu chạy song song...");

    let [result1, result2] = await Promise.all([task1(), task2()]);

    console.log(result1);
    console.log(result2);
}

runTasksInParallel();

Kết quả chạy song song:

  • task1task2 chạy cùng lúc.

  • Cả hai hoàn thành sau 2 giây (thay vì 3 giây như chạy tuần tự).

  • Tối ưu thời gian hơn nếu các tasks độc lập nhau.

So sánh async/await với Promise và callback trong JavaScript

Cách tiếp cận Ưu điểm Nhược điểm
Callback Chạy nhanh, đơn giản với tác vụ nhỏ. Dễ dẫn đến callback hell, khó đọc.
Promise (.then/.catch) Dễ quản lý hơn callback, tránh lồng ghép sâu. Có thể bị Promise chaining, vẫn dài nếu nhiều Promise.
Async/Await Code dễ đọc hơn, giống đồng bộ. Không chạy song song nếu lạm dụng await liên tiếp.

Ví dụ: Cùng một tác vụ viết bằng các cách khác nhau

Sử dụng Callback (Callback Hell)

function task1(callback) {
    setTimeout(() => {
        console.log("Task 1 hoàn thành!");
        callback();
    }, 2000);
}

function task2(callback) {
    setTimeout(() => {
        console.log("Task 2 hoàn thành!");
        callback();
    }, 1000);
}

task1(() => {
    task2(() => {
        console.log("Tất cả tasks hoàn thành!");
    });
});

Nhược điểm: Code lồng ghép nhiều, khó đọc và bảo trì.

Sử dụng Promise Chaining

function task1() {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log("Task 1 hoàn thành!");
            resolve();
        }, 2000);
    });
}

function task2() {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log("Task 2 hoàn thành!");
            resolve();
        }, 1000);
    });
}

task1()
    .then(() => task2())
    .then(() => console.log("Tất cả tasks hoàn thành!"));

Ưu điểm: Không bị lồng callback, dễ đọc hơn.
Nhược điểm: Nếu có quá nhiều .then(), code vẫn dài.

Sử dụng Async/Await (Cách tốt nhất)

async function runTasks() {
    await task1();
    await task2();
    console.log("Tất cả tasks hoàn thành!");
}

runTasks();

Ưu điểm:

  • Code dễ đọc, giống như lập trình đồng bộ.
  • Không bị lồng ghép sâu như callback hell.
  • Không cần .then() lặp đi lặp lại như Promise chaining.

Kết bài

Hàm bất đồng bộ (async functions) trong JavaScript giúp xử lý các tác vụ bất đồng bộ một cách dễ đọc và dễ bảo trì hơn. Bằng cách sử dụng asyncawait, chúng ta có thể viết code theo phong cách đồng bộ, tránh được callback hell và hạn chế việc lồng ghép .then() quá nhiều trong Promise chaining.

Tuy nhiên, để tối ưu hiệu suất, cần hiểu rõ khi nào nên chạy tuần tự (dùng await liên tiếp) và khi nào nên chạy song song (dùng Promise.all()). Ngoài ra, việc xử lý lỗi trong async functions cũng rất quan trọng, giúp đảm bảo chương trình hoạt động mượt mà.

Việc thành thạo async/await không chỉ giúp bạn viết code gọn gàng hơn mà còn nâng cao hiệu suất khi làm việc với API, đọc/ghi tệp tin, hoặc các tác vụ bất đồng bộ khác. Vì vậy, nắm vững kiến thức về async functions sẽ giúp bạn trở thành một lập trình viên JavaScript chuyên nghiệp hơn!

Bài viết liên quan