再来学习一遍 Promise,不信记不住

232 阅读9分钟

Promise 是什么

Promise 是一个代表了异步操作最终完成或者失败的对象。

Promise 提供了一种更优雅的方式来处理异步操作,它让异步代码的结构看起来更接近于同步代码,使得代码更易读、更易维护。

在传统的异步编程中,为了处理异步操作的结果,我们通常需要提供一个回调函数。

但是如果有多个异步操作需要按顺序执行,并且后一个操作的输入依赖于前一个操作的结果,我们就需要使用嵌套的回调函数,这就导致了所谓的"回调地狱"。

getData(function(a){
    getMoreData(a, function(b){
        getEvenMoreData(b, function(c){
            getSoMuchData(c, function(d){
                // 处理数据
            }, function(err){
                // 处理错误
            });
        }, function(err){
            // 处理错误
        });
    }, function(err){
        // 处理错误
    });
}, function(err){
    // 处理错误
});

回调地狱是指在处理 JavaScript 中的异步操作(例如网络请求、文件读取、定时器等)时,我们需要获取多个不同的数据,而每个数据的获取都依赖于前一个数据的结果。

这导致我们必须使用嵌套的回调函数来处理异步操作,代码看起来就像一个"金字塔",非常难以理解和维护,这就是所谓的回调地狱。上面就是一个典型的案例。

Promise 解决了这个问题

Promise 对象代表了一个异步操作的结果,我们可以通过 .then 方法来处理这个结果,而不需要提供回调函数。

此外,.then 方法返回的还是一个 Promise 对象,这意味着我们可以继续链式调用 .then 方法来处理更多的异步操作。

这使得异步代码的结构更清晰,更接近于同步代码。

getData()
  .then(data => processData(data))
  .then(processedData => saveData(processedData))
  .then(() => console.log('All done!'))
  .catch(error => console.log('Error:', error));

这里,getDataprocessDatasaveData 都是返回 Promise 对象的函数。

我们可以通过 .then 方法来依次处理它们的结果,如果在这个过程中出现任何错误,都会被 .catch 方法捕获并处理。

解决回调地狱还有两种常见的方案:Generatorasync/await,挖个坑后面再说

三种状态

Promise 对象有三种状态:

  1. Pending(等待): 这是初始状态,不是fulfilled也不是rejected。
  2. Fulfilled(成功): 操作成功完成。
  3. Rejected(失败): 操作失败或发生错误。

Promise 对象创建后,它会处于 Pending 状态,直到它被 fulfilled 或 rejected。

一旦 Promise 被 fulfilled 或 rejected,它的状态就不会再改变,成为了 settled 状态,也就是说 Promise 是不可变的。

这就像一个真实的承诺,一旦承诺被完成或拒绝,就不能再改变了。

Promise 的缺点

1、无法取消Promise,一旦新建,就会立即执行,中途无法取消

2、如果不设置回调,Promise内部抛出的错误,不会反应到外部

3、处于pending时,无法得知进展(刚开始还是即将完成)

所以,我们要注意的是:

1、对于还在 Pending 状态的 Promise,我们并不知道它是正在进行中,还是已经完成但是在等待一些其他事情。我们只能等待 Promise 被 fulfilled 或 rejected。

2、如果 Promise 已经 settled 了,再对 Promise 对象添加回调函数,回调函数会立即得到执行。如果 Promise 是 fulfilled,会用到 Promise 的返回值去调用成功回调函数;如果 Promise 是 rejected,会用到 Promise 的失败原因去调用失败回调函数。

3、thencatch 方法都返回一个新的 Promise,它们不会修改原有的 Promise 对象。

常用的 Promise 的方法

Promise.prototype.finally(onFinally)

不论 Promise 最后的状态如何,都会执行的操作。

一般用于清理工作,比如关闭文件、数据库连接或者清理数据等。

无论之前的 Promise 是否成功或者失败,我们都需要进行一些清理工作。这种情况下就可以使用 finally 方法:

let promise = new Promise((resolve, reject) => {
  // do something async
});

promise
  .then(result => console.log(result))
  .catch(error => console.log(error))
  .finally(() => console.log('Done!'));  // 无论成功还是失败,都会执行

Promise.resolve(value)

这个方法会返回一个 Promise 对象,这个对象会直接进入 resolved 状态。这个方法经常用于需要返回 promise 对象的场景。

如果你的函数有可能直接返回一个值,但是又需要保持返回 Promise 对象这样的接口,就可以使用 Promise.resolve

function maybeAPromise(value) {
  // 如果 value 不是 Promise,那么将其包装成 Promise
  return Promise.resolve(value);
}

Promise.reject(reason)

  1. 这个方法会返回一个 Promise 对象,这个对象会直接进入 rejected 状态。这个方法主要用于模拟一个失败的异步操作:
function alwaysFails() {
  return Promise.reject(new Error('I always fail. :('));
}

alwaysFails().catch(error => console.log(error.message));  // 输出: I always fail. :(

Promise.all(iterable)

这个方法接收一个 Promise 对象的集合,并返回一个新的 Promise 对象,这个新的 Promise 会在所有的 Promise 都成功的时候才会进入 resolved 状态,或者有一个 Promise 失败的时候就进入 rejected 状态。

如果你有很多异步任务,需要全部完成后才能进行下一步,就可以使用 Promise.all

let promise1 = new Promise(resolve => setTimeout(resolve, 1000, 'one'));
let promise2 = new Promise(resolve => setTimeout(resolve, 2000, 'two'));

Promise.all([promise1, promise2])
  .then(results => console.log(results))  // 输出: ['one', 'two']
  .catch(error => console.log(error));

Promise.race(iterable)

这个方法就像一场比赛,谁跑得快,谁就赢。它接收一组 Promise,然后只会等第一个完成(无论是成功还是失败)的 Promise,然后就返回这个 Promise 的结果。

let promise1 = new Promise((resolve, reject) => {
    setTimeout(resolve, 500, 'one');
});

let promise2 = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, 'two');
});

Promise.race([promise1, promise2]).then((value) => {
  console.log(value); // 'two',因为 promise2 更快地达到了解决状态
});

你有两个 Promise,一个1秒后完成,一个5秒后完成。Promise.race() 就会在1秒后返回,因为那个1秒的 Promise "赢了比赛"。

Promise.any(iterable)

这个方法会等待一组 Promise 中的任何一个成功。只要有一个 Promise 成功了,它就会返回那个成功的 Promise 的结果。

const promise1 = Promise.reject(0);
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'quick'));
const promise3 = new Promise((resolve) => setTimeout(resolve, 500, 'slow'));
const promises = [promise1, promise2, promise3];

Promise.any(promises).then((value) => console.log(value)); // 'quick'

如果所有的 Promise 都失败了,Promise.any() 会返回一个 AggregateError 类型的异常,表示所有的 Promise 都没有成功。

链式调用

当你使用 .then().catch() 时,这些方法会创建一个新的 Promise。链式调用的关键在于,你可以在一个 Promise 上连续调用 .then().catch(),从而让一系列操作按顺序执行。

这是因为每个 .then().catch() 的回调函数都可以返回一个值,这个值决定了新的 Promise 的状态。如果返回的是一个普通值,新的 Promise 就会成功;如果返回的是一个新的 Promise,那么新的 Promise 会根据这个返回的 Promise 来决定成功还是失败。

这种机制使得你可以用 .then().catch() 将多个操作链接在一起。

错误处理要注意的地方

1、在 Promise 链的末尾添加一个 .catch() 来捕获链中的任何错误。但要注意,如果末尾的 .catch() 本身抛出错误,这个错误就不会被捕获。为了处理这种情况,可以再加一个 .catch() 来捕获前一个 .catch() 的错误。

2、如果在一个 .then() 里同时处理成功和失败的情况(.then(successHandler, errorHandler)),如果 successHandler 抛出错误,这个错误不会被同级的 errorHandler 捕获,而是会传递给下一个 .catch().then() 的错误处理函数。为了避免这种情况,建议把成功和失败的处理函数分开,分别用 .then(successHandler).catch(errorHandler) 表示。

3、尽量在合适的层次处理错误,而不是都在链的末尾处理。这样可以更好地定位错误发生的位置,并根据不同的错误采取不同的处理方式。

4、如果使用 Promise.all() 等组合方法,要注意,其中任何一个 Promise 失败,整个 Promise.all() 的结果就会失败。在这种情况下,你需要确保正确处理每个单独的 Promise 的错误,避免整个组合操作失败。

Promise 与异步编程

在 JavaScript 中,异步编程通常用于处理网络请求、定时器、事件处理等。在过去,我们使用回调函数来处理这些异步操作,但这可能导致回调地狱(Callback Hell),代码阅读和理解难度大。

Promise 为处理这些异步操作提供了一种更优雅、更易于理解和管理的方式。

总结下来就是:

1、Promise 是一个代表了异步操作最终完成或者失败的对象,有三种状态。

2、链式调用结构

3、静态方法比如 all、race、resolve、reject 等等

最后,相关面试题自测

请解释 Promise 是什么?

Promise 是一个代表了异步操作最终完成或者失败的对象。

它有三种状态:pending(等待中),fulfilled(已完成),或rejected(已拒绝)。

Promise 提供了一个方式来处理异步操作的结果,无论是成功还是失败。而不必在最初定义回调函数。

Promise.all 和 Promise.race 的区别是什么?

Promise.all 接收一个 Promise 对象的数组,当这些 Promise 对象全部完成时,返回一个新的 Promise 对象,它的状态变为完成,值是一个数组,数组的值是每个 Promise 对象的返回值。

如果其中一个 Promise 对象被拒绝,那么返回的 Promise 对象立即变为拒绝,并返回拒绝的那个 Promise 对象的原因。

Promise.race 同样接收一个 Promise 对象的数组,但它返回一个新的 Promise 对象,这个 Promise 对象在数组中的任意一个 Promise 对象完成或拒绝时,立即变为相同的完成或拒绝状态。

如何处理 Promise 中的错误?

在 Promise 链中,可以使用 catch 方法来捕获错误。

catch 方法返回一个 Promise,并且处理异步代码抛出的错误。通常,我们将 catch 放在链的末尾,以确保捕获到所有在链中可能发生的错误。

什么是回调地狱,Promise 是如何解决的?

回调地狱是指当异步操作执行后的回调函数嵌套层级太多,导致代码难以阅读和维护的问题。

Promise 通过链式调用的方式,可以让我们以一种更加清晰和线性的方式来组织代码,避免了回调。