Cơ bản về Async Await trong Javascript

1889
03-03-2018
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.

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.

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

SHARE