Promise API Trong JavaScript

Có lẽ tiêu đề bài viết đã phần nào lột tả được hết nội dung chính của bài viết này, đó chính là các phương thức trong lớp Promise. Không để các bạn chờ đợi lâu, chúng ta cùng nhau tìm hiểu về Promise API thông qua 6 phương thức tĩnh (Static).

1. Promises.all

Giả sử chúng ta muốn nhiều promise được thực thi song song và xử lí khi tất cả chúng đã sẵn sàng. Ví dụ: tải xuống một số URL song song và xử lý nội dung khi chúng hoàn tất. Để làm được điều đó thì Promises.all là một cái tên không thể không nhắc đến. Cú pháp của Promise.all là:

let promise = Promise.all([...promises...]);

Promise.all nhận một mảng các promise và nó trả về một promise mới. Promise mới được giải quyết khi tất cả các Promise được liệt kê trước đó đã được giải quyết và kết quả của các promise trước đó trở thành kết quả của chính promise mới.

Ví dụ: Promise.all bên dưới settles 3 giây và sau đó nó cho ra kết quả là một mảng [1, 2, 3]:

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(alert); // 1,2,3 when promises are ready: each promise contributes an array member

Xin lưu ý rằng thứ tự của các kết quả giống phải như trong các promise gốc của nó. Mặc dù promises đầu tiên mất nhiều thời gian nhất để giải quyết, nhưng nó vẫn là promise đầu tiên trong mảng kết quả. Một thủ thuật phổ biến là ánh xạ một mảng dữ liệu thành một mảng các promise, rồi gói nó vào Promise.all. Ví dụ: nếu chúng ta có một loạt các URL, chúng ta có thể tìm nạp tất cả chúng như sau:

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://api.github.com/users/jeresig'
];

// map every url to the promise of the fetch
let requests = urls.map(url => fetch(url));

// Promise.all waits until all jobs are resolved
Promise.all(requests)
  .then(responses => responses.forEach(
    response => alert(`${response.url}: ${response.status}`)
  ));

Lưu ý:

  • Nếu bất kỳ promise nào bị từ chối, promise được trả về Promise.all sẽ bị từ chối ngay lập tức với chính lỗi đó. Ví dụ:
Promise.all([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Whoops!

promises thứ hai bị từ chối trong hai giây. Điều đó dẫn đến việc từ chối ngay lập tức Promise.all, vì vậy sẽ .catch sẽ thực thi: lỗi từ chối trở thành kết quả của toàn bộ Promise.all.

  • Promise.all(iterable) cho phép các giá trị non-promise "thông thường" trong iterable

Ví dụ đoạn code dưới đây cho kết quả là [1, 2, 3]:

Promise.all([
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000)
  }),
  2,
  3
]).then(alert); // 1, 2, 3

2. Promises.allSettled

Promise.allSettled chỉ cần đợi cho tất cả các promise giải quyết, bất kể kết quả. Mảng kết quả có:

  • {status:"fulfilled", value:result} để có phản hồi thành công,
  • {status:"rejected", reason:error} cho các lỗi.

Ví dụ: chúng ta muốn tìm nạp thông tin về nhiều người dùng. Ngay cả khi một yêu cầu không thành công, chúng ta cũng có thể vẫn quan tâm đến những yêu cầu khác. Lúc này bạn nên sử dụng  Promise.allSettled:

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://no-such-url'
];

Promise.allSettled(urls.map(url => fetch(url)))
  .then(results => { // (*)
    results.forEach((result, num) => {
      if (result.status == "fulfilled") {
        alert(`${urls[num]}: ${result.value.status}`);
      }
      if (result.status == "rejected") {
        alert(`${urls[num]}: ${result.reason}`);
      }
    });
  });

Dòng resultsở (*)trên sẽ là:

[
  {status: 'fulfilled', value: ...response...},
  {status: 'fulfilled', value: ...response...},
  {status: 'rejected', reason: ...error object...}
]

Vì vậy, đối với mỗi promise, chúng ta có thể nhận được trạng thái của nó và value/error.

Polyfill

Nếu trình duyệt không hỗ trợ Promise.allSettled, bạn có thể dùng polyfill như sau:

if (!Promise.allSettled) {
  const rejectHandler = reason => ({ status: 'rejected', reason });

  const resolveHandler = value => ({ status: 'fulfilled', value });

  Promise.allSettled = function (promises) {
    const convertedPromises = promises.map(p => Promise.resolve(p).then(resolveHandler, rejectHandler));
    return Promise.all(convertedPromises);
  };
}

Giải thích 1 chút nè:

Trong đoạn code trên, promise.map nhận các giá trị đầu vào, biến chúng thành các promise (đề phòng trường hợp một promise không được chuyển qua) với p => Promise.resolve(p), và sau đó thêm .then cho mọi giá trị. Nếu thành công trình xử lý sẽ biến value thành {status:'fulfilled', value}và lỗi reason thành {status:'rejected', reason}. Đó chính xác là định dạng của Promise.allSettled.

Bây giờ chúng ta có thể sử dụng Promise.allSettled để nhận kết quả của tất cả các promise.

3. Promise.race

Tương tự như Promise.all, nhưng nó chỉ đợi promise đã giải quyết đầu tiên và nhận kết quả (hoặc lỗi) của nó. Cú pháp là:

let promise = Promise.race(iterable);

Ví dụ, ở đây kết quả sẽ là 1

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

promise đầu tiên ở đây là nhanh nhất, vì vậy nó đã trở thành kết quả. Sau khi promise đầu tiên "win", tất cả các kết quả / lỗi khác sẽ được bỏ qua.

4. Promise.any

Tương tự như Promise.race, nhưng chỉ đợi promise đầu tiên được thực thi và nhận được kết quả của nó. Nếu tất cả các promise đã cho đều bị từ chối, thì promise trả về sẽ bị từ chối với AggregateError- một đối tượng đặc biệt lưu trữ tất cả các lỗi của promise trong thuộc tính của nó errors.

Cú pháp:

let promise = Promise.any(iterable);

Ví dụ dưới đây cho kết quả là 1:

Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

promise đầu tiên ở đây là nhanh nhất, nhưng nó đã bị từ chối, vì vậy promise thứ hai trở thành kết quả. Sau khi promise đầu tiên "win", tất cả các kết quả tiếp theo đều bị bỏ qua.

Còn đây là một ví dụ khi tất cả các promise đều thất bại:

Promise.any([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ouch!")), 1000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error("Error!")), 2000))
]).catch(error => {
  console.log(error.constructor.name); // AggregateError
  console.log(error.errors[0]); // Error: Ouch!
  console.log(error.errors[1]); // Error: Error
});

Như bạn có thể thấy, các lỗi của promise đều có sẵn trong thuộc tính errors của đối tượng AggregateError.

5. Promise.resolve

Promise.resolve(value) tạo ra một promise đã được giải quyết với kết quả value như sau:

let promise = new Promise(resolve => resolve(value));

Ví dụ: loadCached hàm bên dưới tìm nạp một URL và ghi nhớ (lưu vào bộ nhớ đệm) nội dung của nó. Đối với các calls trong tương lai với cùng một URL, nó ngay lập tức lấy nội dung trước đó từ bộ nhớ cache, nhưng sử dụng Promise.resolve để thực hiện promise đó, vì vậy giá trị trả về luôn là một promise:

let cache = new Map();

function loadCached(url) {
  if (cache.has(url)) {
    return Promise.resolve(cache.get(url)); // (*)
  }

  return fetch(url)
    .then(response => response.text())
    .then(text => {
      cache.set(url,text);
      return text;
    });
}

6. Promise.reject

Promise.reject(error) được tạo ra một promise bị từ chối với error giống như:

let promise = new Promise((resolve, reject) => reject(error));

Trong thực tế, các phương thức Promise.resolve và Promise.reject hiếm khi được sử dụng, bởi vì async/await.Tuy nhiên bài viết này vẫn đề cập đến chúng ở đây để hoàn thiện và cho những bạn không thể sử dụng async/await vì lý do nào đó.

Bài viết thứ 5 trong series tìm hiểu về promise, async/await đã khép lại. Hẹn gặp lại các bạn trong bài 6 với chủ để Promisification

Trần Quang Hào
SUNTECH VIỆT NAM   Đăng ký để nhận thông báo mới nhất