Chúng ta hãy quay trở lại vấn đề được đề cập trong chương Giới thiệu: cuộc gọi lại(Callback): chúng ta có một chuỗi các nhiệm vụ không đồng bộ được thực hiện lần lượt – ví dụ, tải tập lệnh. Làm thế nào chúng ta có thể code nó tốt hơn?

Hứa hẹn(Promises) cung cấp một vài công thức để làm điều đó.

Trong chương này, chúng ta đề cập đến một chuỗi các hứa hẹn(promise).

Nó trông như thế này:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

  alert(result); // 1
  return result * 2;

}).then(function(result) { // (***)

  alert(result); // 2
  return result * 2;

}).then(function(result) {

  alert(result); // 4
  return result * 2;

});

Ý tưởng là kết quả được truyền qua chuỗi các xử lý.then.

Ở đây dòng chảy là:

  1. Lời hứa(Promises) ban đầu giải quyết trong 1 giây (*),
  2. Sau đó, .thenxử lý được gọi (**).
  3. Giá trị mà nó trả về được chuyển cho bộ xử lý.then tiếp theo(***)
  4. …và như thế.

Khi kết quả được truyền dọc theo chuỗi trình xử lý, chúng ta có thể thấy một chuỗi các alertcuộc gọi: 124.

Toàn bộ hoạt động, bởi vì một cuộc gọi để promise.thentrả lại một lời hứa, để chúng ta có thể gọi tiếp theo .thentrên đó.

Khi một trình xử lý trả về một giá trị, nó trở thành kết quả của lời hứa đó, vì vậy tiếp theo .thenđược gọi với nó.

Một lỗi newbie cổ điển: về mặt kỹ thuật, chúng ta cũng có thể thêm nhiều .thenvào một lời hứa. Đây không phải là chuỗi.

Ví dụ:

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000);
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

Những gì chúng ta đã làm ở đây chỉ là một số xử lý cho một lời hứa. Nó không truyền kết quả cho nhau; thay vào đó nó xử lý nó một cách độc lập.

Đây là hình ảnh (so sánh nó với chuỗi ở trên):

Tất cả .thentrên cùng một lời hứa nhận được cùng một kết quả – kết quả của lời hứa đó. Vì vậy, trong code trên tất cả alerthiển thị như nhau : 1.

Trong thực tế, chúng ta hiếm khi cần nhiều người xử lý cho một lời hứa. Chuỗi này được sử dụng thường xuyên hơn nhiều.

1. Trả lại lời hứa

Một trình xử lý, được sử dụng trong .then(handler)có thể tạo và trả lại một lời hứa.

Trong trường hợp đó, người xử lý tiếp tục đợi cho đến khi nó ổn định, và sau đó nhận được kết quả.

Ví dụ:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

new Promise(function(resolve, reject) {

  setTimeout(() => resolve(1), 1000);

}).then(function(result) {

  alert(result); // 1

  return new Promise((resolve, reject) => { // (*)
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) { // (**)

  alert(result); // 2

  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(result * 2), 1000);
  });

}).then(function(result) {

  alert(result); // 4

});

Ở đây đầu tiên .thenhiển thị 1và trở lại new Promise(…)trong dòng (*). Sau một giây, nó được giải quyết và kết quả (đối số của nóresolve, ở đây result * 2) được chuyển cho người xử lý .then thứ hai. Trình xử lý đó là trong dòng (**), nó hiển thị 2và làm điều tương tự.

Vì vậy, đầu ra giống như trong ví dụ trước: 1 → 2 → 4, nhưng bây giờ có độ trễ 1 giây giữa các cuộc gọi alert.

Trả lại lời hứa cho phép chúng ta xây dựng chuỗi hành động không đồng bộ.

2. Ví dụ: loadScript

Chúng ta hãy sử dụng tính năng này với phần được quảng cáo loadScript, được xác định trong chương trước , để tải từng tập lệnh theo thứ tự:

loadScript("/article/promise-chaining/one.js")
  .then(function(script) {
    return loadScript("/article/promise-chaining/two.js");
  })
  .then(function(script) {
    return loadScript("/article/promise-chaining/three.js");
  })
  .then(function(script) {
    // use functions declared in scripts
    // to show that they indeed loaded
    one();
    two();
    three();
  });

Code này có thể được thực hiện ngắn hơn một chút với các hàm mũi tên:

loadScript("/article/promise-chaining/one.js")
  .then(script => loadScript("/article/promise-chaining/two.js"))
  .then(script => loadScript("/article/promise-chaining/three.js"))
  .then(script => {
    // scripts are loaded, we can use functions declared there
    one();
    two();
    three();
  });

Ở đây, mỗi lệnh gọi loadScript trả về một lời hứa và cuộc gọi tiếp theo sẽ chạy.then khi nó được giải quyết. Sau đó, nó bắt đầu tải tập lệnh tiếp theo. Vì vậy, các kịch bản được tải lần lượt.

Chúng ta có thể thêm nhiều hành động không đồng bộ vào chuỗi. Xin lưu ý rằng code vẫn là không lông nhau – nó phát triển xuống, không phải ở bên phải. Không có dấu hiệu nào về kim tự tháp huỷ diệt.

Về mặt kỹ thuật, chúng ta có thể thêm .thentrực tiếp vào từng cái loadScript, như thế này:

loadScript("/article/promise-chaining/one.js").then(script1 => {
  loadScript("/article/promise-chaining/two.js").then(script2 => {
    loadScript("/article/promise-chaining/three.js").then(script3 => {
      // this function has access to variables script1, script2 and script3
      one();
      two();
      three();
    });
  });
});

Code này thực hiện tương tự: tải 3 tập lệnh theo trình tự. Nhưng nó đã phát triển thành đúng hướng. Vì vậy, chúng ta có vấn đề tương tự như với các cuộc gọi lại.

Những người bắt đầu sử dụng lời hứa đôi khi không biết về chuỗi các lời hứa, vì vậy họ viết nó theo cách này. Nói chung, nó được ưa thích.

Đôi khi bạn có thể viết trực tiếp.then, bởi vì hàm lồng nhau có quyền truy cập vào phạm vi bên ngoài. Trong ví dụ trên gọi lại lồng nhau nhất có quyền truy cập vào tất cả các biến script1, script2, script3. Nhưng đó là một ngoại lệ chứ không phải là một quy tắc.

Đồ đạc

Nói một cách chính xác, một trình xử lý có thể trả về không chính xác như một lời hứa, nhưng một đối tượng được gọi là có thể có khả năng có thể sử dụng được – đó là một đối tượng tùy ý có một phương thức .then. Nó sẽ được đối xử giống như một lời hứa.

Ý tưởng là các thư viện của bên thứ 3 có thể tự triển khai các đối tượng có khả năng tương thích với hứa hẹn. Họ có thể có một bộ phương thức mở rộng, nhưng cũng tương thích với các lời hứa có sẵn gốc của javascript, bởi vì chúng thực hiện .then.

Đây là một ví dụ về một đối tượng có thể đọc được:

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve); // function() { native code }
    // resolve with this.num*2 after the 1 second
    setTimeout(() => resolve(this.num * 2), 1000); // (**)
  }
}

new Promise(resolve => resolve(1))
  .then(result => {
    return new Thenable(result); // (*)
  })
  .then(alert); // shows 2 after 1000ms

JavaScript kiểm tra đối tượng được trả về bởi trình xử lý.then trong dòng (*): nếu nó có một phương thức có thể gọi được then, thì nó gọi phương thức đó cung cấp các hàm riêng resolve, rejectnhư các đối số (tương tự như một hàm thực thi) và đợi cho đến khi một trong số chúng được gọi. Trong ví dụ trên resolve(2)được gọi sau 1 giây (**). Sau đó, kết quả được truyền tiếp xuống chuỗi.

Tính năng này cho phép chúng ta tích hợp các đối tượng tùy chỉnh với chuỗi hứa hẹn mà không phải kế thừa từ đó Promise.

3. Ví dụ lớn hơn: lấy(fetch)

Trong lập trình frontend, lời hứa thường được sử dụng cho các yêu cầu mạng. Vì vậy, hãy xem một ví dụ mở rộng về điều đó.

Chúng tôi sẽ sử dụng phương thức fetch để tải thông tin về người dùng từ máy chủ ở xa. Nó có rất nhiều tham số tùy chọn được trình bày trong các chương tiếp theo, nhưng cú pháp cơ bản khá đơn giản:

let promise = fetch(url);

Điều này làm cho một yêu cầu mạng đến urlvà trả lại một lời hứa. Lời hứa sẽ giải quyết với một responseđối tượng khi máy chủ từ xa phản hồi bằng các tiêu đề, nhưng trước khi phản hồi đầy đủ được tải xuống .

Để đọc phản hồi đầy đủ, chúng ta nên gọi phương thức response.text(): nó trả về một lời hứa sẽ giải quyết khi toàn bộ văn bản được tải xuống từ máy chủ từ xa, với kết quả là văn bản đó.

Code dưới đây đưa ra yêu cầu user.jsonvà tải văn bản của nó từ máy chủ:

fetch('/article/promise-chaining/user.json')
  // .then below runs when the remote server responds
  .then(function(response) {
    // response.text() returns a new promise that resolves with the full response text
    // when it loads
    return response.text();
  })
  .then(function(text) {
    // ...and here's the content of the remote file
    alert(text); // {"name": "iliakan", "isAdmin": true}
  });

Đối tượngresponse được trả về fetchcũng bao gồm phương thức response.json()đọc dữ liệu từ xa và phân tích cú pháp dưới dạng JSON. Trong trường hợp của chúng ta điều đó thậm chí còn thuận tiện hơn, vì vậy hãy chuyển sang nó.

Chúng ta cũng sẽ sử dụng các hàm mũi tên cho ngắn gọn:

// same as above, but response.json() parses the remote content as JSON
fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => alert(user.name)); // iliakan, got user name

Bây giờ hãy làm gì đó với người dùng đã tải.

Chẳng hạn, chúng ta có thể thực hiện thêm một yêu cầu cho GitHub, tải hồ sơ người dùng và hiển thị hình đại diện:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

// Make a request for user.json
fetch('/article/promise-chaining/user.json')
  // Load it as json
  .then(response => response.json())
  // Make a request to GitHub
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  // Load the response as json
  .then(response => response.json())
  // Show the avatar image (githubUser.avatar_url) for 3 seconds (maybe animate it)
  .then(githubUser => {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => img.remove(), 3000); // (*)
  });

Code hoạt động; xem ý kiến ​​về các chi tiết. Tuy nhiên, có một vấn đề tiềm ẩn trong đó, một lỗi điển hình cho những người bắt đầu sử dụng lời hứa.

Nhìn vào dòng (*): làm thế nào chúng ta có thể làm một cái gì đó sau khi hình đại diện đã hiển thị xong và bị xóa? Chẳng hạn, chúng ta muốn hiển thị một biểu mẫu để chỉnh sửa người dùng đó hoặc một cái gì đó khác. Đến bây giờ, không còn cách nào khác.

Để làm cho chuỗi lời hứa có thể mở rộng, chúng ta cần trả lại một lời hứa sẽ giải quyết khi hình đại diện kết thúc hiển thị.

Như thế này:

fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise(function(resolve, reject) { // (*)
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser); // (**)
    }, 3000);
  }))
  // triggers after 3 seconds
  .then(githubUser => alert(`Finished showing ${githubUser.name}`));

Đó là, trình xử lý.then trong dòng (*)bây giờ trở lại new Promise, điều đó chỉ được giải quyết sau khi gọi resolve(githubUser)vào setTimeout (**). Tiếp theo .thentrong chuỗi lời hứa sẽ chờ đợi điều đó.

Như một thực tiễn tốt, một hành động không đồng bộ sẽ luôn luôn trả lại một lời hứa. Điều đó làm cho nó có thể lập kế hoạch hành động sau nó; ngay cả khi chúng ta không có kế hoạch mở rộng chuỗi ngay bây giờ, chúng ta có thể cần nó sau này.

Cuối cùng, chúng ta có thể chia code thành các hàm có thể sử dụng lại:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

function loadJson(url) {
  return fetch(url)
    .then(response => response.json());
}

function loadGithubUser(name) {
  return fetch(`https://api.github.com/users/${name}`)
    .then(response => response.json());
}

function showAvatar(githubUser) {
  return new Promise(function(resolve, reject) {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  });
}

// Use them:
loadJson('/article/promise-chaining/user.json')
  .then(user => loadGithubUser(user.name))
  .then(showAvatar)
  .then(githubUser => alert(`Finished showing ${githubUser.name}`));
  // ...

4. Tóm lược

Nếu một trình xử lý .then(hoặc catch/finally, không quan trọng) trả lại một lời hứa, phần còn lại của chuỗi sẽ đợi cho đến khi nó ổn định. Khi có, kết quả của nó (hoặc lỗi) được truyền thêm.

Đây là một bức tranh đầy đủ:

Full series tự học Javascript từ cơ bản tới nâng cao tại đây nha.

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!