一文详解Promise的基础语法,巩固事件循环

1,062 阅读8分钟

前言

异步行为是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

首先在做这道题之前需要知道事件循环的基本概念,否则可能会被绕晕

  1. p = new Promise是同步代码,进入后控制台输出1,随后p的状态发生改变,与之对应的then(a)的回调进入微任务队列。

  2. 由于接下来都没有同步代码了,所以进入微任务队列执行回调,此时只有then(a)的回调,所以控制台输出2。

  3. 随后出现进入p2的Promise,控制台输出3。

  4. 然后p2的状态发生改变,所以关于p2的then(b)的回调进入微任务队列,重点来了!!!很多小伙伴在这里栽跟头了(包括我自己刚学的时候)。接下来then(c)是要等then(b)返回的promise对象状态发生改变才会被推到微任务队列中,但是then(b)还没执行呢!!!所以then(a)的整体代码执行完毕,它默认会返回一个Promise.resolve(undefined),所以then(d)的回调进入微任务队列!!!

  5. 随后整体已经没有同步代码可执行了了,所以执行微任务队列,微任务队列中,现在回想一下刚才推了几个回调到微任务队列中了呢?是不是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有一个更输入的了解。

参考资料

  • 红宝书