Trong thế giới Javascript, lập trình không đồng bộ (asynchronous programming) là một khái niệm quan trọng để thực hiện các tác vụ cần thời gian để hoàn thành, ví dụ như lấy dữ liệu từ server thông quan API hoặc đọc dữ liệu của 1 file từ ổ đĩa. Nó giúp chúng ta không block luồng chính(main thread), và giữ cho ứng dụng của chúng ta hoạt động một cách linh hoạt và phản hồi(responsive) nhanh hơn. Hai từ khóa quan trọng của lập trình không đồng bộ trong Javascript là async và await.
Từ khóa async
Từ khóa async được sử dụng để khai báo một hàm là bất đồng bộ (asynchronous function). Nó cho javascript biết rằng hàm này sẽ xử lý các hoạt động bất đồng bộ và nó sẽ trả về một promise, là một đối tượng thể hiện sự hoàn thành(hoặc thất bại) của hoạt động bất đồng bộ.
async function fetchData() {
// You can define something here
}
Từ khóa await
Từ khóa await được sử dụng bên trong một hàm async để tạm dừng việc thực thi cho đến khi promise được giải quyết(resolved). Hiểu 1 cách đơn giản, nó chờ kết quả của một hoạt động bất đồng bộ(asynchronous operation).
Ví dụ: nếu muốn lấy data từ một API, chúng ta có thể sử dụng await như sau
async function fetchData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
}
Xử lý lỗi(Error handling)
Xử lý lỗi là một phần quan trọng khi làm việc với async và await trong javascript. Nó đảm bảo rằng ứng dụng của bạn có thể xử lý và phục hồi 1 cách linh hoạt sau khi xảy ra các tình huống không mong muốn(unexpected situations). Để xử lý những exception này chúng ta sẽ bọc lệnh gọi await bên trong 1 khối try-catch.
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching data:', error);
}
}
Tại sao nên sử dụng async và await?
Async và await làm cho các đoạn code bất đồng bộ trông giống và hoạt động như là mã đồng bộ(synchronous code). Điều này làm cho người đọc code sẽ dễ hiểu và dễ bảo trì hơn. Nó cũng giúp cho code được sạch hơn, tránh được kịch bản “callback hell” và “pyramid of doom”, có thể xảy ra với các đoạn code callback lồng nhau phức tạp.
Xử lý Multiple await
Khi bạn cần gọi nhiều request await mà chúng độc lập với nhau, bạn có thể sử dụng Promise.all để thực hiện chúng đồng thời. Việc này sẽ hiệu quả hơn việc chờ đợi từng request thực hiện 1 cách tuần tự.
async function fetchMultipleUrls(urls) {
try {
const requests = urls.map(url => fetch(url).then(res => res.json()));
const results = await Promise.all(requests);
return results; // An array of results from each URL
} catch (error) {
// If any request fails, the catch block is executed
console.error('An error occurred while fetching the URLs:', error);
}
}
// Usage
fetchMultipleUrls([
'https://api.example.com/data1',
'https://api.example.com/data2',
'https://api.example.com/data3'
]);
Trong ví dụ trên, Promise.all nhận vào 1 mảng các request và nó sẽ chờ tới khi tất cả các request được giải quyết. Nếu có bất kỳ request nào bị reject thì khối catch sẽ bắt lỗi. Cách tiếp cận này có thể giảm đáng kể tổng thời gian chời đợi tất cả các request hoàn tất(so với trường hợp chạy tuần tự).
So sánh Callback, Promise và Async/Await
Lập trình bất đồng bộ của javascript đã phát triển và thay đổi đáng kể theo thời gian, chuyển từ callback sang promise và cuối cùng là async/await. Mỗi bước trong quá trình phát triển này đã mang lại sự đơn giản và dễ đọc hơn cho mã bất đồng bộ(synchronous code).
Callback là phương thức ban đầu để xử lý các hoạt động bất đồng bộ trong Javascript(nếu các bạn đã từng làm việc với jquery trước đây thì sẽ phải sử dụng rất nhiều callback function). Tuy nhiên, nó có thể dẫn đến các đoạn code lồng sâu(thường gọi là “callback hell”) và gây khó khăn cho việc xử lý lỗi.
function getData(callback) {
// An asynchronous operation like reading a file
readFile('data.txt', 'utf8', (err, data) => {
if (err) {
return callback(err); // Pass the error to the callback
}
callback(null, data); // Pass the data to the callback
});
}
Promise cung cấp 1 cách tiếp cận rõ ràng hơn, dễ quản lý hơn đối với các đoạn code bất đồng bộ. Nó tránh được vấn đề lồng ghép(nesting issue) và làm cho việc xử lý lỗi trở nên đơn giản hơn bằng cách thêm phương thức try-catch vào trong hàm.
function getData() {
// The readFile function returns a promise
return readFilePromise('data.txt', 'utf8')
.then(data => {
return data; // Return data for the next .then()
})
.catch(err => {
throw err; // Handle any errors
});
}
Async/Await là cú pháp bổ sung cho promise để giúp cho mã bất đồng bộ của bạn trông như và hoạt động giống như mã đồng bộ. Điều này giúp chúng ta tiếp tục cải thiện khả năng đọc code và xư lý các lỗi phát sinh.
async function getData() {
try {
const data = await readFilePromise('data.txt', 'utf8');
return data; // Use the data as if it were returned synchronously
} catch (err) {
throw err; // Handle errors in a synchronous-like manner
}
}
Ưu điểm của Async/Await
- Dễ đọc(Readability): Async/Await giúp cho việc đọc và hiểu luồng code được dễ dàng hơn, đặc biệt là so với phương án sử dụng callback.
- Xử lý lỗi(Error handling): Nó cho phép các khối try-catch có thể bắt và xử lý lỗi, điều này không thể thực hiện được nếu bạn sử dụng callback và kém trực quan nếu sử dụng promise.
- Gỡ lỗi(Debugging): Việc gỡ lỗi với các đoạn code async/await đơn giản hơn vì nó hoạt động giống như mã đồng bộ. Ngăn xếp gọi hàm(call stacks) sẽ rõ ràng hơn so với promise và callback.
- Điều khiển luồng(Control Flow): Việc quản lý luồng điều khiển bằng async/await đơn giản hơn vì bạn có thể sử dụng các cấu trúc luồng điều khiển tiêu chuẩn như vòng lặp và điều kiện mà không cần thêm độ phức tạp.
- Thành phần(Composition): Async/await giúp việc soạn thảo(compose) và điều phối(coordinate) nhiều hoạt động bất đồng bộ một cách dễ dàng hơn so với callback và promise.