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 đây mình 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.
>> Tham khảo thêm: Scope, Closure, This và tổ chức bộ nhớ trong Javascript