Hãy tưởng tượng rằng bạn là một ca sĩ hàng đầu và người hâm mộ hỏi ngày và đêm cho đĩa đơn sắp tới của bạn.

Để nhận được một số chi phí, bạn hứa sẽ gửi nó cho họ khi nó được xuất bản. Bạn cung cấp cho người hâm mộ của bạn một danh sách. Họ có thể điền địa chỉ email của mình để khi bài hát có sẵn, tất cả các bên đã đăng ký sẽ ngay lập tức nhận được. Và ngay cả khi có gì đó không ổn, giả sử, một đám cháy trong phòng thu, để bạn không thể xuất bản bài hát, chúng vẫn sẽ được thông báo.

Mọi người đều vui vẻ, đối với bạn, vì mọi người không tập trung vào bạn nữa, còn người hâm mộ vui, vì họ sẽ không bỏ lỡ đĩa đơn.

Đây là một sự tương tự trong cuộc sống thực cho những thứ chúng ta thường có trong lập trình:

  1. Một code sản xuất mã nguồn mà làm một cái gì đó và mất thời gian. Chẳng hạn, một số code tải dữ liệu qua mạng. Đó là một ca sĩ.
  2. Một coce tiêu thụ, người muốn có kết quả của việc sản xuất code khi đã sẵn sàng. Nhiều hàm có thể cần kết quả đó. Đây là những người hâm mộ.
  3. Một lời hứa(promise) là một đối tượng JavaScript đặc biệt liên kết với code sản xuất code và code tiêu thụ với nhau. Xét về sự tương tự của chúng ta: đây là danh sách đăng ký. Code sản xuất, bất cứ lúc nào nó cũng cần để tạo ra kết quả như đã hứa, và lời hứa của cung cấp kết quả đó cho tất cả các code đã đăng ký khi nó sẵn sàng.

1 ví dụ về thực tế và một về code nó không tương tự nhau 100%, bởi vì các lời hứa JavaScript phức tạp hơn một danh sách đăng ký đơn giản: chúng có các tính năng và giới hạn bổ sung. Nhưng nó tốt để chúng ta bắt đầu tìm hiều và sử dụng.

Cú pháp hàm tạo cho một đối tượng lời hứa là:

let promise = new Promise(function(resolve, reject) {
  // executor (the producing code, "singer")
});

Hàm được truyền vào new Promiseđược gọi là hàm thực thi . Khi new Promiseđược tạo, người thực thi(executor) sẽ tự động chạy. Nó chứa code sản xuất mà cuối cùng sẽ tạo ra kết quả. Xét về sự tương tự ở trên: người thực hiện là ca sĩ.

Đối số của nó resolverejectlà các cuộc gọi lại được cung cấp bởi chính JavaScript.

Code của chúng ta chỉ bên trong người thực thi(executor).

Khi người thi hành có được kết quả, sớm hay muộn, không thành vấn đề, nên gọi một trong những cuộc gọi lại sau:

  • resolve(value)– nếu công việc kết thúc thành công, có kết quả value.
  • reject(error)– nếu xảy ra lỗi, errorlà đối tượng lỗi.

Vì vậy, để tóm tắt: người thực thi chạy tự động và cố gắng thực hiện một công việc. Khi kết thúc với nỗ lực, nó gọi resolvenếu nó thành công hoặc rejectnếu có lỗi.

Đối tượngpromise được trả về bởi hàm tạonew Promise có các thuộc tính bên trong này:

  • state– ban đầu "pending", sau đó thay đổi thành "fulfilled"khi resolveđược gọi hoặc "rejected"khi rejectđược gọi.
  • result– ban đầu undefined, sau đó thay đổi thành valuekhi resolve(value)được gọi hoặc errorkhi reject(error)được gọi.

Vì vậy, người thực thi cuối cùng chuyển promiseđến một trong những trạng thái sau:

Sau đó, chúng ta sẽ thấy những người hâm mộ trên thế giới có thể đăng ký và nhận những thay đổi này.

Đây là một ví dụ về một hàm tạo hứa hẹn và một hàm thực thi đơn giản với code sản xuất mã mà mất thời gian (thông qua setTimeout):

let promise = new Promise(function(resolve, reject) {
  // the function is executed automatically when the promise is constructed

  // after 1 second signal that the job is done with the result "done"
  setTimeout(() => resolve("done"), 1000);
});

Chúng ta có thể thấy hai điều bằng cách chạy code ở trên:

  1. Người thực hiện được gọi tự động và ngay lập tức (bởi new Promise).
  2. Người thực hiện nhận được hai đối số: resolvereject. Các hàm này được xác định trước bởi công cụ JavaScript, vì vậy chúng ta không cần tạo chúng. Chúng ta chỉ nên gọi một trong số họ khi sẵn sàng. Sau một giây xử lý trên mạng, gọi toán tử resolve("done")để tạo kết quả. Điều này thay đổi trạng thái của đối tượngpromise:

Đó là một ví dụ về việc hoàn thành công việc thành công, một lời hứa đã hoàn thành.

Và bây giờ là một ví dụ về việc người thực thi từ chối lời hứa với một lỗi:

let promise = new Promise(function(resolve, reject) {
  // after 1 second signal that the job is finished with an error
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

Lệnh gọi reject(...)chuyển đối tượng lời hứa sang trạng thái "rejected":

Tóm tắt như sau, người thực thi nên thực hiện một công việc (thường là một cái gì đó mất thời gian) và sau đó gọi resolvehoặc rejectđể thay đổi trạng thái của đối tượng hứa tương ứng.

Một lời hứa được giải quyết hoặc bị từ chối được gọi là Lời giải quyết, trái ngược với lời hứa ban đầu về thời gian chờ đợi.

Chỉ có thể có một kết quả duy nhất hoặc một lỗi

Chấp hành viên chỉ nên gọi một resolvehoặc một reject. Bất kỳ thay đổi trạng thái là cuối cùng.

Tất cả các cuộc gọi tiếp theo resolverejectđược bỏ qua:

let promise = new Promise(function(resolve, reject) {
  resolve("done");

  reject(new Error("…")); // ignored
  setTimeout(() => resolve("…")); // ignored
});

Ý tưởng là một công việc được thực hiện bởi người thực thi có thể chỉ có một kết quả hoặc một lỗi.

Ngoài ra, resolve/ rejectchỉ mong đợi một đối số (hoặc không có) và sẽ bỏ qua các đối số bổ sung.

Từ chối với các đối tượng Error

Trong trường hợp có sự cố xảy ra, người thực hiện nên gọi reject. Điều đó có thể được thực hiện với bất kỳ loại đối số (giống như resolve). Nhưng nên sử dụng các đối tượng Error(hoặc các đối tượng kế thừa từ Error). Để hiểu lý do cho điều đó xảy ra rõ ràng hơn.

Gọi ngay lập tức resolve/reject

Trong thực tế, một người thi hành thường làm một cái gì đó không đồng bộ và gọi resolve/ rejectsau một thời gian, nhưng nó không phải. Chúng ta cũng có thể gọi resolvehoặc rejectngay lập tức, như thế này:

let promise = new Promise(function(resolve, reject) {
  // not taking our time to do the job
  resolve(123); // immediately give the result: 123
});

Ví dụ, điều này có thể xảy ra khi chúng ta bắt đầu thực hiện một công việc nhưng sau đó thấy rằng mọi thứ đã được hoàn thành và lưu trữ.

Tốt rồi. Chúng ta ngay lập tức có một lời hứa được giải quyết.

Các stateresultlà nội bộ

Các thuộc tính stateresultcủa đối tượng Promise là nội bộ. Chúng ta không thể truy cập trực tiếp vào chúng. Chúng ta có thể sử dụng các phương thức .then/ .catch/ .finallycho điều đó. Chúng được mô tả dưới đây.

1. Người sử dụng: then, catch, finally

Một đối tượng Promise đóng vai trò là một liên kết giữa người thực thi (code sản xuất mã hóa trực tuyến) và các hàm tiêu thụ (các fan hâm mộ), sẽ nhận được kết quả hoặc lỗi. Hàm tiêu thụ có thể được đăng ký (đăng ký mua) sử dụng các phương thức .then, .catch.finally.

1.1. then

Điều quan trọng nhất, cơ bản là .then.

Cú pháp là:

promise.then(
  function(result) { /* handle a successful result */ },
  function(error) { /* handle an error */ }
);

Đối số đầu tiên .thenlà một hàm chạy khi lời hứa được giải quyết và nhận kết quả.

Đối số thứ hai .thenlà một hàm chạy khi lời hứa bị từ chối và nhận được lỗi.

Chẳng hạn, đây là một phản ứng đối với một lời hứa được giải quyết thành công:

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

// resolve runs the first function in .then
promise.then(
  result => alert(result), // shows "done!" after 1 second
  error => alert(error) // doesn't run
);

Hàm đầu tiên được thực thi.

Và trong trường hợp từ chối, cái thứ hai:

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// reject runs the second function in .then
promise.then(
  result => alert(result), // doesn't run
  error => alert(error) // shows "Error: Whoops!" after 1 second
);

Nếu chúng ta chỉ quan tâm đến việc hoàn thành thành công, thì chúng ta chỉ có thể cung cấp một đối số hàm để .then:

let promise = new Promise(resolve => {
  setTimeout(() => resolve("done!"), 1000);
});

promise.then(alert); // shows "done!" after 1 second

1.2. catch

Nếu chúng ta chỉ quan tâm đến lỗi, thì chúng ta có thể sử dụng nulllàm đối số đầu tiên : .then(null, errorHandlingFunction). Hoặc chúng ta có thể sử dụng .catch(errorHandlingFunction), hoàn toàn giống nhau:

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// .catch(f) is the same as promise.then(null, f)
promise.catch(alert); // shows "Error: Whoops!" after 1 second

Gọi .catch(f)này là một sự tương tự hoàn toàn .then(null, f), nó chỉ là một tốc ký(cách viết gọn).

1.3. finally

Giống như có mộtfinally thường xuyên trongtry {...} catch {...}, có những lời hứafinally.

Cuộc gọi .finally(f)tương tự như .then(f, f)trong ngữ cảnh này f có ý nghĩa luôn luôn chạy khi lời hứa được giải quyết: có thể giải quyết hoặc từ chối.

finally là một trình xử lý tốt để thực hiện dọn dẹp, ví dụ như dừng việc tải của chúng ta, vì chúng không còn cần thiết nữa, bất kể kết quả là gì.

Như thế này:

new Promise((resolve, reject) => {
  /* do something that takes time, and then call resolve/reject */
})
  // runs when the promise is settled, doesn't matter successfully or not
  .finally(() => stop loading indicator)
  .then(result => show result, err => show error)

Nó không chính xác là một bí danh then(f,f)mặc dù nó có một số khác biệt quan trọng:

  1. Một trình xử lýfinally không có đối số. Trong finallychúng ta không biết liệu lời hứa có thành công hay không. Không sao đâu, vì nhiệm vụ của chúng ta thường là thực hiện các thủ tục hoàn thiện chung chung.
  2. Một trình xử lýfinally chuyển các kết quả và lỗi cho trình xử lý tiếp theo. Ví dụ, ở đây kết quả được đi qua finallyđể then:
new Promise((resolve, reject) => {
  /* do something that takes time, and then call resolve/reject */
})
  // runs when the promise is settled, doesn't matter successfully or not
  .finally(() => stop loading indicator)
  .then(result => show result, err => show error)

Và ở đây có một lỗi trong lời hứa, đi ngang qua finallyđể catch:

new Promise((resolve, reject) => {
  throw new Error("error");
})
  .finally(() => alert("Promise ready"))
  .catch(err => alert(err));  // <-- .catch handles the error object
  1. Điều đó rất thuận tiện, vì finallykhông có nghĩa là xử lý một kết quả hứa hẹn. Vì vậy, đi qua nó. Chúng ta sẽ nói nhiều hơn về chuỗi hứa hẹn và kết quả giữa những người xử lý trong chương tiếp theo.
  2. Cuối cùng, nhưng không kém phần quan trọng, .finally(f)là một cú pháp thuận tiện hơn .then(f, f): không cần phải sao chép hàm f.

Xử lý hứa hẹn chạy ngay lập tức

Nếu một lời hứa đang chờ xử lý.then/catch/finally , người xử lý chờ đợi nó. Mặt khác, nếu một lời hứa đã được giải quyết, họ sẽ thực hiện ngay lập tức:

// the promise becomes resolved immediately upon creation
let promise = new Promise(resolve => resolve("done!"));

promise.then(alert); // done! (shows up right now)

Lưu ý rằng điều này là khác nhau và mạnh mẽ hơn so với kịch bản danh sách đăng ký trong cuộc sống thực tế của nhà cung cấp. Nếu ca sĩ đã phát hành bài hát của họ và sau đó một người đăng ký vào danh sách đăng ký, họ có thể sẽ không nhận được bài hát đó. Đăng ký trong cuộc sống thực phải được thực hiện trước sự kiện này.

Lời hứa sẽ linh hoạt hơn. Chúng ta có thể thêm trình xử lý bất cứ lúc nào: nếu kết quả đã có, trình xử lý của chúng ta sẽ nhận ngay lập tức.

Tiếp theo, hãy xem các ví dụ thực tế hơn về cách các lời hứa có thể giúp chúng ta viết code không đồng bộ.

2. Ví dụ: loadScript

Chúng ta đã có hàm loadScript tải một tập lệnh từ chương trước.

Đây là biến thể dựa trên cuộc gọi lại, chỉ để nhắc nhở chúng ta về nó:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

Hãy viết lại bằng Promise.

Hàm mới loadScriptsẽ không yêu cầu gọi lại. Thay vào đó, nó sẽ tạo và trả về một đối tượng Promise sẽ giải quyết khi quá trình tải hoàn tất. Code bên ngoài có thể thêm các trình xử lý (Hàm đăng ký) cho nó bằng cách sử dụng .then:

function loadScript(src) {
  return new Promise(function(resolve, reject) {
    let script = document.createElement('script');
    script.src = src;

    script.onload = () => resolve(script);
    script.onerror = () => reject(new Error(`Script load error for ${src}`));

    document.head.append(script);
  });
}

Sử dụng:

let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");

promise.then(
  script => alert(`${script.src} is loaded!`),
  error => alert(`Error: ${error.message}`)
);

promise.then(script => alert('Another handler...'));

Chúng ta có thể thấy ngay một vài lợi ích so với việc dựa trên cuộc gọi lại:

Hứa(Promises)Gọi lại(Callbacks)
Lời hứa cho phép chúng ta làm mọi thứ theo thứ tự tự nhiên. Đầu tiên, chúng ta chạy loadScript(script).thenchúng tôi viết những gì cần làm với kết quả.Chúng ta phải có một callbackchức năng theo ý của chúng tôi khi gọi loadScript(script, callback). Nói cách khác, chúng ta phải biết phải làm gì với kết quả trước khi loadScript được gọi.
Chúng ta có thể gọi .thenmột lời hứa nhiều lần như chúng ta muốn. Mỗi lần, chúng tôi lại thêm một người hâm mộ mới, một hàm đăng ký mới, vào danh sách đăng ký. Thông tin thêm về điều này trong chương tiếp theo: Một chuỗi các Hứa hẹn .Chỉ có thể có một cuộc gọi lại.

Vì vậy, lời hứa(Promise) cho chúng ta làm lưu lượng code tốt hơn và linh hoạt. Nhưng còn nhiều hơn thế. Chúng ta sẽ thấy điều đó trong các chương tiếp theo.

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!