Từ Javascript thuần đến RxJS (Phần 2)

Phần tiếp theo của loạt bài hiểu về cách code Javascript hiện đại. Từ Javascript thuần đến RxJS (Phần 2)

Hỏi ngu:Callback và async đúng là một cặp đôi hoàn hảo!

Đúng. Tuy nhiên không có gì là quá hoàn hảo. Nếu chúng ta quá lạm dụng 2 cặp đôi này ta sẽ bị rơi vào vòng xoáy gọi là callback-hell.

Callback-Hell

Hãy xem xét một ví dụ sau đây:

function XuLy(data) {
  console.log(data);
}
function request(url, HamCallBack) {
  const xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function Success() {
    HamCallBack(xhr.responseText);
  };
  xhr.open("GET", url, true);
  xhr.send();
}
request("https://api.github.com/search/users?q=location:delhi", XuLy); //Dòng 1. Tôi muốn hàm này chạy trước
request("https://jsonplaceholder.typicode.com/posts/2", XuLy);  //Dòng 2. Tôi muốn dòng 1 lấy xong dữ liệu rồi tôi mới chạy dòng 2
console.log("1"); //Kệ bố con 2 dòng trên, tôi cần được hiển thị đầu tiên.

Kết quả thật đáng buồn. Bởi vì dùng async nên mọi thứ trở thành một cuộc đua. Anh nào xong trước thì hiện ra trước.

Bởi vì dòng 1 load data quá nặng nên tốn thời gian. Dòng 2 đã không đợi nó hoàn thành rồi mới chạy. Lúc này Async và Callback không còn là cặp đôi hoàn hảo cho bài toán này nữa.

Đôi khi chúng ta cần lấy được một dữ liệu từ server rồi mới thực hiện tác vụ tiếp theo liên quan đến dữ liệu vừa nhận đc. Bài toán đặt ra là vẫn viết chương trình kiểu async cho nó nhanh mà vẫn phải quản lý thứ tự các hàm được gọi theo ý mình (quản lý theo sync).

Hỏi ngu:Hê hê đoạn này giải quyết dễ. Cứ viết "xhr.open("GET", url, false);" thế là hàm này biến thành sync và thứ tự các hàm sẽ được chạy lần lượt. Vậy hãy thử xem.

Điều đáng buồn là dòng 3 đã phải đợi 2 dòng kia chạy xong. Bài toán mới chỉ giải quyết được một nửa. Nếu muốn triệt để ta phải biến hàm request() ở dòng 2 thành một hàm callback. Bởi vì như ta đã biết, hàm callback nó có tác dụng đợi hàm mẹ chạy xong mới chạy.

Hỏi ngu:Nếu thế thì lại dễ. Chỉ 2 hàm callback gọi nhau, ta chỉ việc vứt dòng 2 vào hàm HamCallback() để nó chạy lần lượt là dc nhỉ?

function XuLy(data) {
  console.log(data);
}
function request(url, HamCallBack) {
  const xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function Success() {
    HamCallBack(xhr.responseText);
    request("https://jsonplaceholder.typicode.com/posts/2", XuLy);  //Gọi dòng 2 sau dòng 1? Viết thế này liệu có ổn không nhỉ?
  };
  xhr.open("GET", url, true);
  xhr.send();
}
request("https://api.github.com/search/users?q=location:delhi", XuLy); //Dòng 1 và dòng 2 viết chung
console.log("1"); //Kệ bố con 2 dòng trên, tôi cần được hiển thị đầu tiên.

Viết thế này thì chết toi rồi. Trình console bị nháy liên tục chứng tỏ là bạn đã viết vòng lặp vô hạn. Không có cách nào dừng được ngoài việc tắt Chrome. Lý do là vì hàm XuLy trở thành hàm đệ quy, trong xử lý lại gọi xử lý rồi gọi tiếp liên tục.

Muốn viết callback chuẩn, lúc này mỗi một hàm callback sẽ phải có hàm XuLy của riêng nó. Vì không muốn tạo quá nhiều hàm thừa. Ta sẽ kết hợp kỹ thuật Hàm không cần tên để tạo 1 hàm xử lý ở mỗi callback luôn.

Ta viết lại như sau:

function request(url, HamCallBack) {
  const xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function Success() {
    if (this.readyState === xhr.DONE) { //Đoạn này check để chắc chắn là đã có dữ liệu thì mới gọi callback
        HamCallBack(JSON.parse(xhr.responseText));
    }
  };
  xhr.open("GET", url, true);
  xhr.send();
}
request("https://api.github.com/search/users?q=location:delhi", (data)=>{ //Đây là sử dụng arrow function để đón giá trị từ HamCallback()
    console.log(data);
    request("https://jsonplaceholder.typicode.com/posts/1", (data)=>{
      console.log(data);
    });
});
console.log("1");

Như ta thấy. Bài toán đã giải xong, thứ tự của các hàm đã hoạt động đúng ý. Chương trình vẫn là async, không chặn hàm. Cho dù server chậm, thì luồng data nhận được vẫn theo đúng thứ tự sync.

Hỏi ngu:Vậy thì có vẻ ổn. Nhưng 3 hàm đợi nhau hoặc 4 hàm đợi nhau thì viết thế nào?

Lúc này ta sẽ viết callback lồng nhau tiếp.

Khái niệm Callback Hell ra đời từ đây. Việc bắt buộc phải viết theo kiểu callback-hell là không ai mong muốn cả. Nhưng để chương trình chạy đúng thì phải viết. Và việc debug cái tam giác này là khá khó khăn.

Và để giải quyết bài toán Callback-hell, một kỹ thuật mới đã được ra đời. Đó là dùng đến Promises

Promises

Hãy thử viết lại ví dụ vừa rồi từ Callback-hell sang Promises như sau:

function request(url) {
  return new Promise((resolve, reject) => { //Chỗ này mới, sử dụng Promise thay vì Callback
    const xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function Success() {
      if (this.readyState === xhr.DONE) {
        resolve(JSON.parse(xhr.responseText));  //Chỗ này không có callback nữa để gọi. Ta dùng hàm resolve() để trả về kết quả.
      }
    };
    xhr.open("GET", url, true);
    xhr.send();
    xhr.onerror = function(error) {
      reject("Error");
    };
  });
}

request("https://api.github.com/search/users?q=location:delhi")
  .then(data1 => {
    console.log(data1);
    return request("https://jsonplaceholder.typicode.com/posts/1");
  })
  .then(data2 => {
    console.log(data2);
    return request("https://api.icndb.com/jokes/random/1");
  })
  .then(data3 => {
    console.log(data3);
    return request("https://jsonplaceholder.typicode.com/posts/2");
  })
  .then(data4 => {
    console.log(data4);
  })
  .catch(error => {
    console.log(error);
  });

Lúc này code viết sẽ "phẳng" hơn, gọn gàng hơn. Hãy giải thích một chút về cách hoạt động của hàm Promises nhé.

Đầu tiên chúng ra cần tạo ra một Promise (lời hứa sẽ giải quyết vấn đề) vào bên trong hàm cần xử lý. Hàm này sẽ có 2 tham số là resolve (giải quyết ngon lành) hoặc reject(không giải quyết đc, thất bại). Khi đã nhận được đủ data, ta gọi hàm resolve() như là gọi một callback để trả về kết quả. Chú ý là resolve sẽ trả về 1 giá trị nhận được từ hàm mẹ, chứ không phải trả về một hàm (Như trong callback-hell gọi hàm HamCallBack() là nó trả về 1 hàm chứ k phải trả về giá trị).

Từ khóa .then() chỉ có thể hoạt động nếu như hàm được gọi là hàm Promises. Vì hàm request() trả về một Promise nên ta có thể dùng .then() để lấy kết quả trả về từ resolve(). Sau đó bên trong .then() ta lại tạo 1 Promise tiếp theo và đợi kết quả ở hàm .then() kế tiếp.

Cách viết này cũng khá giống callback lồng nhau. Cách hoạt động cũng giống. Nhưng kết quả là code được viết gọn gàng hơn. Thậm chí 4 hàm then() và chỉ có 1 hàm catch() để bắt lỗi chung cho cả 4 lần gọi. Hãy tưởng tượng mỗi hàm callback phải có hàm xử lý lỗi riêng xem. Ác mộng.

Ngày nay. Promises cũng được dùng thay cho callback. Nếu chỉ gọi async một lần ta cũng viết dưới dạng Promises sẽ ngắn hơn là Callback. Điểm khác biệt là Promises trả về giá trị, còn Callback trả về hàm.

Hỏi ngu: Cạn lời! Quá tuyệt vời!

Nếu tôi nói là vẫn còn cách viết code đẹp hơn nữa thì bạn nghĩ sao?

Hỏi ngu:

Như ta thấy là vẫn còn phải .then() liên tục. Cách viết này vẫn gọi là cách viết nối nhau (Chaining Promises). Chưa thực sự tách được hàm ra. Chúng ta cần dùng đến Async/Await để viết code không cần đến .then()

Async/Await

Mời các bạn đọc tiếp phần 3.