Cơ bản về Async Await trong Javascript
Khi bắt đầu lập trình với Nodejs, vì Javascript (JS) là bất đồng bộ (asynchoronous) nên mình gặp khó khăn trong việc tổ chức code giống như trong lập trình đồng bộ (synchoronous). Việc cho các đoạn code vào trong các callback khiến mình cảm thấy code trở lên khó đọc theo luồng như trong PHP hay Ruby, nên mình đã tìm hiểu và sử dụng cú pháp Async/Await theo chuẩn ES6 của JS. Sử dụng các cú pháp mới này giúp cho code của mình có thể tổ chức rõ ràng hơn.
Khi sử dụng cú pháp Async/Await thì bạn phải nắm được luồng chạy trong các hàm này và cái gì được trả về trong các hàm này. Sau đâyBizfly Cloud xin trình bày trình tự chạy các câu lệnh khi có Async/Await trong Nodejs.
Async/Await là gì?
Async/Await được biết đến là tính năng do ES7 tạo ra có tác dụng giúp code bất đồng bộ trông đồng bộ hơn, dễ sử dụng và dễ nhìn hơn. Async là hàm bất đồng bộ, có nhiệm vụ trong việc tách rời với phần code của Event Loop, sau đó trả về Promise. Cấu trúc và cú pháp của Async nhìn trông khá giống so với chuẩn hàm đồng bộ. Còn Await là cú pháp để tạm dừng việc code lại đợi kết quả từ một Promise và thành phần này sẽ chỉ dùng khi nằm ở trong Async.
Async/Await có khác biệt gì so với Promise?
Async/Await và Promise có những điểm khác biệt được thể hiện như sau:
Điểm khác biệt | Async/Await | Promise |
Khái niệm | Async và Await là cú pháp để làm việc với Promises một cách dễ dàng hơn. Async là một từ khóa được đặt trước một hàm để biến hàm đó thành một hàm bất đồng bộ (asynchronous function) và Await được sử dụng để tạm dừng việc thực thi của một hàm async cho đến khi một Promise được giải quyết (resolved). | Promise là một đối tượng đại diện cho kết quả của một hoạt động bất đồng bộ. |
Khả năng đọc | Code trông giống như code đồng bộ, dễ đọc hơn, đặc biệt là khi có nhiều thao tác bất đồng bộ liên tiếp. | Sử dụng then() và catch(), đôi khi dẫn đến việc code bị lồng vào nhau |
Xử lý lỗi | Sử dụng try...catch để xử lý lỗi, giúp code rõ ràng hơn và dễ quản lý hơn. | Sử dụng catch() để xử lý lỗi. |
Quản lý chuỗi bất đồng bộ | Chỉ cần sử dụng từ khóa await để tạm dừng việc thực thi cho đến khi Promise được giải quyết, giúp code đơn giản và rõ ràng hơn. | Khi có nhiều Promise cần thực thi theo thứ tự, bạn phải lồng then() vào nhau hoặc sử dụng chuỗi Promise |
Lợi ích khi sử dụng Async/Await
Việc sử dụng Async/Await đem lại những lợi ích sau đây:
Cải thiện đọc mã nguồn: Async/Await làm cho mã nguồn trở nên dễ đọc và hiểu hơn so với sử dụng các callback hoặc promise chain. Nó giúp mã nguồn trông giống như mã đồng bộ, nhưng thực chất lại là bất đồng bộ.
Tránh Callback Hell: Khi sử dụng callback, việc lồng nhau của các callback có thể dẫn đến một cấu trúc mã khó đọc và bảo trì, thường được gọi là "callback hell". Async/Await giúp tránh được vấn đề này bằng cách làm phẳng cấu trúc mã.
Xử lý lỗi dễ dàng hơn: Tính năng này cho phép sử dụng try/catch để bắt và xử lý lỗi, tương tự như cách xử lý lỗi trong mã đồng bộ. Từ đó làm cho việc xử lý lỗi trở nên rõ ràng và dễ quản lý hơn.
Tối ưu hóa hiệu suất: Async/Await cho phép các hàm bất đồng bộ chạy nền mà không chặn luồng chính của chương trình. Do đó, giúp tối ưu hóa hiệu suất, đặc biệt là trong các ứng dụng web, nơi mà việc chờ đợi các yêu cầu mạng là điều phổ biến.
Một số hạn chế
Mặc dù đem đến nhiều lợi ích, nhưng Async/Await cũng có những hạn chế sau đây:
Khó khăn trong việc quản lý lỗi: Mặc dù async/await giúp việc viết mã bất đồng bộ trở nên rõ ràng hơn, nhưng việc xử lý lỗi trong các hàm bất đồng bộ có thể trở nên phức tạp. Bạn cần sử dụng try/catch trong mỗi hàm async để đảm bảo rằng các lỗi được bắt và xử lý đúng cách, điều này có thể làm mã trở nên dài dòng và rối rắm.
Hiệu suất không cao bằng một số giải pháp khác: Tính năng có thể không tối ưu bằng việc sử dụng Promise.all hoặc các phương pháp khác trong trường hợp bạn cần thực hiện nhiều tác vụ bất đồng bộ song song. Async/await thường thực hiện các tác vụ theo tuần tự, trừ khi bạn rõ ràng khởi động các tác vụ đồng thời và chờ tất cả hoàn thành.
Không tương thích ngược: Một số môi trường hoặc phiên bản cũ của các ngôn ngữ lập trình có thể không hỗ trợ cú pháp async/await. Nó đòi hỏi bạn phải sử dụng các công cụ hoặc thư viện bổ sung để đảm bảo tính tương thích.
Khó khăn trong Debugging: Việc debug mã sử dụng async/await có thể phức tạp hơn so với mã đồng bộ truyền thống. Stack trace trong các lỗi có thể khó theo dõi hơn, do các tác vụ bất đồng bộ có thể được thực hiện ở các thời điểm khác nhau.
Xét ví dụ căn bản khi không có Async/Await sau đây
execute() function execute() { findResult() console.log("end of execute") } function findResult() { for(var i = 0; i < 100000; i ) { var j = 100 } console.log('before findResult') db.collection('hospitals').findOne({name: '医療法人神甲会隈病院'}, function(err, result){ console.log('inner findResult callback') } ) console.log('after findResult') }
Đoạn code trên có findOne() là hàm chạy async nên chẳng có gì bàn cãi khi thứ tự in ra sẽ là:
before findResult after findResult end of execute inner findResult callback
Lại xét ví dụ căn bản khi có hàm Async/Await sau đây.
execute() function execute() { findResult() console.log("end of execute") } async function findResult() { for(var i = 0; i < 100000; i ) { var j = 100 } console.log('before findResult') var result = await db.collection('hospitals').findOne({name: '医療法人神甲会隈病院'}) // don’t write callback here // process with result here console.log('after findResult') }
Người ta tạo ra Async/Await là để tránh các hàm callback nên đừng viết await và callback cùng nhau.
Bạn dự đoán đoạn code trên sẽ in ra thứ tự thế nào?
Thứ tự sẽ như sau:
before findResult end of execute after findResult
Để trả lời câu hỏi trên thì cần nhớ một số chú ý sau:
• Await luôn luôn nằm trong hàm async như ví dụ trên (await không thể nằm trong hàm không được khai báo từ khóa async phía trước)
• Thứ tự thực hiện các câu lệnh trong js nói chung hay nodejs nói riêng đều là chạy từ trên xuống dưới (nghĩa là chạy sync chứ không phải async), trừ những hàm liên quan tới I/O thì mới được chạy async (Tham khảo thêm ở bài viết event loop trong js )
• Khi gặp await, nó sẽ convert hàm đó thành promise với callback là tất cả những phần code phía sau await đó. Bản chất await là một promise, phần code nằm sau await thực chất là code nằm trong callback của hàm await đó. Ví dụ 2 đoạn mã dưới đây là tương đương nhau:
async function test() { var result = await db.collection('hospitals').findOne({name: '医療法人神甲会隈病院'}) console.log('after findResult: ', result) ... more code here ... } // //tương đương với function test() { db.collection('hospitals').findOne({name: '医療法人神甲会隈病院'}, function(result){ console.log('after findResult: ', result) ... more code here ... }) }
Nếu nắm được ví dụ trên kia rồi thì những đoạn code phía sau đây bạn sẽ biết thứ tự và kết quả được in ra như thế nào:
Ví dụ 1:
execute() function execute() { var result = findResult() console.log(result) } async function findResult() { for(var i = 0; i < 100000; i ) { var j = 100 } console.log('before findResult') await db.collection('hospitals').findOne({name: '医療法人神甲会隈病院'}) console.log('after findResult') }
Thứ tự in ra là:
before findResult Promise { } after findResult
Để ý thấy hàm findResult dù ko return j nhưng result vẫn in ra là một Promise vì hàm có khai báo async ở phía trước luôn trả về một Promise(giải thích ở phía sau).
Thế để lấy kết quả thực từ câu lệnh findOne() của VD1 ở hàm execute() thì chúng ta cần phải làm gì? Vì findResult() trả về một Promise nên ta chỉ cần gọi hàm then() ở nơi được trả về là được, xét ví dụ 2 sau đây:
Ví dụ 2:
execute() function execute() { findResult().then(function(result){ // call then() here to capture result in async function console.log(result) }) console.log('end of execute') } async function findResult() { for(var i = 0; i < 100000; i ) { var j = 100 } console.log('before findResult') var result = await db.collection('hospitals').findOne({name: '医療法人神甲会隈病院'}) console.log('after findResult') return result }
Kết quả in ra sẽ là:
before findResult end of execute after findResult { _id: 59e8d6930c9c77b21c42d704, .....}
Hàm async luôn trả về một promise
Gọi hàm có từ khóa async phía trước luôn trả về một promise, dù trong hàm đó có await hay không.
Ví dụ 1:
function test() { var promise = returnTen() console.log(promise) } async function returnTen() { return 10 } test() // Promise { 10 }
Ví dụ này promise trả về có kết quả là 10 luôn.
Ví dụ 2:
function test() { var promise = returnTen() console.log(promise) } async function returnTen() { return await 10 } test() // Promise { }
Ví dụ này promise trả về chưa có kết quả luôn.
Khi await nằm trong loop
Chú ý là nếu await nằm trong loop thì sẽ khác biệt một chút, xét đoạn code sau:
for(var i = 0; i < 3; i ) { console.log('before async: ', i) var result = await db.collection('hospitals').findOne({name: '医療法人神甲会隈病院'}) console.log('after async: ', i) }
Nhiều người có lẽ sẽ nghĩ đoạn code trên tương đương với:
for(var i = 0; i < 3; i ) { console.log('before async: ', i) var result = await db.collection('hospitals').findOne({name: '医療法人神甲会隈病院'}) console.log('after async: ', i) } //before async: 0 //before async: 1 //before async: 2 //after async: 3 //after async: 3 //after async: 3
Nhưng không phải, mỗi khi gặp await thì phải đợi kết quả trả về mới chạy tiếp tới i tiếp theo, đoạn code tương đương sẽ là như sau:
var i = 0 console.log('before async: ', i) // before async: 0 db.collection('hospitals').findOne({name: '医療法人神甲会隈病院'}, function(){ console.log('after async: ', i) // after async: 0 i console.log('before async: ', i) // before async: 1 db.collection('hospitals').findOne({name: '医療法人神甲会隈病院'}, function(){ console.log('after async: ', i) // after async: 1 i console.log('before async: ', i) // before async: 2 db.collection('hospitals').findOne({name: '医療法人神甲会隈病院'}, function(){ console.log('after async: ', i) // after async: 2 i }) }) }) Ví dụ kiểm tra: execute() function execute() { findResult().then(function(result){ // call then() here to capture result in async function console.log(result) }) console.log('end of execute') } async function findResult() { for(var i = 0; i < 5; i ) { console.log('before findResult: ', i) result = await db.collection('hospitals').findOne({name: '医療法人神甲会隈病院'}) console.log(i, result) } return i }
Kết quả in ra sẽ là:
before findResult: 0 end of execute 0 { _id: 59f972567909e65c67a28b1b, ..................} before findResult: 1 1 { _id: ....., ..................} before findResult: 2 2 { _id: ....., ..................} before findResult: 3 3 { _id: ....., ..................} before findResult: 4 4 { _id: ....., ..................} 5 // <= this is the output of console.log(result) in callback within execute() function
Xét ví dụ khó hơn khi có 2 hàm async lồng nhau
Ví dụ 1: Hàm thứ 2 là hàm bình thường nhưng có khối async ở phía trong.
execute() function execute() { findResult().then(function(result){ console.log('result 1:', result) }) console.log('end of execute') } async function findResult() { for(var i = 0; i < 100000; i ) { var j = 100 } fA().then(function(result) { console.log('result 2: ', result) }) console.log('before findResult') var result = await db.collection('hospitals').findOne({name: '医療法人神甲会隈病院'}) console.log('after findResult') return result } function fA() { for(var i = 0; i < 100000; i ) { var j = 100 } console.log('before fA') var result = db.collection('hospitals').findOne({name: '都志見病院'}) console.log('after fA') return result }
Thứ tự in ra sẽ là:
before fA after fA before findResult end of execute result 2: {...} after findResult result 1: {...}
Giải thích:
• Trình tự in ra từ đầu cho tới "end of execute" như dự đoán vì code chạy đúng như trình tự synchronous (đồng bộ, hay từ trên xuống dưới)
• Vì sao "after findResult" lại được in ra trước "result 1: {...}" ???:
Vì khi gọi await ở trong hàm f*indResult* thì console.log('after findResult') đã bị đặt vào callback của hàm await đó rồi mới tới return result cho callback của result1 được in ra.
• Vì sao "result 2: {...}" được in ra trước "result 1: {...}" ???:
2 lời gọi fA() trong findResult() và findResult() trong execute() là 2 hàm async không phụ thuộc vào nhau nên hàm nào có kết quả trả về trước sẽ được thực thi trước.
Ở trên thì câu lệnh async ở dòng 36 có kết quả trả về nhanh hơn kết quả trả về ở câu lệnh 22.
Nếu không tin bạn có thể tùy biến cho câu lệnh ở dòng 36 có thời gian thực thi mất 10 giây, lúc này "result2: {...}" sẽ được in ra sau "result 1: {...}"
Ví dụ 2: Hàm thứ 2 là hàm async.
execute() function execute() { findResult().then(function(result){ console.log('result 1:', result) }) console.log('end of execute') } async function findResult() { for(var i = 0; i < 100000; i ) { var j = 100 } fA().then(function(result) { console.log('result 2: ', result) }) console.log('before findResult') var result = await db.collection('hospitals').findOne({name: '医療法人神甲会隈病院'}) console.log('after findResult') return result } async function fA() { for(var i = 0; i < 100000; i ) { var j = 100 } console.log('before fA') result = await db.collection('hospitals').findOne({name: '都志見病院'}) console.log('after fA') return result }
Thứ tự in ra sẽ là:
before fA before findResult end of execute after fA result 2: {...} after findResult result 1: {...}
Cái này được giải thích giống ví dụ trên, và cũng giống như ví dụ trên result 2 được in ra trước result 1 vì hàm async của nó được trả về giá trị sớm hơn.
Một số chú ý khi sử dụng Async/Await (Promise) trong Javascript
• Chú ý khi sử dụng await trong vòng lặp như đã nói phía trên.
• Khi gặp await thì những đoạn code phía sau có kết quả trả về mới thực hiện được nên nếu phần code phía sau không phụ thuộc vào await thì bạn nên xử lý như sau:
Xét ví dụ sau:
async function test() { var result1 = await db.collection('hospitals').findOne({name: '医療法人神甲会隈病院'}) console.log(result1) var result2 = await db.collection('hospitals').findOne({name: 'abcxyz'}) console.log(result2) } test()
Đoạn code trên result1 có kết quả trả về thì hàm lấy result2 mới được chạy. Nhưng điều bạn muốn là cả 2 hàm lấy result1 và result2 phải chạy song song, bạn cần chuyển thành như sau:
async function test() { var promise1 = db.collection('hospitals').findOne({name: '医療法人神甲会隈病院'}) var promise2 = db.collection('hospitals').findOne({name: 'abcxyz'}) var result1 = await promise1 console.log(result1) var result2 = await promise2 console.log(result2) } test()
Nhìn 2 đoạn code có vẻ giống nhau nhưng khác nhau một trời một vực đấy. Bạn nên đọc bài cơ chế hoạt động của Javascript để nắm được trình tự Javascript chạy các câu lệnh như thế nào.
Theo Bizfly Cloud chia sẻ
Bizfly Cloud là nhà cung cấp dịch vụ điện toán đám mây với chi phí thấp, được vận hành bởi VCCorp.
Bizfly Cloud là một trong 4 doanh nghiệp nòng cốt trong "Chiến dịch thúc đẩy chuyển đổi số bằng công nghệ điện toán đám mây Việt Nam" của Bộ TT&TT; đáp ứng đầy đủ toàn bộ tiêu chí, chỉ tiêu kỹ thuật của nền tảng điện toán đám mây phục vụ Chính phủ điện tử/chính quyền điện tử.
Độc giả quan tâm đến các giải pháp của Bizfly Cloud có thể truy cập tại đây.
DÙNG THỬ MIỄN PHÍ và NHẬN ƯU ĐÃI 3 THÁNG tại: Manage.bizflycloud