了解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 的生命周期以及链式概念是如何工作的。
我们如何创建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!") ,就我们的代码而言,不会触发任何特别的东西。如果我们想捕获承诺的结果,那么我们需要做一些其他的事情,我们需要告诉对象在状态改变后触发一个函数。
消耗许诺,然后和捕捉
我们使用对象的方法then 和catch 来处理诺言。方法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避免代码重复
除了then 和catch ,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开发者都应该详细了解它们。这是一个经典的面试问题,所以要做好准备。