【JavaScript狂补知识第③弹】ES6之Promise异步编程
前言
ES6中Promise的提出本质上是为了解决回调地狱的问题,因此我们先来了解什么是回调地狱。
相关概念
回调函数
要了解什么是回调地狱,我们需要先知道回调函数。简单来说,当一个函数作为参数传入另一个函数中,并且它不会立即执行,只有当满足一定条件后该函数才可以执行,这种函数就称为回调函数。
简单的例子比如JS里面常用的定时器:
1 | setTimeout(() => { |
以上箭头函数我们就可以称为回调函数。
回调地狱
一般来说回调函数的使用伴随着异步任务的执行,异步任务不会影响主线程的执行,而是进入异步队列,不会阻塞主线程的执行,并且只有主线程中的任务全部执行完毕,才会在任务队列中按顺序取出已完成的异步任务对应的回调函数放入主线程再执行,哪怕异步任务已经到时间了。
1 | // 尽管是0ms后执行,还是会先执行完主线程的同步任务 |
但是在我们的开发场景中,可能某一个异步任务的执行需要在另一个异步任务的回调函数执行完后再执行,因此就会出现回调函数的嵌套现象。比如我希望在执行三个异步任务,分别打印“欢迎来到我的博客!”、“今天天气真好!”、“祝您天天开心!”,并且要按照顺序打印,那可能就会出现以下代码:(当然正常开发中不会是这么离谱的例子,这里只是方便说明,可能开发会遇到执行一个ajax请求后,拿到请求结果再根据请求结果发起另一个ajax请求都是有可能的)
1 | setTimeout(() => { |
上面这种回调函数嵌套回调函数的现象就称为回调地狱。为了解决回调地狱,ES6就提出了所谓的Promise异步编程的解决方案。
Promise
Promise 在英文中的意思是承诺。在程序中表示,承诺在一段时间后给出结果。根据Promise官网的说法,可以把Promise看作是一个构造函数,可以由 Promise 产生一个对象,以此用来封装一个异步操作,并且可以获取其成功/失败的最终结果。
Promise的状态
对于一个Promise对象来说,其状态会使以下三种状态之一:
- pending:
The initial state of a promise.
,Promise的初始状态,即还未给出结果的状态。 - fulfilled:
The state of a promise representing a successful operation.
,成功操作的Promise状态。 - rejected:
The state of a promise representing a failed operation.
,失败操作的Promise状态。
一个 Promise的状态只能由 pending
变为 fulfilled
,或者变为 rejected
,且只能改变一次。
Promise对象的使用
Promise构造函数
我们可以通过new Promise(executor)
来创建Promise对象,Promise构造函数接受一个executor执行器函数,这个executor又接受两个参数resolver和reject,分别表示操作成功/失败要使用的一个“工具”。
我们可以先创建一个Promise实例并通过console.log来初步认识一下:
1 | const p1 = new Promise((resolve, reject) => {}); |
最终打印结果如下图所示:
从上图可以看出Promise实例的初始状态确实为pending,而一旦我们的异步操作成功后,我们可以调用resolve工具来返回成功操作的结果以及转变Promise的状态,这里假设成功操作返回一个字符串:(reject同理,异步操作失败时调用)
1 | const p1 = new Promise((resolve, reject) => { |
通过上述代码我们可以知道,excutor函数中resolve/reject的调用或者不调用,影响着Promise的状态以及我们的后续使用。
Promise对象的方法
有了Promise之后我们可以在excutor中执行异步任务,对于任务成功调用resolve返回任务成功的相关内容,可能是数据等等,reject同理,那么我们怎么拿到这些数据呢?Promise给对象实例提供了一些方法,下面通过代码来演示:
Promise.prototype.then()
Promise.prototype.then 方法绑定在原型 prototype 上,意味着这是一个实例方法,必须在实例对象上才能使用。返回值是一个Promise对象,具体返回的规则如下:
如果 then 中的回调函数:
返回了一个值,那么 then 返回的 Promise 将会成为接受状态(fulfilled),并且将返回的值作为接受状态的回调函数的参数值。
没有返回任何值,那么 then 返回的 Promise 将会成为接受状态(fulfilled),并且该接受状态的回调函数的参数值为 undefined。
抛出一个错误,那么 then 返回的 Promise 将会成为拒绝状态(rejected),并且将抛出的错误作为拒绝状态的回调函数的参数值。
返回一个已经是接受状态(fulfilled)的 Promise,那么 then 返回的 Promise 也会成为接受状态,并且将那个 Promise 的接受状态的回调函数的参数值作为该被返回的Promise的接受状态回调函数的参数值。
返回一个已经是拒绝状态(rejected)的 Promise,那么 then 返回的 Promise 也会成为拒绝状态,并且将那个 Promise 的拒绝状态的回调函数的参数值作为该被返回的Promise的拒绝状态回调函数的参数值。
返回一个未定状态(pending)的 Promise,那么 then 返回 Promise 的状态也是未定的,并且它的终态与那个 Promise 的终态相同;同时,它变为终态时调用的回调函数参数与那个 Promise 变为终态时的回调函数的参数是相同的。
1
2
3
4
5
6
7const p1 = new Promise((resolve, reject) => {
resolve('异步任务执行成功!')
});
p1.then(data => {
console.log(data); // 异步任务执行成功!
});当我们异步任务成功后,我们可以通过resolve返回成功的结果,这时会改变p1的状态为fulfilled,然后会自动调用**.then**方法,并把resolve返回的结果作为.then中回调函数的参数data,后续针对data进行相关处理。
Promise.prototype.catch()
那么如果异步任务失败呢?怎么拿到对应的任务失败的信息?下面还是通过代码来演示:
1
2
3
4
5
6
7
8
9
10const p1 = new Promise((resolve, reject) => {
reject('异步任务执行失败!')
});
p1.then(data => {
console.log(data);
})
.catch(err => {
console.log(err); // 异步任务执行失败!
})可以看到如果excutor调用的是reject则会触发**.catch**方法。catch同样返回一个Promise 对象。如果 onRejected抛出一个错误或返回一个本身失败的 Promise,那么 catch()返回的 Promise的状态为rejected;否则,状态为 fulfilled。
因此我们知道通过在excutor中调用resolve/reject会导致.then或者.catch其中一个被触发。
现在回到最开始那个问题,我希望在执行三个异步任务,分别打印“欢迎来到我的博客!”、“今天天气真好!”、“祝您天天开心!”,并且要按照顺序打印,那么通过Promise我们怎么实现呢?
1 | const p1 = new Promise((resolve, reject) => { |
可以看到我们可以在第一个异步任务执行完后的.then方法中返回一个新的Promise对象,那么.then方法返回值就是一个Promise对象实例,就又可以继续在第二个任务执行成功后触发.then方法。通过Promise这种链式调用的方式避免了之前回调地狱嵌套很多层的情况,并且可读性更强,更趋向于同步的写法了。
那现在假设我希望对于每一个异步任务都有一个不同的catch,那需要怎么改写呢?其实.then的参数是两个,第一个参数是成功的回调函数onFulfilled,第二个参数是失败的回调函数onRejected,因此我们可以改写为下面这样:
1 | const p1 = new Promise((resolve, reject) => { |
然后我们来看一下控制台的输出:
我们可以看到确实提示了任务2失败,但是后面还打印了一个undefined,原因其实是因为reject(‘任务2失败的信息!’)后下一个.then的失败的回调函数没有返回值,这就导致了then没有返回值,那么.then会返回一个fulfilled状态的Promise,并且该接受状态的回调函数的参数值为 undefined,而这个fulfilled状态的Promise就会触发下一个then的成功的回调,打印对应的信息,因此就出现了打印undefined的情况。
解决这种情况的方式很简单,只需要在console.log(‘任务2失败了!’);后面抛出一个错误就可以了。抛出一个错误,那么 then 返回的 Promise 将会成为拒绝状态(rejected),并且将抛出的错误作为拒绝状态的回调函数的参数值。
1 | const p1 = new Promise((resolve, reject) => { |
async/await
虽然上述写法解决了回调地狱问题,但是如果遇到很多异步任务关联就需要去写一大长串的链式调用不太好看,因此ES7就提出了async/await 这一基于Promise的解决异步的最终方案。
通过await来获取fulfilled状态Promise的返回值,对于rejected的Promise则通过try-catch来捕获,下面是前文那个例子在async/await中的写法:
1 | const p1 = new Promise((resolve, reject) => { |
控制台输出如下:
Promise的方法
之前讲的都是Promise对象的方法,是需要通过Promise实例调用的,Promise还有许多静态方法,下面展示了常见的Promise方法。
Promise.resolve(value)
参数
可以是一个值,也可以是一个 Promise 对象,或者是一个 thenable。
返回值
- 若给定值为一个字面值或数组等等之类的,则返回一个成功的 Promise
- 若参数本身就是一个 Promise 对象,则直接返回这个 Promise 对象。
1 | const p1 = new Promise((resolve, reject) => { |
Promise.reject(reason)
参数
失败的原因。
返回值
返回一个状态为 rejected 的 Promise 对象。
Promise.all(iterable)
参数
多个 promise /值组成的可迭代类型(Array、Map、Set等)。
返回值
若传入的参数为空的可迭代对象,例如 Promise.all([]),则返回一个已完成状态(fulfilled)的 Promise
若传入的参数不包含任何 promise,则返回一个异步完成(asynchronously resolved)的 Promise。
其它情况下返回一个处理中(pending)的 Promise。只有接收的参数的 promise 全部成功,这个返回的 Promise 才会是成功的,只要有一个失败,则返回一个失败的 Promise,且返回的值为第一个失败的值。
若所有 promise 都成功,则返回的 Promise 的值为一个包含了多个结果的数组
1 | const p1 = new Promise((resolve, reject) => { |
Promise.race(iterable)
参数
可迭代对象,一般为包含多个 promise 的数组
返回值
返回一个 promise,只要给定的可迭代对象中的 promise 有一个先完成(成功或失败),就采用它的值作为最终 promise 的值,状态与这个完成的 promise 相同。
1 | const p1 = new Promise((resolve, reject) => { |