了解JavaScript中的Promise

43 阅读9分钟

了解JavaScript中的承诺

对于一些用户来说,由于抽象的力量,他们甚至在不知不觉中已经在他们的代码中使用了承诺。然而,在处理任何JavaScript应用程序时,我们必须了解什么是承诺,它是如何工作的,以及它背后的力量。

但在我们开始之前,让我们先看看下面的伪代码。

const promiseToReader = new Promise((resolve, reject) => {

    setTimeout(function() {
        if (userLikedTheArticle) {
            resolve('This article is awesome!')
        } else {
            reject('I should have never been here! ;p')
        }
    }, enoughToReadArticle)

})

我希望你能欣赏一下我的JavaScript幽默:p,但这段代码非常重要,因为它是我们第一次接触到承诺。

什么是Promise?

Promise 对象表示一个异步操作的最终完成(或失败),以及它的结果值。更准确地说,它是一个在创建承诺时不一定知道的值的代理。它允许异步操作的最终成功值或失败原因。用更简单的话来说,虽然技术上不正确,但它就像一个函数,它可以并行运行,一旦完成就返回它的值,即使函数的结果还没有出来,也允许调用的块继续运行。

一个Promise ,可以处于以下任何一种状态。

  • pending:初始状态,既不履行也不拒绝。
  • fulfilled:意味着操作成功完成。
  • rejected: 意味着操作失败。

诺言的一个有趣的行为是它们可以是连锁的。下图显示了一个Promise 的生命周期以及链式概念是如何工作的。

image.png

我们如何创建Promise?

让我们从构造定义开始。

/**
 * Creates a new Promise.
 * @param executor A callback used to initialize the promise. This callback is passed two arguments:
 * a resolve callback used to resolve the promise with a value or the result of another promise,
 * and a reject callback used to reject the promise with a provided reason or error.
 */
new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;

那么这些都是什么意思呢?让我们在一个空白的例子中看看

const welcomeToPromises = new Promise((resolve, reject) => {
    // This is the body if the asynchronous operation
    // When this operation finishes we can call resolve(...) to set the Promise as fulfilled
    // In case of failure, we can call reject(...) to set the Promise as rejected

    // Let's do now something async, like waiting for a second
    // Though in reality you would probably do a remote operation, like XHR or an HTML5 API.
    setTimeout(() => {
        resolve('Success!') // Super! all went well!
    }, 1000)
}

到目前为止,我们只是创建了承诺,主体中的代码开始执行。1秒后,Promise将改变它的状态为已完成,然而我们的resolve("Success!") ,就我们的代码而言,不会触发任何特别的东西。如果我们想捕获承诺的结果,那么我们需要做一些其他的事情,我们需要告诉对象在状态改变后触发一个函数。

消耗许诺,然后和捕捉

我们使用对象的方法thencatch 来处理诺言。方法then 将注册一个回调,在承诺得到解决或拒绝后自动执行,而方法catch 将注册一个回调,当Promise 失败时将被触发。

让我们再次看看这两个方法的声明。

/**
 * Attaches callbacks for the resolution and/or rejection of the Promise.
 * @param onfulfilled The callback to execute when the Promise is resolved.
 * @param onrejected The callback to execute when the Promise is rejected.
 * @returns A Promise for the completion of which ever callback is executed.
 */
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;


/**
 * Attaches a callback for only the rejection of the Promise.
 * @param onrejected The callback to execute when the Promise is rejected.
 * @returns A Promise for the completion of the callback.
 */
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;

让我们用我们的promiseToReader 的例子来看看它们的作用。

promiseToReader.then(result => {
    console.log('result', result)
}, error => {
    console.error('error in then', error)
})
  
promiseToReader.catch(error => {
    console.error('error', error)
})

在我们的例子中,我们注册了一个then 回调,和一个catch 回调。在then 块的情况下,我们可以注册2个回调,一个用于成功,一个用于失败,而在catch ,我们只能注册一个失败回调。

回调只是一个函数,有一个参数,就是操作的结果,在成功的情况下有一个value ,在失败的情况下有一个reason ,虽然这只是一个惯例,技术上我们可以返回任何我们想要的东西。

那么,我们的例子会发生什么呢?

嗯......如果userLikedTheArticle ,那么回调将被触发,我们将在控制台看到结果。然而,如果!userLikedTheArticle ,那么失败回调将被调用,在这种情况下,调用两次,一次是针对then 方法,另一次是针对catch 方法。

现在让我们假设我们将所有这些代码封装成一个test(userLikedTheArticle) 函数,并按如下方式调用这个函数两次。

console.log('Initiating test...')
console.log('Good reader started reading...')
test(true)
console.log('Probably what is a bot started reading...')
test(false)
console.log('Both users are now reading, and soon I should get my results')

你认为输出结果会是什么?

Initiating test...	
Good reader started reading...	
Probably what is a bot started reading...	
Both users are now reading, and soon I should get my results	
result	This article is awesome!	
error in then	I should have never been here! ;p	
error	I should have never been here! ;p

棒极了!但要好好阅读,注意执行中的顺序。虽然test(true) 被调用在console.log("Probably what is a bot started reading...") 之前,但后者先被打印在屏幕上,这是因为承诺,还在解决,因此在到达那行代码时,还没有实现。这很吸引人,你可以看到它的实况,并在演示中玩玩它。

承诺链

因为.then().catch() 总是返回一个新的承诺,所以可以通过精确控制错误的处理方式和时间来实现承诺链。为什么会有这样的帮助?想象一下这样的情况:用户在表单上输入一个URL,我们需要检索和处理这个URL的信息,你会怎么做?它可以是这样的。

fetch(url)
  .then(validate)
  .then(process)
  .catch(handleErrors)

它看起来很美,让我们建立一个简单的异步任务管道。

用Finally避免代码重复

除了thencatch ,promises还暴露了第三种方法来注册一个回调,finally ,声明语法如下。

finally?<U>(onFinally?: () => U | Promise<U>): Promise<U>;

这些回调,与之前的回调类似,将返回一个承诺,所以它可以被链起来,并且它将在承诺结算时被执行,无论是实现还是失败。这些回调对于清理或关闭加载器非常有用。它们非常容易使用。

promiseToReader.finally(function() {
    // settled (fulfilled or rejected)
    console.log("Finally settled!")
});

我如何取消一个Promise?

简短的回答是:你不能!承诺在设计上是不能取消的。承诺在设计上是不能被取消的,然而,一些非常聪明的人想到了一些可以模拟取消的方法。甚至还有一些库可以帮助完成这项任务,但由于本质上承诺是不会被取消的,所以我不会在这里介绍它们。

额外的东西

除了promise对象之外,Promise 还提供了一系列的静态方法,可以帮助你更容易地处理promise,让我们来看看这些方法。

  • Promise.all(iterable):等待所有的承诺被解决,或者等待任何承诺被拒绝。

    如果返回的承诺被解析,它将被解析为来自被解析的承诺的值的聚合数组,其顺序与在多个承诺的迭代中定义的顺序相同。

    如果它被拒绝,它被拒绝的原因来自迭代表中被拒绝的第一个承诺。

  • Promise.allSettled(iterable):等到所有的承诺都解决了(每个承诺都可能解决或拒绝)。

    返回一个在所有给定的承诺解决或拒绝后解决的承诺,其中有一个对象数组,每个对象描述每个承诺的结果。

  • Promise.race(iterable):等待,直到任何一个承诺被解决或拒绝。

    如果返回的承诺解决了,它就会用迭代器中第一个承诺的值来解决。

    如果它被拒绝,它将被拒绝,并附上第一个被拒绝的承诺的原因。

  • Promise.reject(reason):返回一个新的Promise对象,该对象以给定的理由被拒绝。

  • Promise.resolve(value):返回一个新的Promise对象,该对象以给定的值被解析。如果值是一个thenable(即有一个then方法),返回的承诺将 "跟随 "那个thenable,采用它的最终状态;否则返回的承诺将用值来实现。

    一般来说,如果你不知道一个值是否是一个承诺,可以用Promise.resolve(value)代替它,并将返回值作为一个承诺来处理。

异步/等待

当链式承诺时,代码可能会失控,而且可能会很难读。所以有些人想到了另一种方法来处理承诺,使用异步函数和等待操作符。

让我们看看它们的定义。

  • 等待操作符。用来等待一个承诺。它只能在一个异步函数中使用。
  • 异步函数:是一个用async关键字声明的函数。异步函数是AsyncFunction构造函数的实例,并且允许在其中使用 await 关键字。async和await关键字使异步的、基于承诺的行为能够以更简洁的风格编写,避免了明确配置承诺链的需要。

但是,一个例子总是更好的,所以让我们跳入其中

function resolveAfter2Seconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('calling');
  const result = await resolveAfter2Seconds();
  console.log(result);
  // expected output: 'resolved'
}

asyncCall();

你认为结果会是什么?

> "calling"
> "resolved"

与之前的例子不同,在这里,调用的执行 "停止 "了,等待Promise被解决后再继续。这种语法避免了使用回调,可以给代码带来很大的清晰性。


总结

Promise在JavaScript中被广泛使用,现在大多数浏览器都会提供Promise API的本地实现,只有一些旧的浏览器,如IE的某些版本,需要一个polyfilll或类似的东西来工作。诺言的使用非常有趣,尽管最初可能真的很难掌握。练习、练习、再练习,这个功能有很多用例,毫无疑问,任何JS开发者都应该详细了解它们。这是一个经典的面试问题,所以要做好准备。