一、前言
以往的经验告诉我,在接触自己比较陌生的名词和技术前首先要问三个问题:
- 它是用来做什么的?
- 它是如何实现的?
- 没有它,应该怎么办?
今天我们主要关注第一个问题点,浅析一下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(已完成)。如果改变已经发生了,你再对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():
finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise.race():
和Promise.all类似,Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
promise.resolve():
有时需要将现有对象转为 Promise 对象,Promise.resolve方法就起到这个作用。
promsie.reject():
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。
五、总结
1.介绍了Promise的作用、定义和特点;
2.列举了Promise的简单使用和错误捕获方法;
3.简单列举了Promise部分方法的使用场景;
文中可能有不大严谨的地方,欢迎大家指出。
关于Promise的更多讲解可以参考