Được giới thiệu trong ES6, Promises cho phép bạn dễ dàng viết mã bất đồng bộ mà không phải xử lý các hàm callback khác nhau. Với Promises, không cần lo lắng về các hàm callback đa cấp vừa khó viết vừa khó bảo trì.
Có tính năng khác trong JavaScript làm cho việc viết mã bất đồng bộ dùng Promises dễ dàng hơn: hàm async/await. Chúng cho phép viết mã trông đồng bộ nhưng thực sự thực hiện các thủ tục bất đồng bộ.
Trong hướng dẫn này, chúng ta sẽ thảo luận hàm async/await là gì và cách dùng nó trong mã. Hãy bắt đầu!
Promises: Ôn tập nhanh
Trước khi nói về hàm async/await, cần nhắc lại Promises. Một promise đại diện cho một hoạt động bất đồng bộ. Nó nói cho mã biết một hoạt động sẽ được thực hiện và, nếu hoạt động thành công, một giá trị sẽ được trả về. Ngược lại, lỗi sẽ được trả về cho phần còn lại của chương trình.
Một Promise đại diện cho giá trị không biết khi Promise được tạo. Promise chỉ là lời hứa rằng các giá trị trong tương lai sẽ được gửi trở lại mã của bạn. Vì Promise là đối tượng, nó phải viết hoa.
Xét kịch bản này. Bạn cần truy xuất tài nguyên từ API. Sẽ mất một giây hoặc hai để yêu cầu xử lý. Thay vì làm người dùng chờ đợi, bạn có thể chuyển mã vào Promise để phần còn lại của chương trình tiếp tục chạy.
Điều này có nghĩa là bạn có thể tiếp tục render một phần giao diện người dùng trang web trong khi dữ liệu đang được truy xuất. Sớm nhất khi giá trị được trả về, Promise sẽ gửi nó cho chương trình chính.
Dưới đây là ví dụ về Promise:
let sendCookies = new Promise(resolve => { setTimeout(() => { resolve("Bánh quy của bạn đã được gửi!"); }, 1000); }); sendCookies.then(data => { console.log(data); })
Mã trả về: Bánh quy của bạn đã được gửi! Khi thực thi sendCookies.then(), Promise được khởi tạo. Chương trình chờ 1.000 mili giây rồi gửi giá trị "Bánh quy của bạn đã được gửi!" về chương trình chính.
Cách dùng Async và Await
Trong hàm async/await, câu lệnh await chặn mã thực thi bên trong hàm async cho đến khi Promise được trả về. Đó là lý do nhà phát triển thường nói hàm async/await trông đồng bộ nhưng thực hiện tác vụ bất đồng bộ.
Xét ví dụ sau:
function sendCookies() { return new Promise(resolve => { setTimeout(() => { resolve("Bánh quy của bạn đã được gửi!"); }, 1000); }); } async function main() { const sendMessage = await sendCookies(); console.log(sendMessage); } main();
Mã trả về: Bánh quy của bạn đã được gửi! Tốn 1.000 mili giây cho hàm sendCookies() trả về giá trị "Bánh quy của bạn đã được gửi". Trong trường hợp này, chúng ta khai báo hàm async để mã chờ Promise resolve hoặc reject.
Từ khóa "async" nói cho mã biết chúng ta muốn thực hiện hoạt động bất đồng bộ trong hàm. Từ khóa "await" nói cho mã chờ trả về của Promise sendCookies() trước khi tiếp tục thực thi chương trình.
Hàm async luôn trả về Promise.
Dùng Async và Await với nhiều bước
Hàm async/await thường dùng khi có nhiều Promises cần làm việc. Đôi khi gọi là chuỗi Promises (Promises chaining). Vì mã sẽ chờ Promise được trả về bởi mỗi bước trước khi chuyển sang bước tiếp theo:
function processOrder() { return new Promise(resolve => { setTimeout(() => { resolve("Đơn hàng bánh quy đang được xử lý..."); }, 1000); }); } function sendCookies() { return new Promise(resolve => { setTimeout(() => { resolve("Bánh quy của bạn đã được gửi!"); }, 1000); }); } async function main() { const processingMessage = await processOrder(); console.log(sendMessage); const sendMessage = await sendCookies(); console.log(sendMessage); }
Mã trả về:
Đơn hàng bánh quy đang được xử lý… Bánh quy của bạn đã được gửi!
Mỗi bước mất 1.000 mili giây. Hàm sendCookies() không thực thi cho đến khi Promise từ processOrder() đã được trả về.
Biểu thức Async
Có ba cách dùng hàm async/await.
Cách đầu tiên là cách chúng ta đã thấy: khai báo hàm. Trong ví dụ, khai báo hàm trả về Promise, sau đó dùng "async" và "await" để thực thi.
Bạn cũng có thể khai báo hàm async dùng hàm mũi tên (arrow functions):
const main = async () => { const sendMessage = await sendCookies(); console.log(sendMessage); }
Mã trả về: Bánh quy của bạn đã được gửi! Giống ví dụ đầu nhưng thay vì khai báo main(), dùng hàm mũi tên.
Tương tự, bạn có thể dùng cú pháp biểu thức hàm:
const main = async function() { const sendMessage = await sendCookies(); console.log(sendMessage); }
Mã trả về: Bánh quy của bạn đã được gửi! Như thấy, đầu ra giống nhau. Khác biệt chỉ là cách khai báo hàm.
Thực tế, cú pháp này rất giống ví dụ trước. Chúng ta chỉ dùng từ khóa "function()" thay vì hàm mũi tên.
Không có cách tốt nhất để khai báo hàm async/await. Tất cả tùy thuộc vào chương trình bạn viết và cú pháp bạn thích. Mặc dù bạn có thể tranh luận hàm mũi tên ngắn gọn nhất, các cách khai báo khác có thể tốt hơn trong trường hợp khác.
Xử lý yêu cầu web dùng Async/Await
Một trong những cách dùng phổ biến nhất của hàm async/await là xử lý yêu cầu web với API dựa trên Promise, như fetch(). Bạn có thể đọc thêm về cách dùng fetch() trong hướng dẫn người mới về JavaScript fetch.
Xét ví dụ này:
async function retrieveComments() { const res = await fetch('https://jsonplaceholder.typicode.com/comments'); var comments = await res.json(); comments = comments.map(comment => comment.name); console.log(comments); } retrieveComments();
Mã trả về:
["id labore ex et quam laborum", "quo vero reiciendis velit similique earum" …]
Khi thực thi retrieveComments(), dùng "await" chờ fetch() chạy. Nghĩa là phần còn lại của chương trình không tiếp tục cho đến khi yêu cầu web xử lý xong. Vì fetch() trả về Promise.
Khi Promise được trả về bởi fetch(), chuyển giá trị trả về sang JSON. Sau đó map qua đối tượng lấy danh sách tên tất cả bình luận và in danh sách ra console.
Cách xử lý lỗi
Ôi, như tôi希望 lỗi không xảy ra trong mã. Nhưng chúng xảy ra, và đó là điều nhà phát triển phải dự phòng. Trong hàm bất đồng bộ, xử lý lỗi làm đồng bộ dùng câu lệnh try…catch. Xét mã này:
async function retrieveCookies() { const res = await fetch('https://thisapidoesnotexist.app/cookies'); var cookieNames = await res.json(); cookieNames = cookieNames.map(cookie => cookie.name); resolve(cookieNames); } async function printCookies() { const cookies = await retrieveCookies(); console.log(cookies); } printCookies().catch(res => console.log(res.message));
Mã trả về:
NetworkError when attempting to fetch resource. Mặc dù mã hoạt động, API chúng ta truy cập không tồn tại. Nghĩa là Promise không trả về dữ liệu, nên trả về Promise bị từ chối.
Trong ví dụ, dùng .catch() in lỗi trả về bởi hàm nếu có. Trong trường hợp này, dùng reject('Error') báo cho mã biết lỗi đã nảy sinh.
Nếu câu lệnh này thực thi, .catch() sẽ hoạt động và in lỗi đó.
Tương tự, hàm async có thể bắt lỗi cú pháp:
async function retrieveCookies() { const res = await fetch('https://thisapidoesnotexist.app/cookies'); var cookieNames = await res.json(); cookieNames = cookieNames.map(cookie => cookie.name); resolve(cookieNames); } async function printCookies() { try { const cookies = await retrieveCookies(); console.log(cookies); } catch(error) { console.log(error); } } printCookies();
Mã trả về: NetworkError when attempting to fetch resource. Trong ví dụ này, dùng try/catch kiểm tra xem await có trả về Promise thành công không. Lại lần nữa API không hợp lệ, gây từ chối Promise. Khi xảy ra, câu lệnh "catch" trong khối try/catch thực thi, ghi lỗi ra console.
Kết luận
Hàm Async/await giúp viết các hoạt động bất đồng bộ trong mã. Khi hàm async được khai báo, bạn dùng "await" để chờ hoạt động resolve. Từ khóa await phải dùng với hàm trả về Promise.
Bây giờ bạn đã sẵn sàng dùng hàm async/await JavaScript như chuyên gia!