5个核心知识点,让你彻底掌握Promise

作者: jie 分类: Promise 发布时间: 2023-08-14 13:59

首先来看一下 Promise 中最核心的 5 个知识点:

  1. 概念:Promise 是 JS 中用于处理异步操作的一种对象。
  2. 状态:Promise 对象有三种状态:pending(等待状态)、fulfilled(完成状态)以及 reject(拒绝状态)。Promise 只能从 pending 状态变为 fulfilled 状态或 reject 状态,状态一旦改变就不能再变。
  3. 用法:Promise 对象可以通过构造函数来创建,构造函数接受一个函数作为参数(实参)。这个实参有两个参数,分别是 resolve 和 reject。resolve 和 reject 也是两个函数。
  4. 方法:then()catch()finally()Promise.all()Promise.race() 等。
  5. 错误处理:如果在执行 resolve 的回调时抛出异常,就会进入 reject 状态。catch 方法返回的仍然是一个 Promise 对象,可以继续调用 then 方法。

下面是这 5 点核心知识的思维导图:

1. 三个状态

当 Promise 进入 resolve 或者 reject 状态后,就可以使用 then() 方法或者 catche() 方法进行链式调用。

1.1 pending 等待状态

当创建一个 Promise 时,它首先就会处于这个状态。此时,异步操作还没有完成,仍在进行中。

const promise = new Promise((resolve, reject) => {
  // 这里是异步操作,例如 setTimeout
});

1.2 fulfilled 完成状态

调用 resolve 函数后,就表示异步操作已经完成,这时的 Promise 就时 fulfilled 状态。在完成状态下,可以通过 then 方法获取异步操作的结果。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("操作成功!"); // Promise 进入 fulfilled 状态
  }, 1000);
});

// fulfilled 状态下,可以通过 then 方法获取异步操作的结果
promise.then(result => {
  console.log(result); // 输出: "操作成功!"
});

1.3 reject 拒绝状态

异步操作失败,或者在 Promise 执行过程中抛出了错误时,通过调用 reject 函数可以让 Promise 进入拒绝状态。在拒绝状态下,可以通过 catche 方法捕获错误或异常。

1.3.1 异步操作失败时调用 reject:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("数据加载失败!"); // Promise 进入 reject 状态
  }, 1000);
});

// reject 状态下,可以通过 catch 方法捕获错误或异常
promise.catch(error => {
  console.log(error); // 输出: "数据加载失败!"
});

1.3.2 Promise 执行过程中抛出错误:

const promise = new Promise((resolve, reject) => {
  throw new Error("出现了一个错误!"); // Promise 进入 reject 状态
});

promise.catch(error => {
  console.log(error.message); // 输出: "出现了一个错误!"
});

2. 静态方法

通过上面的几段代码示例不难看出,Promise 对象是通过 Promise 的构造函数创建的。上面示例中的 then()catch() 和 finally() 都是 Promise.prototype 上的方法。除了原型链上的这三个方法外,Promise 还提供了一些静态方法。

2.1 Promise.all()

Promise.all() 用于处理多个 Promise,在”并行执行异步操作“、”资源预加载“、”事务处理“及”批量处理任务“等场景下都有应用。

Promise.all() 接受一个 Promise 对象数组作为参数,返回的是一个新的 Promise 对象。这就意味着,可以在返回的新的 Promise 对象上调用 then()catch() 和 finally() 方法,获取到 Promise 成功之后的结果或者是捕获 Promise 执行过程中的异常和错误。

需要注意的是,只有当数组中的所有 Promise 对象全部成功完成(fulfilled)时,Promise.all() 才会成功完成,如果数组中任何一个 Promise 失败(reject),Promise.all() 返回的新的 Promise 对象也会立即失败,并返回第一个失败的 Promise 的错误。

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values); // 输出: Array [3, 42, "foo"]
});

2.1.1 并行执行异步操作

比如在一个页面中,页面上展示的数据需要从 3 个 API 接口中获取,只有从这 3 个接口中成功拿到数据后,再渲染页面。

// 使用 fetch API 作为示例,根据实际情况使用其他的 AJAX 工具或库
function fetchHeadlines() {
  return fetch('https://api.example.com/headlines').then(response => response.json());
}

function fetchSportsNews() {
  return fetch('https://api.example.com/sports').then(response => response.json());
}

function fetchEntertainmentNews() {
  return fetch('https://api.example.com/entertainment').then(response => response.json());
}

// 使用 Promise.all() 并行获取所有接口数据
Promise.all([fetchHeadlines(), fetchSportsNews(), fetchEntertainmentNews()])
  .then(results => {
    const [headlines, sports, entertainment] = results;

    // 使用这三个结果来渲染页面...
    console.log(headlines, sports, entertainment);
  })
  .catch(error => {
    // 如果任何一个请求失败,这里会捕获错误
    console.error('There was an error fetching the news:', error);
  });

2.1.2 资源预加载

要预先加载页面上的图片、视频、音频等资源时,可以用 Promise.all() 来完成,以提高用户体验。

function preloadImage(src) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = reject;
    img.src = src;
  });
}

const imageSources = [
  'path/to/image1.jpg',
  'path/to/image2.jpg',
  'path/to/image3.jpg'
];

Promise.all(imageSources.map(src => preloadImage(src)))
  .then(images => {
    console.log('所有图片都已加载完成');
    // 进行其他操作,例如将图片添加到页面中
    // images.forEach(img => document.body.appendChild(img));
  })
  .catch(error => {
    console.error('预加载图片时发生错误:', error);
  });

这样做的目的,通常是确保在与图片进行交互之前,所有图片都已全部加载。比如,要在所有图片加载完成之后创建一个图片幻灯片。

2.1.3 事务处理

在数据库的操作中,“事务”是一个较为常见的概念。事务确保一系列操作要么全部执行成功,要么全部不执行。这主要是为了保证数据的完整性和一致性。

比如,在一个银行系统中,其中有两个操作:从一个账户扣款和向另一个账户存款。严格来讲,这两个操作必须都成功,否则,事务应该回滚。

针对这个场景,下面是一段通过 Promise 实现的简易实现:

function debitAccount(accountId, amount) {
  return new Promise((resolve, reject) => {
    // 模拟从账户扣款的异步操作
    setTimeout(() => {
      console.log(`Debited ${amount} from account ${accountId}`);
      resolve('success');
      // 如果扣款失败,可以调用 reject() 来表示操作失败
      // reject(new Error('Debit failed'));
    }, 1000);
  });
}

function creditAccount(accountId, amount) {
  return new Promise((resolve, reject) => {
    // 模拟向账户存款的异步操作
    setTimeout(() => {
      console.log(`Credited ${amount} to account ${accountId}`);
      resolve('success');
      // 如果存款失败,可以调用 reject() 来表示操作失败
      // reject(new Error('Credit failed'));
    }, 1000);
  });
}

function transferFunds(fromAccountId, toAccountId, amount) {
  return Promise.all([
    debitAccount(fromAccountId, amount),
    creditAccount(toAccountId, amount)
  ]);
}

// 使用示例
transferFunds('A123', 'B456', 100)
  .then(() => {
    console.log('Transfer successful');
  })
  .catch(error => {
    console.log('Transfer failed:', error.message);
    // 在真实的场景中,需要回滚之前的操作,比如退款到原账户
  });

2.1.4 批量处理任务

实际开发中有很多批量处理任务的需求,比如批量发送邮件的场景。

首先,定义一个发送邮件的方法 sendEmail,该方法返回一个 Promise。

function sendEmail(user, message) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 模拟邮件发送成功或失败
      const isEmailSent = Math.random() > 0.1; // 90% 的概率发送成功
      if (isEmailSent) resolve(`邮件已发送给 ${user}`);
      else reject(`邮件发送失败给 ${user}`);
    }, 1000);
  });
}

之后,使用 Promise.all() 来批量发送邮件:

const users = ['user1@example.com', 'user2@example.com', 'user3@example.com'];
const message = '这是一条通知邮件';

const emailPromises = users.map(user => sendEmail(user, message));

const emailPromises = users.map(user =>
  sendEmail(user, message)
    .then(result => ({ success: true, user, result }))
    .catch(error => ({ success: false, user, error }))
);

Promise.all(emailPromises)
  .then(results => {
    const successfulEmails = results.filter(result => result.success);
    const failedEmails = results.filter(result => !result.success);

    console.log(`成功发送 ${successfulEmails.length} 封邮件:`, successfulEmails);
    console.log(`发送失败 ${failedEmails.length} 封邮件:`, failedEmails);
  });

这里有一点需要注意:前面我们提到 Promise.all() 方法会在数组中的某个 Promise 失败后,整个 Promise.all() 都会失败。而这个场景中,我们需要拿到发送成功的和发送失败的邮件数量,就可以通过在每个 Promise 上添加一个 catch() 方法来做到。在 Promise 上添加 catch() 方法后能让 Promise 解析为成功状态。

你可能会有疑问,Promise 上添加一个 catch() 方法后,应该代表这个 Promise 失败(reject)了呀,为什么会被解析成成功(fulfilled)状态呢?

如果你有这个疑问,说明你对 Promise 的 reject 状态理解的还不够。我们回顾一下上面对 reject 状态的描述:

异步操作失败,或者在 Promise 执行过程中抛出了错误时,通过调用 reject 函数可以让 Promise 进入拒绝状态。

关键点在这里:异步操作失败 or 抛出错误

当我们为一个 Promise 添加 .catch() 方法时,我们实际上是在为这个 Promise 添加了一个错误处理函数。如果这个 Promise 失败(rejected),那么 .catch() 中的函数会被执行。但是,.catch() 本身也会返回一个新的 Promise 对象。

如果 .catch() 中的函数执行成功并返回一个值,或者没有任何返回值,那么由 .catch() 返回的新 Promise 会是一个成功(fulfilled)的状态。如果 .catch() 中的函数抛出一个错误,那么由 .catch() 返回的新 Promise 会是一个失败(rejected)的状态。

因此,当我们在每个 sendEmail Promise 后面添加 .catch() 并返回一个对象时,无论 sendEmail 是否失败,由 .catch() 返回的新 Promise 都会是一个成功的状态,因为我们没有在 .catch() 中抛出任何错误。

这就是为什么说,通过添加 .catch(),我们可以确保每个 Promise 都解析为成功状态。

在上面的示例中,尽管 failingPromise 是一个失败的状态,但由 .catch() 返回的新 Promise 是一个成功的状态,因为我们在 .catch() 中返回了一个值。

2.2 Promise.race()

Promise.race() 是 Promise 的一个静态方法,在需要对多个并发操作进行“竞速”或设置超时时非常有用。

Promise.race() 接受一个 Promise 对象的数组(或其他可迭代对象)作为参数。当数组中的任何一个 Promise 完成(无论是 resolve 还是 reject)时,Promise.race() 返回的 Promise 也会立即完成,并采用第一个完成的 Promise 的状态和值。

Promise.race() 有两个常见的应用场景:竞速超时控制

2.2.1 竞速

Promise.race() 可以被视为一个竞速,哪个 Promise 先完成,就采用哪个 Promise 的结果。

比如,我们要从多个源或者服务器请求相同的资源,并且请求返回的资源都是相同的。如果我们想要从响应最快的接口上拿到资源,那么这种场景下,就可以用 Promise.race() 方法。

const urls = [
  'https://server1.example.com/data',
  'https://server2.example.com/data',
  'https://server3.example.com/data'
];

// 请求数据,返回最快的响应
Promise.race(urls.map(url => fetch(url)))
  .then(response => console.log(response))
  .catch(error => console.error(error));

2.2.2 超时控制

假设你有一个异步操作,你希望它在一定的时间内完成,否则就认为它超时。

除了服务端设置请求超时外,我们前端也可以维护一个超时时间。比如,你发送了一个网络请求,但不希望请求过程无限期地等待,这时就可以用 Promise.race() 设置一个超时时间来实现这一需求。

function fetchWithTimeout(url, timeout) {
  const fetchPromise = fetch(url);
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Request timed out')), timeout);
  });

  return Promise.race([fetchPromise, timeoutPromise]);
}

fetchWithTimeout('https://api.example.com/data', 5000)
  .then(data => console.log(data))
  .catch(error => console.error(error));

在这个示例中,如果 fetch 请求在 5 秒内没有完成,timeoutPromise 会被 reject,导致整个 Promise.race() 被 reject。

2.3 其他静态方法

除 Promise.all() 和 Promise.race() 外,Promise 还提供了两个静态方法:Promise.allSettled() 和 Promise.any() 方法。

这两个方法我就不在这里过多赘述了,它们的用法都很简单,你完全可以作为扩展知识去了解它们。这里推荐去看 MDN 的文档,非常详细:

MDN – Promise 文档:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise

发表回复