前言
异步行为是js的基础,但是以前的实现并不理想,在早期的JavaScript中,只支持定义回调函数来表明异步操作完成,串联异步操作是一个常见的问题,通常需要深度嵌套的回调函数(俗称回调地狱来解决,这样的代码看起来不好理解也不好维护,所以Promise就出来解决这类问题了。
回调地狱
比如以下代码就是回调地狱
setTimeout(function () { //第一层
console.log('张三')
setTimeout(function () { //第二层
console.log('李四')
setTimeout(function () { //第三层
console.log('王五')
}, 1000)
}, 2000)
}, 3000)
一层嵌套着一层,只有等上一层执行完毕,下一层才可以开始执行,我们这里只嵌套了三层,真正的业务中可能嵌套层级更深,并且回调函数内部代码更复杂的话,看起来代码就难以理解又难以维护。
如果上面的代码用Promise写的话,应该是这样的:
let p = new Promise((resolve, reject) => {
setTimeout(() => { resolve() }, 3000)
})
p.then(() => {
console.log('张三')
return new Promise((resolve, reject) => {
setTimeout(() => { resolve() }, 2000)
})
})
.then(() => {
console.log('李四')
return new Promise((resolve, reject) => {
setTimeout(() => { resolve() }, 1000)
})
})
.then(() => {
console.log('王五')
})
当然这也不是最好的写法,但是看起来是不是比回调地狱好多了,至少看起来没有嵌套很多层,接下来我们就详细介绍一下Promise。
Promise
ES6新增的引用类型Promise可以通过new操作符来实例化。创建新期约时需要传入执行器函数作为参数否则会抛出SyntaxError
let p = new Promise(()=>{})
console.log(p)//Promise <pending>
Promise的状态
期约是一个有状态的对象,它可能处于以下三种状态之一:
- 待定(pending)
- 兑现(resolved或者fulfilled)
- 拒绝(rejected)
Promise的最初状态就是pending,在这样的状态下,Promise可以转变为resolved状态或者rejected状态,无论转变为何种状态都是不可逆的。也就是说只要从待定状态变为兑现或者拒绝,Promise的状态就不可再改变。而且不能保证Promise一定能从pending状态脱离,所以无论哪种状态都要有一定的行为。
Promise的状态是私有的,所以只能在内部进行操作。内部操作在Promise的执行器(也就是Promise的回调)中完成,执行器函数有两个参数,这两个参数我们一般将它命名为resolve()和reject()。调用resolve函数会把状态改变为兑现,调用reject函数会把状态改变为拒绝,并且会抛出错误。
例如:
let p1 = new Promise((resolve,reject)=>resolve())
console.log(p1)//Promise <resolved>
let p2 = new Promise((resolve,reject)=>reject())
console.log(p2)//Uncaught error (in promise)
//状态一旦改变就不可逆
let p3 = new Promise((resolve,reject)=>{
resolve()
reject()//没有效果
})
console.log(p3)//Promise <resolved>
在前面的例子中并没有什么异步操作,Promise本身是同步的,上面的代码是在Promise进行初始化时就将状态改变,所以并没有出现异步操作,接下来我们证明一下Promise本身是同步的
let p = new Promise((resolve, reject) => {
console.log('promise')
setTimeout(() => { resolve() }, 1000)
})
console.log(p)
控制台最后输出promise和 ’Promise <pending>‘,可见promise本身是同步的
promise.resolve和promise.reject
promise并不是一开始就一定是处于pending,我们可以通过Promsie.resolve()或者Promise.reject()实例化一个解决或者拒绝的Promise
比如下面这两个期约实例其实是一样的:
let p1 = new Promise((resolve,reject)=>resolve())
let p2 = Promsie.resolve()
使用这种静态方法实际上可以把任何值转换成一个Promsie,这个解决的Promise的值对应着传给Promsie.resolve()的第一个参数,多个参数将会被忽略
console.log(Promise.resolve())//Promise <resolved>:undefined
console.log(Promise.resolve(1))//Promise <resolved>:1
//多个参数将被忽略
console.log(Promise.resolve(1,2,3,4))//Promise <resolved>:1
这里有个注意点,当传入的参数也是一个Promise,那就是一个空包装,也就是说Promise.resolve()是个幂等函数
let p = Promise.resolve(1)
console.log(Promise.resolve(p))//Promise <resolved>:1
console.log(p === Promise.resolve(p))//true
console.log(p === Promise.resolve(Promise.resolve(p)))true
对比Promise.reject(),它的设计就没有照搬Promise.resolve()的幂等逻辑,如果给它传一个期约对象,那这个期约会成为它返回拒绝期约的理由
let p = Promise.reject()
let p1 = Promise.reject(Promise.resolve())//Promise<rejected>:Promise<resolved>
console.log(p === p1)//false
Promiseprototype.then()
Promiseprototype.then()是为Promise实例添加处理程序的主要方法,这个then可以接收两个参数:onResolved()和onRejected(),当Promise状态为resolved就进入onResolve(),当Promise状态为rejected就进入onReject(),简单来说就是Promise状态发生改变了then方法才会被调用并且执行相应的回调函数。
let p = new Promise((resolve,reject)=>{
setTimeout(()=>{resolve()})
})
p.then(()=>{
console.log('resolved')
})
let p1 = new Promise((resolve,reject)=>reject())
p1.then(null,()=>{
console.log('rejected')
})
最后控制台输出rejected,resolved,这里就涉及到事件循环的概念了,建议可以看一下 一文详解事件循环
这里有些注意点需要特别注意一下,当then()中传的是非函数处理程序就会出现值穿透,也就是会被静默忽略,比如:
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
先思考一下最后结果是什么呢?是控制台什么都没有还是报错还是控制台有东西呢?这里一定要记住then要传函数,不推荐传非函数的处理程序,否则会被静默忽略,也就是说前两个都被忽略了,这个值直接被传到最后一个then,然后控制台输出1,console.log是函数方法,不会被忽略
Promise.prototype.then()方法返回一个新的Promise实例对象:
let p1 = new Promise(()=>{})
let p2 = p1.then()
console.log(p1,p2,p1===p2)//Promise<pending>,Promise<pending>,false
这个新的Promise实例对象是通过then()的返回值被Promise.resolve()包装而成的,如果没有返回值默认返回undefined,这个undefined也会被包装成一个Promise.resolve(undefined),并且链式调用的后一个then要等前一个then的状态发生改变了才会被调用,举个栗子:
let p1 = new Promise((resolve, reject) => {
console.log(1)
resolve()
}).then(a => {
console.log(2)
let p2 = new Promise((resolve, reject) => {
console.log(3)
resolve()
}).then(b => {
console.log(4)
}).then(c => {
console.log(5)
})
}).then(d => {
console.log(6)
})
答案解析:1,2,3,4,6,5
首先在做这道题之前需要知道事件循环的基本概念,否则可能会被绕晕
-
p = new Promise是同步代码,进入后控制台输出1,随后p的状态发生改变,与之对应的then(a)的回调进入微任务队列。
-
由于接下来都没有同步代码了,所以进入微任务队列执行回调,此时只有then(a)的回调,所以控制台输出2。
-
随后出现进入p2的Promise,控制台输出3。
-
然后p2的状态发生改变,所以关于p2的then(b)的回调进入微任务队列,重点来了!!!很多小伙伴在这里栽跟头了(包括我自己刚学的时候)。接下来then(c)是要等then(b)返回的promise对象状态发生改变才会被推到微任务队列中,但是then(b)还没执行呢!!!所以then(a)的整体代码执行完毕,它默认会返回一个Promise.resolve(undefined),所以then(d)的回调进入微任务队列!!!
-
随后整体已经没有同步代码可执行了了,所以执行微任务队列,微任务队列中,现在回想一下刚才推了几个回调到微任务队列中了呢?是不是then(b)和then(d),所以先执行then(b)的回调,控制台输出4,现在then(b)执行完毕,返回一个Promise.resolve(undefined),所以then(c)的回调进入微任务队列,然后继续执行then(d)的回调,控制台输出6,随后执行then(c)的回调,控制台输出5
Promise.prototype.catch()
这个方法用于给Promise添加拒绝的处理程序,我们之前都是一起写在then()里面的,但是如果有很多then链式调用,那都写在then里面就显得混乱,事实上,这个就是个语法糖,调用它就相当于调用Promise.prototype.then(null,onRejected)
let p = Promise.reject()
p.catch(()=>{
console.log('rejected')//rejected
})
Promise.all()和Promise.race()
Promise类提供两个将多个Promise实例组合成一个Promise的静态方法:Promise.all()和Promise.race()
Promise.all()
Promise.all()静态方法创建的期约会在一组期约全部resolve()后再resolve(),这个静态方法接收一个可迭代对象,返回一个新Promise:
let p1 = Promise.all([
Promise.resolve(),
Promise.resolve()
])
p1.then(()=>{
console.log('resolved')//resolved
})
let p2 = Promise.all([
Promise.resolve(),
Promise.reject()
])
p2.catch(()=>{
console.log('reject')//reject
})
Promise.all()中的所有Promise实例都要是resolve,p1才是resolve,只要有一个是reject,对应的集成的promise实例就是reject,而且必须要传入可迭代对象,否则会抛出错误!!!
Promise.race()
Promise.race()静态方法返回一个包装期约,这个方法接收一个可迭代对象,返回一个新期约,race翻译过来就是比赛的意思,也就是说这组对象中哪个promise实例先解决或者先拒绝,那么这个合成的promise实例就是这个最先解决或者最先拒绝的promise实例的镜像:
let p1 = Promise.race([
Promise.reject(1),
new Promise((resolve,reject)=>{
setTimeout(()=>{resolve(2)})
})
])
p1.then((data)=>{
console.log(data)
}).catch((data)=>{
console.log(data)
})
最后控制台输出1
总结
最后建议大家可以去看一下这篇文章,写的非常好,有很多关于Promise的题可以练习,看完保证对Promise有一个更输入的了解。
参考资料
- 红宝书