Sync và Async trong Javascript

JavaScript là ngôn ngữ lập trình Single-thread (đơn luồng), có nghĩa là tại 1 thời điểm chỉ có thể xử lý 1 lệnh. Giờ thì bạn hãy tưởng tượng client gửi request lấy dữ liệu từ một API. Ở đây có thể xảy trường hợp server có thể mất thời gian để xử lý request (tệ hơn là server không trả về kết quả) nếu ở đây đợi đến khi server trả về kết quả mới chạy tiếp thì nó sẽ khiến trang web không phản hồi. Vậy Javascript mới tạo ra Asynchronous để giúp chúng ta làm việc này (như callbacks, Promises, async/await) giúp luồng chạy của web không bị chặn lại khi đợi request. Sau đây chúng ta cùng tìm hiểu về sync, callback(ES5), promise(ES6) và async/await(ES7) trong javascript.

I. Đồng bộ và bất đồng bộ:

1. Đồng bộ (synchronous/sync) là xử lý theo từng bước. Chỉ khi nào công việc hiện tại thực hiện xong thì mới thực hiện công việc tiếp theo. Điều này sinh ra một trạng thái được gọi là trạng thái chờ.

Ưu điểm:

  • Hạn chế mắc các lỗi liên quan đến quá trình chạy
  • Dễ dàng sửa lỗi

Hạn chế:

  • Vì chạy tuần tự và phải chờ đợi nhau nên sinh ra trạng thái chờ. Thời gian xử lý lâu khi các câu lệnh cần thao tác với dữ liệu bên ngoài gây ảnh hưởng đến trải nghiệm người dùng

2. Bất đồng bộ (asynchronous/async) cho phép xử lý nhiều tác vụ cùng lúc. Nếu tác vụ nào xong trước sẽ cho ra kết quả trước.

Ưu điểm:

  • Xử lý nhiều tác vụ cùng lúc nên giảm thiểu được thời gian xử lý và không sinh ra trạng thái chờ

Hạn chế:

Lưu ý:

Hạn chế xử dụng bất đồng bộ trong các tác vụ thêm, sửa với cở sở dữ liệu. Vì một công việc phải trải qua hai giai đoạn:

  • Validate dữ liệu
  • Insert vào database

=> Giả sử giai đoạn validate hoàn thành sau giai đoạn insert dữ liệu thì nó là một cái gì đó rất tệ nếu giai đoạn validate có lỗi

II. Callback (ES5)

1. Khái niệm:

Callback là giải pháp đầu tiên được đưa ra của Javascript để giải quyết các vấn đề liên quan đến xử lý bất đồng bộ theo đúng trình tự mong muốn.

Định nghĩa về callback theo trang MDN web docs

A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action.

Callback là một hàm được truyền vào một hàm khác dưới dạng đối số, sau đó được gọi bên trong hàm để hoàn thành một số loại quy trình hoặc hành động.

2. Ví dụ và cách hoạt động

Ví dụ: Con mèo ăn cơm mất 5s sau đó uống nước

Ví dụ trên cho kết quả uống nước xong trước ăn cơm mặc dù hàm ăn cơm được gọi trước. => Để con mèo ăn và uống theo đúng trình tự thì chúng ta cần phải có thông tin khi nào con mèo ăn xong mới cho nó uống nước.

3. Ưu nhược điểm của callback

Ưu điểm:

  • Là mô hình khá phổ biến nên rất dễ hiểu.
  • Rất dễ implement trong function của chúng ta

Nhược điểm:

  • Khi thao tác bất đồng bộ, các callback phải chờ nhau thực hiện dẫn đến tổng thời gian hoàn thành tác vụ lâu hơn.
  • Dài dòng, khó đọc, khó bảo trì.
  • Callback hell (pyramid of doom): là cách code không tối ưu, dẫn đến nhiều callback lồng nhau gây khó debug hay bảo trì.

Ví dụ: callback hell

a(function(resultA){
  b(resultA, function(resultB){
	c(resultB, function(resultC){
	  console.log(resultC);
	});
  });
});

Để tránh callback hell thì có nhiều cách như: thiết kế ứng dụng theo dạng module, đặt tên callback, định nghĩa hàm trước khi gọi,...

III. Promise (ES6)

1. Định nghĩa

Promise là người em sinh sau đẻ muộn so với callback nên nó có thể khác phục được những vấn đề như callback hell hay pyramid of doom khá là tốt, giúp code trở nên dễ đọc, gọn gàng hơn.

Promise có nghĩa là một sự hứa hẹn hay lời hứa, mà lời hứa thì sẽ có hai trạng thái là hoàn thành và thất bại.

2. Cách sử dụng

Cách tạo ra một promise:

Hàm executor là một hàm sẽ được gọi ngay khi Promise được gọi tới, nó chứa hai tham số:

  • resolve: hàm sẽ được gọi khi xử lý thành công.
  • reject: hàm được gọi khi xử lý thất bại.

Tại mỗi thời điểm, Promise sẽ có những trại thái khác nhau, bắt đầu với pending hay unsetted. Trạng thái này chính là trạng thái ban đầu của Promise khi được khởi tạo và đang chờ kết quả trả về. Khi quá trình xử lý thực hiện xong xuôi, promise sẽ chuyển sang trạng thái setted, khi kết quả được trả về, sẽ có hai khả năng có thể xảy ra:

  • api
      .getUser('user')
      .then(user => api.getPostsOfUser(user))
      .then(posts => api.getCommentsOfPosts(posts))
      .catch(error => {
        throw error
      })
    0: trạng thái xử lý thành công.
  • api
      .getUser('user')
      .then(user => api.getPostsOfUser(user))
      .then(posts => api.getCommentsOfPosts(posts))
      .catch(error => {
        throw error
      })
    1: trạng thái xử lý thất bại.

Ví dụ: trước hôm thi đại học, mẹ ngồi cạnh bạn và nói "Cố đỗ đại học rồi mẹ sẽ mua iphone 12 pro max cho". Khi đó, lời hứa của mẹ bạn sẽ trông như sau:

Khi một promise được thực hiện, nếu thành công thì sẽ gọi callback trong hàm then, nếu thất bại thì promise sẽ gọi promise trong hàm catch.

Sync và Async trong Javascript

3. Promise chaining

Nếu xử lý các câu lệnh bất đồng bộ liên tiếp nhau với callback rất dễ dẫn đến tình trạng callback hell như đã nói ở trên khi mà có quá nhiều hàm callback bị lồng vào nhau làm cho việc đọc hiểu cũng như debug trở nên khó khăn. Promise chaining hay chuỗi promise được sinh tra nhằm khắc phục vấn đề trên. Giá trị trả về của hàm

api
  .getUser('user')
  .then(user => api.getPostsOfUser(user))
  .then(posts => api.getCommentsOfPosts(posts))
  .catch(error => {
    throw error
  })
2 sẽ là một promise khác, do đó có thể dùng promise để gọi liên tiếp các hàm bất đồng bộ. Promise thứ hai sẽ được xử lý khi promise thứ nhất trả về
api
  .getUser('user')
  .then(user => api.getPostsOfUser(user))
  .then(posts => api.getCommentsOfPosts(posts))
  .catch(error => {
    throw error
  })
0 hoặc
api
  .getUser('user')
  .then(user => api.getPostsOfUser(user))
  .then(posts => api.getCommentsOfPosts(posts))
  .catch(error => {
    throw error
  })
4.

Ví dụ: Có một đoạn callback như sau:

Đoạn callback trên sau khi được viết lại bằng Promise:

api
  .getUser('user')
  .then(user => api.getPostsOfUser(user))
  .then(posts => api.getCommentsOfPosts(posts))
  .catch(error => {
    throw error
  })

Trong ví dụ ở trên ta lần lượt gọi đến ba hàm

api
  .getUser('user')
  .then(user => api.getPostsOfUser(user))
  .then(posts => api.getCommentsOfPosts(posts))
  .catch(error => {
    throw error
  })
5,
api
  .getUser('user')
  .then(user => api.getPostsOfUser(user))
  .then(posts => api.getCommentsOfPosts(posts))
  .catch(error => {
    throw error
  })
6 và
api
  .getUser('user')
  .then(user => api.getPostsOfUser(user))
  .then(posts => api.getCommentsOfPosts(posts))
  .catch(error => {
    throw error
  })
7. Chỉ cần một trong ba hàm này bị lỗi, promise sẽ chuyển qua trạng thái reject và callback trong hàm catch sẽ được gọi đến, lúc này việc bắt lỗi sẽ trở nên dễ dàng hơn.

Tuy nhiên tiện là như vậy nhưng toàn bộ các hàm

api
  .getUser('user')
  .then(user => api.getPostsOfUser(user))
  .then(posts => api.getCommentsOfPosts(posts))
  .catch(error => {
    throw error
  })
2 chỉ được tính là một câu lệnh. Điều này sẽ gây khó khăn cho việc debug sau này.

4. Promise.all()

api
  .getUser('user')
  .then(user => api.getPostsOfUser(user))
  .then(posts => api.getCommentsOfPosts(posts))
  .catch(error => {
    throw error
  })
9 nhận và là một đối số và thông thường là một mảng các promise. Trạng thái của promise này sẽ là
api
  .getUser('user')
  .then(user => api.getPostsOfUser(user))
  .then(posts => api.getCommentsOfPosts(posts))
  .catch(error => {
    throw error
  })
0 nếu trạng thái của tất cả các promise được truyền vào là
api
  .getUser('user')
  .then(user => api.getPostsOfUser(user))
  .then(posts => api.getCommentsOfPosts(posts))
  .catch(error => {
    throw error
  })
0 ngược lại, promise sẽ mang trạng thái
api
  .getUser('user')
  .then(user => api.getPostsOfUser(user))
  .then(posts => api.getCommentsOfPosts(posts))
  .catch(error => {
    throw error
  })
4.

5. Promise.race()

Khác với

api
  .getUser('user')
  .then(user => api.getPostsOfUser(user))
  .then(posts => api.getCommentsOfPosts(posts))
  .catch(error => {
    throw error
  })
9, hàm
async function getUser() {
  return ...
}
4 sẽ xử lý promise đầu tiên có kết quả trả về không quan tâm kết quả trả về có lỗi hay không.

Ví dụ promise1 được resolve đầu tiên nên giá trị mà promise trả về sẽ là giá trị của promise1 và bằng 1.

IV. Async/await (ES7)

Async/await là cơ chế giúp bạn thực thi các thao tác bất đồng bộ một cách tuần tự hơn , giúp đoạn code nhìn qua tưởng như đồng bộ nhưng thực ra lại là chạy bất đồng bộ, giúp chúng ta dễ hình dung hơn.

Sync và Async trong Javascript

1. Async

Để định nghĩa một hàm bất đồng bộ với async, ta cần khai báo từ khóa

async function getUser() {
  return ...
}
5 ngay trước từ khóa định nghĩa hàm.

Regular function:

async function getUser() {
  return ...
}

Function expression:

getUser = async function() {
  return ...
}

Kết hợp với cú pháp arrow function của ES6

Giá trị trả về của Async Function sẽ luôn là một Promise mặc cho bạn có gọi await hay không, nếu trong code không trả về Promise nào thì sẽ có một promise mới được resolve với giá trị lúc đầu (nếu không có giá trị nào trong return kết quả trả về sẽ là undefine). Promise này sẽ ở trạng thái thành công với kết quả được trả về qua từ khóa return của hàm async, hoặc ở trạng thái thất bại với kết quả được đẩy qua từ khóa throw trong hàm async.

2. Await

Về cơ bản thì

async function getUser() {
  return ...
}
6 giúp cho cú pháp trông dễ hiểu hơn, thay thì phải dùng
api
  .getUser('user')
  .then(user => api.getPostsOfUser(user))
  .then(posts => api.getCommentsOfPosts(posts))
  .catch(error => {
    throw error
  })
2 nối tiếp nhau thì chỉ cần đặt
async function getUser() {
  return ...
}
6 trước mỗi hàm mà chúng ta muốn đợi kết quả của thao tác bất đồng bộ. Chỉ dùng được
async function getUser() {
  return ...
}
6 trong hàm nào có
async function getUser() {
  return ...
}
5 đứng phía trước.