Web nâng cao (8) - JavaScript cho React (7) - bất đồng bộ bằng callback

Bài trước: Web nâng cao (7) - JavaScript cho React (6)

-----

1.1.1       Lập trình bất đồng bộ bằng callback

Trong JavaScript có 3 kĩ thuật để lập trình bất đồng bộ gồm: callback, promise và async/await.

Để hiểu về kĩ thuật lập trình bất đồng bộ bằng callback, chúng ta sẽ cùng làm một chức năng đơn giản là lấy dữ liệu từ Internet về trình duyệt.

Bạn vào trang Google, gõ từ khóa JSON placeholder để tìm tới trang web cho phép bạn có thể truy cập và lấy dữ liệu dạng JSON về trình duyệt. Ví dụ trang: https://jsonplaceholder.typicode.com/

Trang JSON Placeholder cho phép bạn lấy dữ liệu về dưới dạng lời gọi API, bạn có thể nhập trực tiếp các lời gọi API vào thanh địa chỉ của trình duyệt để lấy dữ liệu về, ví dụ:

https://jsonplaceholder.typicode.com/todos/1

      /*

      {

        "userId": 1,

        "id": 1,

        "title": "delectus aut autem",

        "completed": false

      }

      */

Bạn có thể thay đổi giá trị Id của lời gọi API thành 2, 3, 4 hoặc “rỗng” để xem kết quả.

            https://jsonplaceholder.typicode.com/todos/2

      https://jsonplaceholder.typicode.com/todos/3

      https://jsonplaceholder.typicode.com/todos/4

      https://jsonplaceholder.typicode.com/todos/

Trong JavaScript bạn có 2 cách để tạo một request là sử dụng đối tượng XMLHttpRequest và Fetch API. Phần này sẽ thực hành sử dụng đối tượng XMLHttpRequest.

Để tạo đối tượng request, bạn sử dụng lệnh sau:

const request = new XMLHttpRequest();

Tùy từng thời điểm, request sẽ có các trạng thái sau:

Giá trị

Trạng thái

Mô tả

0

UNSENT

Đã tạo ra đối tượng request, chưa gọi phương thức open()

1

OPENED

Đã gọi phương thức open()

2

HEADER_RECEIVED

Đã gọi phương thức send(), đã có thông tin các header và trạng thái

3

LOADING

Đang tải dữ liệu về máy client, dữ liệu chứa trong thuộc tính responseText

4

DONE

Hoàn thành một phiên gửi request

Đoạn mã nguồn sau đây viết theo kiểu tuần tự để lấy dữ liệu từ server về client, đồng thời kiểm tra giá trị của trạng thái request:

// khai báo và tạo request

     const request = new XMLHttpRequest();

     console.log(request, request.readyState);

     request.open('GET', 'https://jsonplaceholder.typicode.com/todos/');

     console.log(request, request.readyState);

     // gửi request tới server

     request.send();

     // xuất ngay kết quả do server trả về

     console.log(request, request.readyState); // chưa có dữ liệu do server chưa trả về kịp

Rõ ràng bạn thấy cách viết theo kiểu tuần tự khá bị động, rất khó để biết khi nào thì client nhận đủ dữ liệu của server để thực hiện các xử lý tiếp theo.

Bạn có thể viết theo kiểu bất đồng bộ bằng cách dùng hàm setTimeout(), tuy nhiên bạn phải biết được “khi nào thì client nhận đủ dữ liệu từ server?”.

// khai báo và tạo request

     const request = new XMLHttpRequest();

     console.log(request, request.readyState);

     request.open('GET', 'https://jsonplaceholder.typicode.com/todos/');

     console.log(request, request.readyState);

     // gửi request tới server

     request.send();

     // xuất ngay kết quả do server trả về

     console.log(request, request.readyState); // chưa có dữ liệu do server chưa trả về kịp

     // chờ đủ lâu để xuất kết quả do server trả về

     setTimeout(() => console.log(request, request.readyState), 4000);

Có cách hay hơn để biết “khi nào client nhận đủ dữ liệu” là dựa vào sự kiện readystatechange. Sự kiện readystatechange sẽ phát sinh mỗi khi đối tượng request có thay đổi trạng thái, khi trạng thái thay đổi bạn sẽ gọi một hàm callback để xử lý. Đồng thời bạn cần kiểm tra khi nào request.readyState có giá trị là 4 và client thực sự nhận được dữ liệu từ server (status = 200) thì mới thực hiện các xử lý trên dữ liệu:

 // khai báo và tạo request

      const request = new XMLHttpRequest();

      // gắn bộ lắng nghe sự kiện readystatechange cho đối tượng request

      request.addEventListener('readystatechange', () => {

          if(request.readyState === 4 && request.status === 200){

            console.log(request.responseText);

          } else if (request.readyState === 4 ){

            console.log("Không nhận được dữ liệu từ server!");

          }

      });

     request.open('GET', 'https://jsonplaceholder.typicode.com/todos/');

    // gửi request tới server

     request.send();

Ở đoạn mã trên, bạn có thể thay đổi đường dẫn của API thành “https://jsonplaceholder.typicode.com/todosabc/” để kiểm tra cho trường hợp không lấy được dữ liệu từ server về client.

Giờ chúng ta sẽ đưa cả đoạn mã xử lý ở trên vào một hàm, có tên là getTodos()

const getTodos = () => {

        const request = new XMLHttpRequest();

        request.addEventListener('readystatechange', () => {

          if(request.readyState === 4 && request.status === 200){

            console.log(request.responseText);

          } else if (request.readyState === 4 ){

            console.log("Không nhận được dữ liệu từ server!");

          }

        });

        request.open('GET', 'https://jsonplaceholder.typicode.com/todos/');

        request.send();

      };

      getTodos();

Chúng ta sẽ tổ chức lại đoạn mã trên theo kiểu callback để tiện cho việc sử dụng về sau. Vậy là chúng ta sẽ sử dụng callback ở 2 nơi, một là trong bộ lắng nghe sự kiện addEventListener(), hai là trong hàm getTodos():

      const getTodos = (callback) => {

        const request = new XMLHttpRequest();

 

        request.addEventListener('readystatechange', () => {

          if(request.readyState === 4 && request.status === 200){

            callback(undefined, request.responseText);

          } else if (request.readyState === 4 ){

            callback("Không nhận được dữ liệu từ server!", undefined);

          }

        });

        request.open('GET', 'https://jsonplaceholder.typicode.com/todos/');

        request.send();

      };

      getTodos( (err, data) => {

        if(err){

          console.log(err);

        } else {

          console.log(data);

        }

      });

Với hàm getTodos( (err, data) => { }), bạn có thể tùy ý viết các xử lý khác nhau dựa trên dữ liệu errdata nhận được.

Bạn có thể kiểm tra lại về tính bất đồng bộ của đoạn mã bằng cách thêm vào các dòng lệnh khác như sau:

…… 

console.log("Câu lệnh 1");

      console.log("Câu lệnh 2");

      getTodos( (err, data) => {

        if(err){

          console.log(err);

        } else {

          console.log(data);

        }

      });

      console.log("Câu lệnh 4");

// Câu lệnh 1

// Câu lệnh 2

// Câu lệnh 4

// dữ liệu của hàm callback getTodos

Chúng ta sẽ chuyển đổi dữ liệu do server gửi về thành đối tượng (dùng phương thức JSON.parse()) để tiện cho việc xử lý sau này:

request.addEventListener('readystatechange', () => {

          if(request.readyState === 4 && request.status === 200){

            const data = JSON.parse(request.responseText);

            callback(undefined, data);

          } else if (request.readyState === 4 ){

            callback("Không nhận được dữ liệu từ server!", undefined);

          }

        });

Tóm lại, để lập trình bất đồng bộ sử dụng kĩ thuật callback, bạn cần thực hiện các việc sau:

– Bước 1: sử dụng kĩ thuật lập trình callback của JavaScript

– Bước 2: định nghĩa hàm, nhằm xử lý một tác vụ cụ thể, trong đó có gọi tới hàm callback cho trường hợp xử lý thành công và xử lý thất bại

– Bước 3: gọi hàm (đã định nghĩa ở Bước 2) để thực thi, trong đó định nghĩa thêm các xử lý dựa trên dữ liệu trả về của hàm callback trong trường hợp thành công và thất bại

Xem hình minh họa:


Hiện tượng callback hell

Callback hell là hiện tượng quá nhiều hàm callback lồng nhau, làm cho mã nguồn khó đọc, khó bảo trì. Chữ “hell” có nghĩa là địa ngục, nỗi ám ảnh; ý nói đoạn mã nguồn mà có nhiều hàm callback lồng nhau sẽ là nỗi ám ảnh của lập trình viên.

Chúng ta cùng quay trở lại hàm getTodos() để lấy dữ liệu từ trên mạng. Giả sử trước khi lấy được dữ liệu với id=3 từ trên mạng, bạn cần phải có dữ liệu với id = 1, và id = 2. Bạn sẽ gọi 2 hàm callback lồng nhau với id = 1 và id = 2. Bạn sẽ sửa lại hàm getTodos() để thực hiện yêu cầu này:

      const getTodos = (resource, callback) => {

        const request = new XMLHttpRequest();

        request.addEventListener("readystatechange", ()=>{

          if(request.readyState === 4 && request.status === 200) {

            const data = JSON.parse(request.responseText);

            callback(undefined, data);

          } else if(request.readyState === 4){

            callback("Không lấy được dữ liệu từ Server!", undefined);

          }

        });

        request.open('GET', resource);

        request.send();

      }

     // gọi hàm getTodos() để lấy dữ liệu, truyền theo hàm callback (err, data) => {}

getTodos( "https://jsonplaceholder.typicode.com/todos/1", (err, data) => {

       if (err) {

         console.log(err);

       } else {

          console.log(data);

          // neu co du lieu id = 1 thi goi tiep getTodos() voi callback (err, data) => {}

          // de xu ly du lieu id = 2

          getTodos( "https://jsonplaceholder.typicode.com/todos/2", (err, data) => {

          if(err) {

            console.log(err);

          } else {

              console.log(data);

             // neu co du lieu id = 2 thi goi tiep getTodos() voi callback (err, data) => {}

          // de xu ly du lieu id = 3

              getTodos( "https://jsonplaceholder.typicode.com/todos/3", (err, data) => {

                if(err) {

                  console.log(err);

                } else {

                  console.log(data);

                }

              });

          }

       });

      }

     });

     // {userId: 1, id: 1, title: 'delectus aut autem', completed: false}

     // {userId: 1, id: 2, title: 'quis ut nam facilis et officia qui', completed: false}

     // {userId: 1, id: 3, title: 'fugiat veniam minus', completed: false}

Ở đoạn mã trên, bạn sẽ thấy là khi nào có được dữ liệu với id = 1, sẽ gọi tiếp hàm callback (err, data) => {}, khi có được dữ liệu với id = 2, lại gọi tiếp hàm callback (err, data) => {}. Chỉ với 3 hàm callback lồng nhau như vậy đã thấy đoạn mã nguồn rất phức tạp, khó hiểu, khó bảo trì.

Để khắc phục tình trạng callback hell, chúng ta sẽ tìm hiểu một giải pháp cho lập trình bất đồng bộ khác có tên là promise.

-----

Cập nhật: 5/4/2022

-----