Web nâng cao (9) - JavaScript cho React (8) - Promise

Bài trước: Web nâng cao (8) - JavaScript cho React (7) - Bất đồng bộ bằng callback

-----

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

Promise là một cơ chế (hay một kĩ thuật lập trình) của JavaScript, nó giúp bạn lập trình bất đồng bộ mà không rơi vào tình trạng callback hell.

Vì promise cũng là một kĩ thuật lập trình bất đồng bộ, nên để dễ hiểu về promise, bạn cần nhìn lại một chút về lập trình bất đồng bộ bằng callback. Trong lập trình bất đồng bộ bằng kĩ thuật callback, bạn sẽ định nghĩa một hàm xử lý nghiệp vụ bất kì (ví dụ lấy dữ liệu từ trên mạng về), bạn sẽ truyền vào hàm xử lý nghiệp vụ một hàm callback và sẽ gọi hàm callback này ở thời điểm thích hợp, ứng với việc lấy dữ liệu thành công hay thất bại; khi gọi hàm xử lý nghiệp vụ, bạn sẽ viết thêm phần xử lý cho giá trị trả về của hàm callback ứng với trường hợp thành công và thất bại.

Để hiểu về kĩ thuật lập trình bất đồng bộ bằng promise, bạn cũng sẽ tiếp cận theo cách sau:

– Hiểu được promise là gì, nó chạy như thế nào

– Định nghĩa hàm xử lý một nghiệp vụ nào đó có dùng tới promise

– Gọi hàm xử lý nghiệp vụ (vừa định nghĩa ở bước trên), trong đó sẽ định nghĩa hàm callback để sử dụng dữ liệu do promise trả về

Quan sát ví dụ để hiểu kĩ thuật lập trình promise

Promise nghĩa là một “lời hứa”, do vậy sẽ có quá trình tạo ra lời hứa, quá trình thực hiện lời hứa (có thể thực hiện được hoặc không).

JavaScript hiện thực ý tưởng “lời hứa” vào trong đối tượng promise. Vì là đối tượng, nên promise sẽ có các thuộc tính và phương thức cần thiết để nó có thể hoạt động được.

Lập trình bất đồng bộ bằng promise thực chất là việc nhúng (hay gắn) một đối tượng promise vào một hàm xử lý nghiệp vụ bất kì. Khi đã được nhúng promise, chúng ta có thể định nghĩa hàm xử lý nghiệp vụ, gọi hàm xử lý nghiệp vụ dựa trên cơ chế hoạt động của promise, giúp cho việc xử lý nghiệp vụ được thực thi theo cơ chế bất đồng bộ.

Ví dụ sau sẽ gắn đối tượng promise vào hàm layDuLieu():

const layDuLieu = () => {

        return new Promise( (resolve, reject) => {

 

        });

      };

Ở đoạn mã trên, bạn đã khởi tạo một đối tượng promise (dùng từ khóa new), thực chất promise cũng là một hàm (function), rồi gán hàm promise vào hàm layDuLieu().

Một promise, hay một hàm có nhúng promise khi mới được tạo ra (mới khai báo) sẽ ở trạng thái chờ (pending). Ví dụ:

const layDuLieu = () => {

        return new Promise( (resolve, reject) => {

 

        });

      };

      console.log(layDuLieu());

      // Promise {<pending>}

Khi khởi tạo đối tượng promise, bạn sẽ truyền vào cho nó một hàm callback gồm 2 tham số (resolve, reject) => {}; trong hàm callback này bạn sẽ viết mã nguồn xử lý nghiệp vụ.

Bản thân tham số resolve và reject lại là hai hàm, dùng để xử lý cho hai tình huống: “lời hứa thực hiện thành công” (resolve) và “không thực hiện được lời hứa” (reject). Vì vậy, về bản chất kĩ thuật promise cũng vẫn dựa trên kĩ thuật callback, chỉ khác nhau ở cách thức thực hiện.

Hàm resolve sẽ được gọi khi “lời hứa thực hiện thành công” hay nghiệp vụ xử lý thành công. Hàm reject sẽ được gọi khi “lời hứa không thực hiện được” hay nghiệp vụ xử lý thất bại.

Khi gọi hàm xử lý nghiệp vụ, kết quả trả về của promise sẽ được gửi tới phương thức .then() hoặc .catch(). Bạn sẽ viết các hàm để xử lý tùy theo kết quả trả về của promise.

Ví dụ sau là trường hợp xử lý nghiệp vụ thành công (lấy được dữ liệu), khi đó hàm resolve() được gọi để trả về dữ liệu lấy được. Khi gọi hàm layDuLieu(), trong tham số đầu tiên của phương thức then(), bạn sẽ viết hàm callback để xử lý dữ liệu nhận được, tham số của hàm callback (ví dụ data) sẽ chứa dữ liệu do hàm resolve() trả về:

const layDuLieu = () => {

        return new Promise( (resolve, reject) => {

          // các xử lý để lấy dữ liệu

          resolve("Du lieu");

        });

      };

      layDuLieu().then( (data) => {

        console.log(data);

      });

Ví dụ sau là trường hợp xử lý nghiệp vụ thất bại (không lấy được dữ liệu), khi đó hàm reject() được gọi để trả về thông báo lỗi. Khi gọi hàm layDuLieu(), trong tham số thứ 2 của phương thức then(), bạn sẽ viết hàm callback để xử lý lỗi, tham số của hàm callback (ví dụ err) sẽ chứa thông báo lỗi do hàm reject() trả về:

const layDuLieu = () => {

        return new Promise( (resolve, reject) => {

          // các xử lý để lấy dữ liệu

          reject("khong lay duoc du lieu");

        });

      };

      layDuLieu().then( (data) => {

        console.log(data);

      }, (err) => {

        console.log(err);

      });

Do các hàm callback trong phương thức then() chỉ có một tham số nên bạn có thể bỏ với dấu ngoặc đơn cho gọn:

layDuLieu().then(data => {

        console.log(data);

      }, err => {

        console.log(err);

      });

Bạn cũng có thể đưa hàm callback thứ 2 của phương thức then() vào phương thức catch() như sau:

layDuLieu().then(data => {

        console.log(data);

      })

      .catch(err => {

        console.log(err);

      });

Áp dụng promise vào thực tế

Tới đây, bạn đã hiểu kĩ thuật lập trình bất đồng bộ bằng promise, giờ bạn sẽ áp dụng promise vào một ví dụ thực tế.

Giả sử bạn cần viết một hàm, có tên là getTodos(), để lấy dữ liệu từ một server, có URL là https://jsonplaceholder.typicode.com/todos/1 :

// định nghĩa hàm getTodos, có nhúng đối tượng promise

      const getTodos = (resource) => {

        return new Promise( (resolve, reject) => {

          const request = new XMLHttpRequest();

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

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

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

              resolve(data);

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

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

            }

          });

          request.open('GET', resource);

          request.send();

       });

    };

 

    // gọi để thực thi hàm getTodos(),

    // sử dụng cơ chế trả dữ liệu về của promise để xử lý

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

      console.log(data);

    })

    .catch( (err) => {

      console.log(err);

    });

Bạn có thể thay đổi URL của server để hàm getTodos() không nhận được dữ liệu: ví dụ https://jsonplaceholder.typicode.com/todosabc/1.

Chuỗi các promise (chaining promises)

Như đã trình bày, promise sẽ giúp bạn tránh được tình trạng callback hell (quá nhiều hàm callback lồng nhau). Làm được điều này là do sau khi phương thức then() thực thi xong, bạn có thể trả về một promise khác, do vậy có thể gọi tiếp các phương thức then(). Tính chất này được gọi là chuỗi các promise hay chaining promises.

Quay trở lại ví dụ đã thực hiện ở phần callback hell: dùng hàm getTodos() để lấy dữ liệu từ server. Giả sử trước khi lấy được dữ liệu với id=3, bạn cần phải có dữ liệu với id = 1, và id = 2. Như vậy sẽ có tình trạng 3 hàm callback lồng nhau. Xem đoạn mã nguồn minh họa.

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

       if (err) {

         console.log(err);

       } else {

          console.log(data);

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

          if(err) {

            console.log(err);

          } else {

              console.log(data);

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

                if(err) {

                  console.log(err);

                } else {

                  console.log(data);

                }

              });

          }

       });

      }

     });

 

Với chaining promises, bạn có thể viết lại đoạn mã lấy dữ liệu như sau:

    <script>

      // định nghĩa hàm getTodos, có nhúng đối tượng promise

      const getTodos = (resource) => {

        return new Promise( (resolve, reject) => {

          const request = new XMLHttpRequest();

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

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

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

              resolve(data);

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

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

            }

          });

          request.open('GET', resource);

          request.send();

       });

    };

 

    // gọi để thực thi hàm getTodos(),

    // sử dụng cơ chế trả dữ liệu về của promise để xử lý

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

      console.log("promise 1: ", data);

      return getTodos("https://jsonplaceholder.typicode.com/todos/2");

    }).then( (data) => {

      console.log("promise 2: ", data);

      return getTodos("https://jsonplaceholder.typicode.com/todos/3");

    }).then( (data) => {

      console.log("promise 3:", data);

    })

    .catch( (err) => {

      console.log(err);

    });

    </script>

Lưu ý, bất cứ đối tượng promise nào trả về lỗi thì lỗi đó sẽ được chuyển tới phương thức .catch() để xử lý, và ngưng quá trình xử lý các promise tiếp theo.

-----