Cơ bản về async await trong javascript
nodejs
73
Javascript
265
async
8
White

Đào Văn Hùng viết ngày 29/09/2018

Nếu cho tôi 6 tiếng để đốn hạ một cái cây, tôi sẽ dành 4 tiếng đầu tiên để mài rìu.
-- Abraham Lincoln

Bạn có thể đọc bài gốc tại đây

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.

1. 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

2. 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:

VD1:

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 VD2 sau đây:

VD2:

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,
.....}

3. 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.

VD1:

function test() {
  var promise = returnTen()
  console.log(promise)
}

async function returnTen() {
  return 10
}

test() // Promise { 10 }

VD này promise trả về có kết quả là 10 luôn.

VD2:

function test() {
  var promise = returnTen()
  console.log(promise)
}

async function returnTen() {
  return await 10                                                         
}

test()  // Promise { <pending> }

VD này promise trả về chưa có kết quả luôn.

4. 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

5. Xét ví dụ khó hơn khi có 2 hàm async lồng nhau.

VD1: 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 findResult 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()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: {...}"

VD2: 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.

6. 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ụ:

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.

Link tham khảo:
https://stackoverflow.com/questions/43302584/why-doesnt-the-code-after-await-run-right-away-isnt-it-supposed-to-be-non-blo

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Đào Văn Hùng

9 bài viết.
51 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
31 18
(Ảnh) Link gốc bài viết (Link). Mở đầu Khi học một ngôn ngữ lập trình, một trong những thứ bạn phải nắm được đó là ngôn ngữ đó truyền biến vào ...
Đào Văn Hùng viết 4 tháng trước
31 18
White
26 13
Học hỏi chính là kinh nghiệm. Những thứ khác chỉ là thông tin. Albert Einstein Link gốc bài viết tại (Link). Đối với những bạn lập trình...
Đào Văn Hùng viết 1 năm trước
26 13
White
22 9
(Ảnh) Link gốc bài viết (Link) Mở đầu Có lẽ khi lập trình không nhiều người quan tâm tới cách bộ nhớ tổ chức lưu trữ và thao tác với biến như t...
Đào Văn Hùng viết 5 tháng trước
22 9
Bài viết liên quan
White
1 0
Lâu lâu không động vào nodejs không biết mấy ông tool tiếc này đi đâu về đâu rồi. Trước đây thì mình vẫn có thể dùng istanbul với mocha đơn giản th...
Hoàng Duy viết 2 năm trước
1 0
White
5 1
Xử lý đồng bộ một mảng bằng Promise thay cho async.eachSeries Tựa Đang muốn chạy một hàm trong đó xử lý đồng bộ từng phần tử trong một mảng, do g...
Cuong Pham viết 2 năm trước
5 1
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
9 bài viết.
51 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!