Series Javascript thần thánh – Bối rối vì quá nhiều cách xử lí bất đồng bộ trong Javascript?

This entry is part 5 of 5 in the series Series Javscript thần thánh

Trong bài viết trước, ta đã trình bài về các phiên bản nâng cấp của Javascript. Trong bài viết bài, chúng ta sẽ cũng tìm hiểu về các cách xử lí bất đồng bộ trong Javascript.

Đầu tiên, tại sao chúng ta cần phải xử lí bất đồng bộ?

Javascript là ngôn ngữ đơn luồng:

Khác với các ngôn ngữ khác, Javascript là ngôn ngữ lập trình đơn luồng. Điều này có nghĩa là chỉ có một luồng xử lí duy nhất có mọi tác vụ như đọc file, yêu cầu dữ liệu từ API và tiếp nhận thao tác.

Một trong những ví dụ đơn giản nhất để giải thích về luồng đó là game. Giả sử bạn có một game chơi cờ ca rô. Để bới nhàm chán, bạn thêm nhạc nền vào cho game. Khi đó, game của bạn phải cần phải có 2 luồng xử lí song song với nhau: 1 luồng dành cho việc đọc file mp3, 1 luồng để xử lí thao tác click chuột. Đây là cách xử lí kinh điển của các ngôn ngữ hướng đối tượng, tiêu biểu là Java. Các này gọi là xử lí đa luồng (multithread).

Tuy nhiên, Javascript chỉ có 1 luồng duy nhất. Javascript chọn cách xử lí tiến trình theo phương pháp bất đồng bộ để tránh tình trạng “treo” máy. (I/O blocking).

Xử lí bất đồng bộ:

Đâu tiên, chúng ta sẽ tìm hiểu xem đồng bộ và bất đồng bộ khác nhau như thế nào?

Chúng ta xem cùng phân tích quá trình nấu cơm và luộc rau như sau. ?

Đồng bộ:

Trong quá trình nấu cơm và luộc rau, ta sẽ có thời gian đợi cơm chín và rau chín. Quá trình chờ đợi này tương tư như quá trình đọc file hoặc gọi API, đều sẽ gây tắc nghẽn luồng xử lí.

Bất đồng bộ:

Trong quá trình xử lí đồng bộ, nếu chỉ có 1 luồng xử lí duy nhất nên ta phải thực hiện tuần tự các tiến trình. Do đó, ta cần phải ngồi đợi cơm chín thì mới đi luộc rau được.

Nếu chúng ta xử lí bất đồng bộ thì quá trình sẽ như sau:

Thay vì phải ngồi chờ cơm chín rồi mới đi lặt rau thì tranh thủ thời gian chờ này, ta có thể đi luộc rau trước. Sau khi cơm chín, sẽ có tiếng tin tin của nồi cơm điện, ta sẽ tiếp tục dọn cơm ra bàn ăn. Tiếng tin tin này giống như hàm trả về (callback) trong Javascript, dùng để “đánh dấu” một tiến trình gây tắc nghẽn đã thực thi xong và có thể tiếp tục thực hiện tiến trình khác.

Cách cài đặt xử lí bất đồng bộ của Javascript:

Javascript cung cấp cho ta nhiều cách để xử lí tiến trình bất đồng bộ. Có 3 cách cơ bản đó là: Hàm trả về (callback), promise và async/await.

Callback:

Bản chất Javascript được thiết kế ban đầu là ngôn ngữ lập trình theo hướng function programing. Trong đó, hàm trả về là cách xử lí nguyên thủy nhất để xử lí bất đồng bộ.

Hàm trả về trong Javascript có thể hiểu đơn giản là việc có thể truyền một hàm vào hàm khác dưới dạng một tham số.

Ví dụ: Ta sử dụng AJAX để gọi API từ server nào đó:

function callAPI(callback) {
   var url = "https://reqres.in/api/users?page=2";
   var xhttp = new XMLHttpRequest();
   xhttp.open("GET", url, true);
   xhttp.send();
  
   xhttp.onreadystatechange = function() {
 
    if (this.readyState == 4 && this.status == 200) {
      var data = JSON.parse(this.response);
      callback(data);
    }
  };
 
}

function handleData(data){
  console.log(data);
}

callAPI(handleData);



Trong ví dụ trên, ta cần khai báo một hàm có tên là handleData. Hàm này sẽ được truyền vào hàm callAPI dưới dạng một tham số có tên là callback. Trong hàm callAPI, sau khi có dữ liệu, ta sẽ dùng tham số callback để thực thi phần xử lí dữ liệu.

Điểm hạn chế của sử dụng hàm trả về là cách triển khai cực kì khác lạ so với các ngôn ngữ khác: “Tại sao một hàm có thể dùng để làm tham số nhỉ?”. ?

Ngoài ra, nếu ứng dụng của chúng ta gọi rất nhiều API liên tiếp thì số lượng hàm lồng ghép vào nhau càng nhiều. Hiện tượng lồng ghép này gọi là callback hell:

Callback hell sẽ “giúp” code của bạn có hình như trên

Promise:

Promise được bổ sung vào Javascript năm 2015 sau khi cập nhật bản nâng cấp ES6. ES6 sẽ cung cấp ta cú pháp để khai báo và triển khai promise. Trong bài biết viết này, mình chỉ ví dụ về cách triển khai promise như thế nào thôi nhé.

Trong ví dụ dưới, chúng ta sẽ dùng hàm fecth để lấy dữ liệu từ API.  Hàm fecth là hàm được định nghĩa sẵn của Javascript. Về bản chất, hàm fetch trả về một promise. Cách triển khai của của promise này như sau:

fetch('https://reqres.in/api/users?page=2')
  .then((response)=>{
  return response.json();
  })
  .then((json)=>{
  console.log(json);
})

Trong ví dụ trên, để triển khai promise ta sẽ dùng hàm then. Trong hàm then ta sẽ truyền vào một hàm khác (ở đây mình dùng cú pháp hàm mũi tên để cho gọn).

Việc sử dụng promise có phần gọn gàng hơn so với dùng hàm trả về. Với promise, chúng ta có thể sử dụng hàm then liên tiếp nhau để hạn chế lồng ghép các hàm vào nhau. Tuy nhiên, trong hàm then, chúng ta vẫn phải truyền vào một tham số là một hàm, cách này cũng chưa đẹp lắm. ?

Async và await:

Sau khi bản nâng cấp ES8 (năm 2017), ta có thêm cú pháp hay ho nữa cho Javascript là: async/await. Cú pháp này được xem là tối ưu nhất để làm cho code chúng ta “sạch sẽ” hơn. Async/await mang đến cú pháp xử lí một promise mà không cần dùng đến hàm then. ?

Trong ví dụ dưới đây, chúng sẽ viết lại cách triển khai của hàm fecth sử dụng cú pháp async/await:

const makeRequest = async () => {
  const response = await fetch('https://reqres.in/api/users?page=2');
  const json = await response.json();
  console.log(json); 
};

makeRequest();

Trong ví dụ trên, thay vì sử dụng hàm then hai lần liên tiếp, ta chỉ cần dùng từ khóa await. Cần lưu ý là await phải luôn đi kèm với từ khóa async ở phần khai báo hàm.

Với việc sử dụng async/await, chúng ta không cần phải truyền một hàm bất kì nào để làm tham số nữa, loại bỏ hoàn toàn việc lồng ghép các hàm vào nhau.

Kết:

Javascript là ngôn ngữ đơn luồng và bất đồng bộ.

Để xử lí bất đồng bộ trong Javascript, ta có thể dùng hàm trả về (callback), promise hoặc async/await. Trong 3 cách trên, ta nên ưu tiên sử dụng async/await để có phong cách code mạch lạc hơn.

 

 

Series Navigation<< Series Javascript thần thánh – ES7 bổ sung nhẹ vài tính năng cho Javascript

Categories: