JavaScript Asynchronous: Hiểu về Promise và Async/Await từ A-Z (Part 1)
Frontend Dzui Lắm
Tác giả

JavaScript là single-threaded, vậy tại sao nó vẫn có thể xử lý hàng trăm request, timer, event và các tác vụ bất đồng bộ mà không làm treo giao diện?
Để trả lời câu hỏi này, chúng ta cần hiểu cách JavaScript xử lý Asynchronous Programming, đặc biệt là Promise và async/await.
Trong bài viết này, chúng ta sẽ đi từ nền tảng của JavaScript Runtime đến cách sử dụng Promise và Async/Await trong các ứng dụng Frontend thực tế.
1. JavaScript là Single-threaded
Một trong những đặc điểm quan trọng của JavaScript là:
JavaScript sử dụng một Call Stack duy nhất để thực thi JavaScript code.
Ví dụ:
console.log("A");
console.log("B");
console.log("C");
Code được thực thi theo thứ tự:
A
B
C
Tại một thời điểm, JavaScript chỉ thực thi một operation trên Call Stack.
Vậy nếu JavaScript chỉ có một thread thì chuyện gì xảy ra khi chúng ta gọi API?
const response = fetch("/api/users");
Request có thể mất vài trăm milliseconds hoặc thậm chí vài giây.
Nếu JavaScript phải đứng yên chờ request hoàn thành thì UI sẽ bị block.
Nhưng thực tế:
Browser vẫn nhận click.
Animation vẫn có thể chạy.
Người dùng vẫn có thể nhập dữ liệu.
Những đoạn code khác vẫn có thể được thực thi.
Đó là nhờ cơ chế asynchronous của JavaScript Runtime.
2. Synchronous vs Asynchronous
Synchronous
Code được thực hiện tuần tự:
console.log("A");
console.log("B");
console.log("C");
Flow:
A
↓
B
↓
C
Operation trước hoàn thành thì operation sau mới bắt đầu.
Asynchronous
Với asynchronous code:
console.log("A");
setTimeout(() => {
console.log("B");
}, 1000);
console.log("C");
Kết quả:
A
C
B
JavaScript không đứng yên chờ setTimeout().
Nó đăng ký task và tiếp tục thực hiện code phía sau.
3. Event Loop
Để hiểu Promise và async/await, chúng ta cần hiểu một chút về Event Loop.
Có thể hình dung đơn giản:
JavaScript
│
▼
Call Stack
│
▼
Browser Runtime
│
▼
Task Queues
│
▼
Event Loop
│
└──────► Call Stack
Ví dụ:
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("End");
Kết quả:
Start
End
Timeout
setTimeout(..., 0) không có nghĩa callback chạy ngay lập tức.
Nó chỉ có nghĩa:
Đưa callback vào hàng đợi sau khi timer hoàn thành và chờ Event Loop đưa nó vào Call Stack khi phù hợp.
4. Callback Hell
Trước khi Promise trở nên phổ biến, asynchronous code thường được xử lý bằng callback.
Ví dụ:
getUser(userId, user => {
getOrders(user.id, orders => {
getProducts(orders, products => {
getRecommendations(products, recommendations => {
console.log(recommendations);
});
});
});
});
Khi số lượng asynchronous operation tăng lên, code trở nên:
Khó đọc.
Khó debug.
Khó xử lý error.
Khó maintain.
Đây thường được gọi là Callback Hell.
Promise được giới thiệu để giải quyết một phần lớn vấn đề này.
5. Promise là gì?
Promise là một object đại diện cho kết quả của một asynchronous operation.
Một Promise có 3 trạng thái:
Pending
│
┌───────┴───────┐
▼ ▼
Fulfilled Rejected
Pending
Operation vẫn đang được xử lý.
Fulfilled
Operation hoàn thành thành công.
Rejected
Operation thất bại.
Ví dụ:
const promise = fetch("/api/users");
fetch() trả về một Promise.
Chúng ta có thể xử lý kết quả:
fetch("/api/users")
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
});
6. Promise chaining
Promise cho phép chúng ta chain nhiều asynchronous operation:
getUser()
.then(user => getOrders(user.id))
.then(orders => getOrderDetail(orders[0].id))
.then(detail => {
console.log(detail);
})
.catch(error => {
console.error(error);
});
Flow:
getUser()
↓
user
↓
getOrders()
↓
orders
↓
getOrderDetail()
↓
detail
Code đã dễ đọc hơn callback hell.
Tuy nhiên, với business logic phức tạp, Promise chaining vẫn có thể trở nên dài.
Đó là lý do async/await trở nên phổ biến.
7. Async/Await
async/await giúp chúng ta viết Promise-based code theo syntax gần giống synchronous code.
Ví dụ với Promise:
function getUser() {
return fetch("/api/user")
.then(response => response.json());
}
Có thể viết:
async function getUser() {
const response = await fetch("/api/user");
return response.json();
}
Code business logic trở nên dễ đọc hơn:
async function loadOrder() {
const user = await getUser();
const orders = await getOrders(user.id);
const detail = await getOrderDetail(orders[0].id);
return detail;
}
8. async function luôn trả về Promise
Đây là điểm rất quan trọng.
async function hello() {
return "Hello";
}
Mặc dù chúng ta return một string:
return "Hello";
nhưng kết quả của function vẫn là Promise.
const result = hello();
console.log(result);
Kết quả về bản chất là:
Promise
Có thể sử dụng:
hello().then(value => {
console.log(value);
});
Hoặc:
const value = await hello();
9. await thực sự làm gì?
Ví dụ:
async function test() {
console.log("A");
await delay(1000);
console.log("B");
}
await có nghĩa là:
Tạm dừng việc thực thi async function hiện tại cho đến khi Promise hoàn thành.
Nó không có nghĩa là block toàn bộ JavaScript thread.
Ví dụ:
async function test() {
console.log("A");
await delay(1000);
console.log("B");
}
test();
console.log("C");
Kết quả:
A
C
B
10. Promise vs Async/Await
Hai cách viết:
Promise
getUser()
.then(user => {
return getOrders(user.id);
})
.then(orders => {
console.log(orders);
})
.catch(error => {
console.error(error);
});
Async/Await
try {
const user = await getUser();
const orders = await getOrders(user.id);
console.log(orders);
} catch (error) {
console.error(error);
}
Về bản chất:
Async/Await
↓
Syntax trên Promise
Async/await không thay thế Promise.
Nó là một cách viết dễ đọc hơn khi làm việc với Promise.
11. Khi nào nên sử dụng Promise?
Promise chaining có thể phù hợp khi:
Có pipeline xử lý dữ liệu.
Muốn sử dụng
.then().Đang làm việc với functional programming pattern.
Cần sử dụng Promise combinator.
Ví dụ:
getUser()
.then(validateUser)
.then(createSession)
.then(loadDashboard);
12. Khi nào nên sử dụng Async/Await?
Async/await thường rất dễ đọc khi xử lý business logic:
async function checkout() {
try {
const cart = await getCart();
const payment = await createPayment(cart);
const order = await createOrder(payment);
return order;
} catch (error) {
console.error(error);
}
}
Đặc biệt phù hợp với:
API service.
Business logic.
Form submission.
Authentication.
Data fetching.
Angular Service.
React hooks/service layer.
Node.js backend.
13. Một hiểu lầm phổ biến về await
Nhiều developer nghĩ:
"Dùng await nghĩa là mọi request đều chạy tuần tự."
Điều này không hoàn toàn đúng.
Vấn đề nằm ở cách chúng ta tạo Promise.
Ví dụ:
const users = await getUsers();
const products = await getProducts();
const orders = await getOrders();
Ở đây các request được xử lý tuần tự.
Nhưng nếu:
const usersPromise = getUsers();
const productsPromise = getProducts();
const ordersPromise = getOrders();
const users = await usersPromise;
const products = await productsPromise;
const orders = await ordersPromise;
thì cả 3 Promise đã được tạo trước.
Đây chính là nền tảng để chúng ta xử lý nhiều request đồng thời.
Và đây sẽ là nội dung chính của Part 2.
14. Tổng kết Part 1
Có thể ghi nhớ:
JavaScript
↓
Single-threaded
↓
Event Loop
↓
Asynchronous
↓
Promise
↓
Async/Await
Các ý quan trọng nhất:
JavaScript có một Call Stack chính.
Asynchronous operation được hỗ trợ bởi JavaScript Runtime.
Promise đại diện cho kết quả của asynchronous operation.
Promise có
pending,fulfilled,rejected.asyncfunction luôn trả về Promise.awaitgiúp chờ Promise trong async function.Async/await không phải cơ chế asynchronous mới, mà là syntax làm việc với Promise.
awaitkhông block toàn bộ JavaScript thread.Không phải cứ dùng
awaitlà request bắt buộc chạy tuần tự.
Ở Part 2, chúng ta sẽ đi vào phần quan trọng hơn trong thực tế Frontend:
Làm thế nào để xử lý 10, 100 hoặc hàng nghìn request hiệu quả mà không khiến ứng dụng và server bị quá tải?
Thích bài viết này?
Nội dung trên Vết Mực luôn được chia sẻ miễn phí. Nếu bài viết mang lại giá trị cho bạn, hãy cân nhắc ủng hộ để chúng mình có thể duy trì máy chủ, phát triển thêm tính năng mới và tiếp tục xây dựng một không gian dành cho những người yêu viết lách. ✨
Các cách ủng hộ:
- •Viết và đăng bài trên Vết Mực
- •Chia sẻ bài viết với bạn bè
- •Góp ý để chúng mình cải thiện sản phẩm qua email: nsikhoa@gmail.com
Dù bạn chọn ủng hộ hay chỉ đơn giản là tiếp tục đọc và chia sẻ bài viết, đó đều là nguồn động lực rất lớn với chúng mình. ❤️
Bình luận
Đăng nhập để để lại bình luận.