Hiểu và sử dụng Async/Await trong JavaScript
Giới thiệu
Khi phát triển ứng dụng web hoặc mobile bằng JavaScript, chúng ta thường xuyên phải làm việc với các tác vụ bất đồng bộ (asynchronous operations) như:
- Gọi API từ server
- Đọc hoặc ghi file
- Truy vấn cơ sở dữ liệu
- Tải hình ảnh hoặc dữ liệu từ Internet
Nếu không xử lý đúng cách, chương trình có thể bị khó đọc và khó bảo trì. Đây là lý do Async/Await ra đời để giúp lập trình viên viết code bất đồng bộ theo phong cách gần giống code đồng bộ thông thường.
Trong bài viết này, chúng ta sẽ tìm hiểu Async/Await là gì, cách hoạt động và những lưu ý quan trọng khi sử dụng.
Bất đồng bộ trong JavaScript là gì?
JavaScript hoạt động theo cơ chế single-thread, nghĩa là chỉ thực hiện một tác vụ tại một thời điểm.
Giả sử chúng ta gọi một API mất 5 giây để phản hồi. Nếu JavaScript phải chờ đủ 5 giây mới chạy tiếp các câu lệnh phía dưới thì trải nghiệm người dùng sẽ rất tệ.
Vì vậy JavaScript sử dụng cơ chế bất đồng bộ để cho phép chương trình tiếp tục thực hiện các công việc khác trong khi chờ kết quả từ API.
Ví dụ:
console.log("Bắt đầu");
setTimeout(() => {
console.log("Đã hoàn thành");
}, 3000);
console.log("Kết thúc");
Kết quả:
Bắt đầu
Kết thúc
Đã hoàn thành
Mặc dù setTimeout được gọi trước, JavaScript không chờ nó hoàn thành mà tiếp tục thực thi các lệnh tiếp theo.
Promise và vấn đề Callback Hell
Trước khi Async/Await xuất hiện, Promise là giải pháp phổ biến để xử lý bất đồng bộ.
Ví dụ:
fetch("/api/users")
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
});
Promise đã giúp code dễ quản lý hơn callback truyền thống. Tuy nhiên khi phải thực hiện nhiều tác vụ nối tiếp nhau, chuỗi .then() có thể trở nên dài và khó đọc.
Async/Await là gì?
Async/Await được giới thiệu từ ES2017 nhằm đơn giản hóa việc làm việc với Promise.
Có hai từ khóa chính:
asyncawait
Hàm được đánh dấu bằng async sẽ luôn trả về Promise.
Ví dụ:
async function getData() {
return "Hello";
}
Thực tế JavaScript sẽ hiểu tương đương:
Promise.resolve("Hello");
Sử dụng Await
Từ khóa await cho phép chương trình tạm dừng bên trong hàm async cho đến khi Promise hoàn thành.
Ví dụ:
async function getUsers() {
const response = await fetch("/api/users");
const data = await response.json();
console.log(data);
}
Đoạn code trên thực hiện tương tự Promise nhưng dễ đọc hơn rất nhiều.
Thay vì phải theo dõi nhiều tầng .then(), chúng ta có thể đọc code từ trên xuống dưới giống như một luồng xử lý thông thường.
Ví dụ thực tế gọi API
Giả sử cần lấy danh sách người dùng từ API:
async function loadUsers() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
const users = await response.json();
console.log(users);
} catch (error) {
console.error("Có lỗi xảy ra:", error);
}
}
Ở đây:
await fetch(...)chờ API phản hồiawait response.json()chuyển dữ liệu sang JSONtry/catchxử lý lỗi phát sinh
Đây là mẫu code được sử dụng rất phổ biến trong React, React Native và Node.js.
Chạy tuần tự và song song
Một lỗi phổ biến của người mới là sử dụng await liên tiếp cho các tác vụ độc lập.
Ví dụ:
const user = await getUser();
const posts = await getPosts();
const comments = await getComments();
Cách này sẽ đợi từng tác vụ hoàn thành rồi mới chạy tác vụ tiếp theo.
Nếu các API không phụ thuộc lẫn nhau, chúng ta nên chạy song song bằng Promise.all.
const [user, posts, comments] = await Promise.all([
getUser(),
getPosts(),
getComments()
]);
Lợi ích:
- Tăng tốc độ xử lý
- Giảm thời gian chờ
- Tối ưu hiệu năng ứng dụng
Đây là kỹ thuật rất quan trọng trong các dự án thực tế.
Những lỗi thường gặp
Quên từ khóa await
Ví dụ:
const users = fetch("/api/users");
console.log(users);
Kết quả:
Promise { <pending> }
Do Promise chưa được resolve nên chúng ta chỉ nhận được đối tượng Promise thay vì dữ liệu thực tế.
Sử dụng await ngoài async
Ví dụ:
function loadData() {
const data = await getUsers();
}
JavaScript sẽ báo lỗi vì await chỉ được sử dụng bên trong hàm async.
Cách đúng:
async function loadData() {
const data = await getUsers();
}
Không xử lý lỗi
Mọi lời gọi API đều có khả năng thất bại.
Vì vậy nên sử dụng:
try {
const data = await getUsers();
} catch (error) {
console.error(error);
}
thay vì giả định rằng API luôn thành công.
Khi nào nên dùng Async/Await?
Async/Await đặc biệt phù hợp khi:
- Gọi API
- Làm việc với database
- Đọc ghi file
- Xử lý nhiều bước tuần tự
- Thực hiện các tác vụ mạng
Tuy nhiên không phải lúc nào cũng cần dùng. Với các tác vụ đơn giản hoặc cần xử lý song song phức tạp, Promise vẫn là lựa chọn hợp lý.
Kết luận
Async/Await là một trong những tính năng quan trọng nhất của JavaScript hiện đại. Nó giúp code bất đồng bộ trở nên dễ đọc, dễ bảo trì và gần với tư duy lập trình tuần tự hơn.
Đối với các lập trình viên React, React Native hay Node.js, việc thành thạo Async/Await gần như là kỹ năng bắt buộc. Bên cạnh đó, hiểu rõ cách hoạt động của Promise, Event Loop và Promise.all sẽ giúp bạn viết những ứng dụng hiệu quả và tối ưu hơn.
Nếu mới bắt đầu học JavaScript, Async/Await là một trong những chủ đề nên đầu tư thời gian tìm hiểu kỹ vì bạn sẽ gặp nó gần như mỗi ngày trong công việc thực tế.
All rights reserved