一、前言
以往的经验告诉我,在接触自己比较陌生的名词和技术前首先要问三个问题:
- 它是用来做什么的?
- 它是如何实现的?
- 没有它,应该怎么办?
今天我们主要关注第一个问题点,浅析一下Promise的使用场景和一些特点。
二、定义
A
Promise
is an object representing the eventual completion or failure of an asynchronous operation.
这是MDN上对Promise的描述:Promise 是一个对象,它表现了一个异步操作最终的完成状态或者失败状态。
简单点说,Promise是为了更好地写异步操作而产生的,它保存着一个未来才会结束的事件的结果。
三、特点
- 对象状态不受外界影响,Promise对象有三种状态,pending(进行中)、fulfilled(成功)和rejected(失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
2.一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已完成)或者 settled。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
tips:很多教程里把resolved(已完成)等价于fullfilled(成功)状态,下文中Promise状态定型为resolved包含fullfilled和rejected两种状态。但是由于习惯写法,Promise中“成功”的回调函数的名字依然叫做resolve。
四、使用
在使用Promise之前我们先看看我们在没有Promise时是如何写异步操作的,以ajax请求为例子:
eg1:
$.ajax('url1')
.done((res)=>{
$.ajax('url2')
.done((res)=>{
$.ajax('url3')
.done((res)=>{
console.log(res)
})
})
})
这种有“层次感”的代码即callback hell,一旦嵌套三层以上就十分影响可读性,也容易出错。
再看看Promise如何实现上面的需求:
eg2:
Promise.resolve($.ajax('url1'))
.then((res)=>$.ajax('url2'))
.then((res)=>$.ajax('url3'))
.then((res)=>console.log(res))
如上,我们可以看到代码不再是层层嵌套,这样写异步操作更为直观。
1.基本用法
eg3:(先上代码)
const makePromise = ()=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
let num = Math.random()*10
if(num >5){
resolve('resolve--->' + num)
}else{
reject('reject--->' + num)
}
})
},2000)
}
makePromise()
.then(str=>console.log(str),err=>console.log('oh no' + err)) //resolve--->8.866619911022287
说明:
- Promise是一个构造函数,使用new可以生产Promise实例;
- 构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
- resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 fullfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
- Promise实例生成以后,可以用then方法分别指定fullfilled状态和rejected状态的回调函数。(then的参数是两个回调函数)
2. 使用Promise.prototype.catch()捕获错误
Promise有3个缺点:
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
- 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
现在我们针对第二点展开Promise实例的catch方法的使用
eg4:
makePromise()
.then(res=>$.ajax1(res))
.then(res=>$.ajax2(res))
.catch(err=>console.log(err)) //可以捕获前面所有Promise对象的error
说明:
- catch一般在Promise链的最后一步调用,它可以捕获前面任何一个Promise对象的error;
- 虽然then的第二个参数可以获取上一个Promise的error,但是不提倡这么写;应为catch可以捕获上面多个Promise的error,写法也更容易理解;
- Promsie实例执行完catch方法后,也会变成fullfilled;
eg5:
//bad
makePromise()
.then(res=>console.log(res),err=>console.log(err))
//good
makePromise()
.then(res=>console.log(res))
.catch(err=>console.log(err))
3.学会使用Promise.all()
Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
eg6:
const newPromise = Promise.all([promise1,promise2,promise3])
newPromise的状态由promise1,promise2,promise3共同决定;
分以下两种情况:
- 只有promise1、promise2、promise3的状态都变成fulfilled(完成),newPromise的状态才会变成fulfilled,此时promise1、promise2、promise3的返回值组成一个数组,传递给newPromise的回调函数。
- 只要promise1、promise2、promise3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
eg7:
let arr = [1,2,3]
let promiseArr = arr.map((item=>makePromise(item))
Promise.all(promsieArr)
.then(resArr=>console.log(resArr.length)) //3
上面的例7中只有当promiseArr中的三个Promise实例的状态定型(resolved,包含fullfilled和rejected两种状态)才会进入进入Promise.all后面的回调:
- 如果3个Promise实例的定型状态为fullfilled那么,Promise.all()得到的实例状态也会是fullfilled,3个Promise实例的返回值会放在一个数组中,作为入参传给Promise.all后面then的第一个回调函数;
- 如果3个Promise实例定型后,有任何一个实例的状态为rejected,那么Promise.all()得到的实例状态为rejected,第一个为rejected的Promise的返回值会传给Promise.all后面then的第二个回调函数;
有一点值得注意的是:
如果作为Promise.all参数的 Promise 实例,如果自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()后面的catch方法。
原因在三.2中的说明里提到了:“Promise实例调用catch方法后,状态会变为fullfilled”。也就是说在没调用catch方法状态为rejected的Promise实例,调用catch后状态变为了fullfilled,Promise.all()的状态会变成fullfilled,当然不会触发Promise.all()后面的catch了。
eg8:
let promise1 = new Promise((resolve,reject)=>{
resolve('hello')
}).then(res=>res)
.catch(err=>err)
let promise2 = new Promise((resolve,reject)=>{
throw new Error('this is an error')
}).then(res=>res)
.catch(err=>err)
let newPromise = Promise.all([promise1,promise2])
newPromise.then(res=>console.log(res))
.catch(err=>console.log(err))
/*
["hello", Error: this is an error
at Promise (<anonymous>:7:12)
at new Promise (<anonymous>)
at <a…]
*/
可以看到打印的结果是一个数组,代表newPromise的完成后的状态为fullfilled,两个Promise实例的返回值作为入参传给了then,而newPromise后面的catch并不会捕获到promise2中的error,应为promise2自己catch了error。
学了这么多理论,下面说一下Promise.all的使用场景:
一个网页,需要加载三个数据源的内容(图片、文字、背景音乐...),它们之间是没有依赖关系的,加载完成后取消loading:
//请求图片的Promsie
let promisePic = new Promise((resolve,reject)=>{
$.ajax('url1').done(res=>resolve(res))
.fail(err=>reject(err))
})
//请求文字的Promise
let promiseText = new Promise((resolve,reject)=>{
$.ajax('url2').done(res=>resolve(res))
.fail(err=>reject(err))
})
//请求背景音乐的Promise
let promiseBgm = new Promise((resolve,reject)=>{
$.ajax('url3').done(res=>resolve(res))
.fail(err=>reject(err))
})
let promiseLoad = Promise.all([promsiePic,promiseText,promiseBgm])
promiseLoad.then(()=>{clearLoading()})
.catch((err)=>console.log(err))
4.更多方法
Promise.prototype.finally(fn):
finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
Promise.race([p1, p2, ...]):
和Promise.all类似,Promise.race方法同样是接收多个 Promise 实例,但它返回最先fullfill的Promise的结果,如果有一个reject,就提前reject。
Promise.resolve(value):
有时需要将现有对象转为 Promise 对象,Promise.resolve方法就起到这个作用。
Promsie.reject(value):
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。
5.新的提案(proposal)[2019.8.28更新]
Promise.allSettled([p1, p2, ...]):
和Promise.all()类似,接受由Promise对象组成的数组,Promise.all()会在所有的Promise状态为fullfilled或者其中一个状态为rejected时返回,但是它不会管数组中Promise的状态,只要所有的Promise 状态为settled(fullfilled or rejected)。
tips: This is useful in cases where you don’t care about the state of the promise, you just want to know when the work is done, regardless of whether it was successful.
Promise.any([p1, p2, ...]):
Promsie.any()和Promise.race()类型,接受一个由Promise对象组成的数组,当其中任何一个状态变为fullfilled时,就会返回这Promise的结果。但是和Promise.race()不同的是,它在有Promise状态为rejected时也不会立即返回,只有在所有Promise reject时才返回rejected 的Promise。
五、总结
1.介绍了Promise的作用、定义和特点;
2.列举了Promise的简单使用和错误捕获方法;
3.简单列举了Promise部分方法的使用场景;
文中可能有不大严谨的地方,欢迎大家指出。
关于Promise的更多讲解可以参考
- 阮一峰老师的ECMAScript 6 入门
- 手写一个Promise