【JavaScript】【ES6】Promise

82 阅读12分钟

前言

Promise是开发过程中使用频率很高的一个对象,但是我们不一定都能对这个对象描述的清楚。因此,在本篇文章中,我将对根据各方资料做一个汇总,便于大家和自己的回顾学习

一、Promise介绍

1.1 什么是Promise

  • Promise,这个单词对应的中文意思是“承诺,诺言”;
    • 是JS中的一个原生对象,是一种异步编程的解决方案,可以替换传统的函数解决方案
    • 是一个类
  • Promise是一个状态机,分为三种状态
    • pending:待定状态,分为 executor 后,处于该状态
    • fulfilled:兑现状态,调用resolve()后,Promise的状态更改为 fulfilled,且无法再次更改
    • rejected:拒绝状态,调用reject()后,Promise的状态更改为 rejected,且无法更改
function request() {
  const flag = Math.random() <= 0.5 ? true : false
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (flag) {
        resolve('成功的消息')
        return
      }
      reject('失败的消息')
    }, 2000)
  })
}

console.log('请求开始')
request()
  .then(msg => console.log(msg), err => console.log(err))

1.2 背景

  1. 为了解决回调地狱的问题
  2. 目前是前端最好的异步解决方案
  • 什么是回调地狱问题?

    如果在回调函数中再传入一个函数,就会出现一个嵌套结构,如此层层嵌套,就会形成回调地狱

    简单举个场景:业务中有时在请求数据会出现这种情况,需要发送多个Ajax请求,假设现在有1, 2, 3, 4这四个请求,4需要3的响应数据中的某个数据作为传参,3需要2的响应数据中的某个数据作为传参,2需要1的响应数据中的某个数据作为传参,如此层层嵌套就是回调地狱。这样的代码维护性差,且可读性也差

    //有多个异步任务,要求需要同时拿到所有异步任务的结果,下边就是用回调地狱
    $.get("url", (res1) => {
        conosle.log(res1)
        $.get("url+res1", (res2) => {
            conosle.log(res2)
            $.get("url+res2", (res3) => {
                conosle.log(res3)
                $.get("url+res3", (res4) => {
                    conosle.log(res4)
                })
            })
        })
    })
    
  • Promise是如何解决回调地狱的?

    • Promise构造函数接收一个函数作为参数,我们需要处理的异步任务就卸载该函数体内,该函数的两个参数是 resolve和reject。异步任务执行成功时调用resolve函数返回结果,反之调用reject
    • Promise对象的then方法用来接收处理成功时响应的数据,catch方法用来处理失败时相应的数据
    • Promise的链式编程可以保证代码的执行顺序,前提是每一次在then做完处理后,一定要return一个Promise对象,这样才能在下一次then时接收到对象
    function fn(str){
       var p = new Promise(function(resolve,reject){
            //处理异步任务
            var flag=true;
            setTimeout(function(){
                if(flag){
                    resolve(str)
                }
                else{
                    reject('操作失败')
                }
            })
        })
        return p;
    }
    
    fn('武林要以和为贵')
        .then((data)=>{
            console.log(data);
            return fn('要讲武德');
        })
        .then((data)=>{
            console.log(data);
            return fn('不要搞窝里斗')
        })
        .then((data)=>{
            console.log(data);
        })
        .catch((data)=>{
            console.log(data);
        })
    

1.3 Promise的用法

  • 声明:new Promise((resolve, reject) => {})
  • 出参:
    • resolve:将状态从未完成变为成功,在异步操作成功时调用,并将异步操作的结果作为参数传递出去
    • reject:将状态从未完成变为失败,在异步操作失败时调用,并将异步操作的错误作为参数传递出去

1.3.1 resolve的参数

resolve 传入的参数情况:

  • 如果传入的普通的值或对象,那么就会被传递到 then 的参数的

  • 如果传入的是一个Promise,那么当前的 Promise 的状态将会由传入的 promise 来决定

    const newPromise = new Promise((resolve, reject) => {
      resolve('success')
    })
    
    new Promise((resolve, reject) => {
      // 当前 Promise 的状态由传入的 Promise 去决定
      resolve(newPromise)
    })
      .then(res => {
        console.log('res', res) // res success
      })
      .catch(err => {
        console.log('err', err)
      })
    
    
  • 如果传入的是一个对象,且该对象实现了then方法(thenable),也会执行该then方法,并且由该then方法决定后续的状态

    new Promise((resolve, reject) => {
      // 如果 resolve 传入的是对象,且该对象实现了 then 方法
      // 则该 Promise 的状态由 then 方法决定
      resolve({
        then(resolve, reject) {
          reject('error')
        },
      })
    })
      .then(res => {
        console.log('res', res)
      })
      .catch(err => {
        console.log('err', err) // err error
      })
    
    

二、Promise的方法

2.1 Promise 的实例方法

2.1.1 then 方法

通过 then方法可以对 Promise中的resolve进行处理。then方法的返回值是一个Promise实例

```js
new Promise(resolve => {
  resolve('你好')
})
    .then(res => {
        console.log(res)  // 你好
    })
```
  • 多次调用then方法

    同一个Promise实例可以调用多个then方法,当 Promise 中 resolve被回调时,所有 then 方法传入的回调函数都会被调用

    const promise = new Promise(resolve => {
      resolve('你好')
    })
    
    // 同时调用
    promise.then(res => console.log(res)) // 你好
    promise.then(res => console.log(res)) // 你好
    promise.then(res => console.log(res)) // 你好
    
  • then 方法传入的回调函数可以有返回值

    • 返回的是普通值,那么这个普通值将作为一个新的 Promise 的resolve 的值

      const promise = new Promise(resolve => {
        resolve('你好')
      })
      
      promise.then(() => 'then').then(res => console.log(res)) // 打印 then
      // promise.then(() => 'then') 相当于
      promise.then(() => {
        return new Promise(resolve => {
          resolve('then')
        })
      })
      
    • 返回的是Promise,那么就可以再次调用then方法

      const promise = new Promise(resolve => {
        resolve('你好')
      })
      promise
        .then(() => {
          return new Promise(resolve => {
            setTimeout(() => {
              resolve('success')
            }, 2000)
          })
        })
        .then(msg => {
          // 2 秒后打印 success
          console.log(msg)
        })
      
    • 返回的是一个对象,并且该对象实现了 thenable,该 then 函数有两个参数resolvereject,则 resolve 的将会传递给下一个 Promise

      const promise = new Promise(resolve => {
        resolve('你好')
      })
      promise
        .then(() => {
          return {
            then(resolve) {
              return resolve('success')
            },
          }
        })
        .then(msg => {
          // 打印 success
          console.log(msg)
        })
      

2.1.2 catch 方法

处理then方法的第二个参数来捕获reject错误之外,还可以通过catch方法,catch返回一个Promise

const promise = new Promise((resolve, reject) => {
  reject('error')
})

promise.then(undefined, err => {
  // 打印 error
  console.log(err)
})

// 但是这种写法不太符合`promise/a+`规范
promise.catch(err => {
  // 打印 error
  console.log(err)
})

// 下面是符合`promise/a+`规范的写法
promise
  .then(() => {})
  .catch(err => {
    console.log(err)
  })
// 已知 then 方法也可以返回一个 promise,因此在 then 后面追加 catch,以此来捕获 rejected 的情况,更加具有语义化

catch方法也是可以多次调用的,只要 Promise 实例的状态为 rejected,那么就会调用catch方法

const promise = new Promise((resolve, reject) => {
  reject('error')
})

// 这两个 catch 都会调用
promise.catch(err => {
  console.log(err)
})
promise.catch(err => {
  console.log(err)
})

catch方法的返回值:

catch 方法也会返回一个 Promise 实例,返回值的情况:

  • 普通值,将作为 resolve 的参数

2.1.3 finally 方法

finally 是ES9(ES2018)新增的一个特性,无论一个 Promise实例是fulfilledrejectedfinally都会执行。

const promise = new Promise((resolve, reject) => {
    if(Math.ceil(Math.random()*10) % 2) {
        resolve("success")
    } else {
        reject('error')
    }
})

promise
  .then(res => {
    console.log('res:', res)
  })
  .catch(err => {
    console.log(('err', err))
  })
  .finally(() => {
    console.log('finally code execute')
  })

finally 不接收任何参数,这意味着没有办法知道,前面的Promise状态到底是fulfilled还是rejected。这表明,finally 方法里的操作,应该是与状态无关的,不依赖于Promise的执行结果,可以避免在thencatch处理器中国重复编写代码

finally本质上是then方法的特例。

promise
.finally(() => {
  // 语句
});

// 等同于
promise
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);

2.2 Promise的类方法

2.2.1 resolve 方法

Promise.resolve()静态方法将给定的值转换为一个Promise。如果该值本身就是一个 Promise,那么该 Promise 将被返回;如果该值是一个 thenable 对象,Promise.resolve()将调用其then方法及其两个回调函数;否则返回的 Promise 将会以该值兑现

new Promise(resolve => {
    resolve({ name: "张三" })
})
    .then(res => {
        console.log(res)
    })

image.png

还可以直接类方法resolve(),使用Promise.resolve()相当于new Promise(resolve => { resolve() })

Promise.resolve({name: '张三'})
    .then(res => {
        console.log(res)
    })

image.png

resolve参数形态:

  • Promise实例:原封不动地返回入参
  • thenable对象:将此对象转为 Promise 对象并返回(thenable为包含 then() 的对象,执行 then() 相当于执行此对象的 then()
  • 原始值/对象(不带 then() 的对象):将此对象转为Promise对象返回
  • 不带参数:返回Promise对象,状态为resolved

2.2.2 reject 方法

Promise.resolve()方法逻辑基本相同,只不过Promise.reject()相当于创建一个 Promise 实例,并且 rejected 了

Promise.reject('error').catch(error => {
  console.log('error', error)
})

image.png

参数形态Promise.reject()无论传递什么参数都会原样输出

Promise.reject(
  new Promise(resolve => {
    resolve('hello')
  })
).catch(err => {
  // 原样打印 Promise 实例
  console.log('err', err)
})

image.png

2.2.3 all 方法

将多个实例包装成一个新实例,返回全部实例状态变更后的结果数组。

  • Promise.all()的入参是Promise[], 数组的每个成员都是一个Promise实例

  • 如果 Promise 队列中所有的实例状态都是fulfilled,那么Promise.all()返回的实例的状态就会变为fulfilled,并且resolve()的参数是一个数组,按照顺序放置队列中每个 Promise 成功后的结果

    let i = 0
    function genPromise() {
      return new Promise(resolve => {
        resolve(`success${(i = i + 1)}`)
      })
    }
    
    const promiseArr = [genPromise(), genPromise(), genPromise()]
    
    Promise.all(promiseArr)
        .then(res => {
          // [ 'success1', 'success2', 'success3' ]
          console.log('res', res)
        })
    

    image.png

  • 如果队列中 Promise 实例有一个是rejected,那么Promise.all()返回的实例就会变为rejected状态,并且reject()参数是队列中第一个rejected的返回值

    const promiseArr = [
      new Promise(resolve => {
        resolve('success')
      }),
      new Promise((resolve, reject) => {
        reject('error1')
      }),
      new Promise((resolve, reject) => {
        reject('error2')
      }),
    ]
    
    Promise.all(promiseArr)
      .then(res => {})
      .catch(err => {
        // error 1
        console.log(err)
      })
    

    image.png

  • 缺点:如果在 Promise 队列中有一个状态是 rejected,那么我们就无法获取到其他 fullfilled 以及 pending 的 Promise 实例了。

    有点一荣俱荣,一损俱损的感觉

  • 适用场景

    • 彼此相互依赖,其中任何一个被reject,其它都失去了实际价值

2.3.4 allSettled 方法

将多个实例包装成一个新实例,返回全部实例状态变更后的结果数组。

Promise.allSettled()是ES11(ES2020) 中新增了一个 API,是为了解决all方法的缺陷,算是 all 方法的优化版

  • Promise.allSettled()的入参是Promise[], 数组的每个成员都是一个Promise实例
  • 特点
    • 该方法返回的 Promise 实例,会在所有 Promise 实例执行完毕后,状态方可变为fulfilled,并且只会是fulfilled
    • 无论队列中的 Promise 实例的状态如何,都能获取到结果
    • 打印的结果,会包含状态、值和原因的
const promiseArr = [
  new Promise((resolve, reject) => {
    resolve('success1')
  }),
  new Promise((resolve, reject) => {
    reject('error')
  }),
  new Promise((resolve, reject) => {
    resolve('success2')
  }),
]

Promise.allSettled(promiseArr).then(res => {
  // res [
  //   { status: 'fulfilled', value: 'success1' },
  //   { status: 'rejected', reason: 'error' },
  //   { status: 'fulfilled', value: 'success2' }
  // ]
  console.log('res', res)
})

image.png

  • 适用场景
    • 彼此不依赖,其中任何一个被 reject,对其它不影响
    • 期望知道每个 promise 的执行结果

2.3.5 race 方法

将多个实例包装成一个新实例,返回全部实例状态优先变更后的结果(先变更后返回)

  • 入参:具有Iterator接口的数据结构
  • 成功失败:如果某一个任务最先完成fulfilled/rejected,那么返回的实例的状态也会变成对应的fulfilled/rejected,同时获取到最先完成的结果
const promiseArr = [
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('success1')
    }, 1000)
  }),
  new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('error')
    }, 2000)
  }),
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('success2')
    }, 3000)
  }),
]

Promise.race(promiseArr)
  .then(res => {
    console.log('res', res)
  })
  .catch(err => {
    console.log('err', err)
  })
// 最终打印 res success1
// 如果第二个任务最先完成,那么就会打印 err error

2.3.6 any 方法

Promise.any()是 ES12 新增的特性,和Promise.race()类似,区别在于:

  • any 方法会等待一个fulfilled状态,才会决定返回 Promise 实例的状态
  • 如果队列中所有的实例都是rejected状态,那也需要等到所有执行完毕后才会决定返回的 Promise 实例的状态
const promiseArr = [
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('success1')
    }, 2200)
  }),
  new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('error')
    }, 2000)
  }),
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('success2')
    }, 3000)
  }),
]

Promise.any(promiseArr)
  .then(res => {
    console.log('res', res)
  })
  .catch(err => {
    console.log('err', err)
  })
// 遇到第一个 fulfilled,就会转变返回的 Promise 实例的状态
// 如果所有的都是 rejected,那么只有所有执行完毕后,返回的 Promise 实例才会转变
// 并且会抛出一个错误:[AggregateError: All promises were rejected]

三、Promise 的使用

3.1 Promise的链式调用

制作一个模拟网络请求:

  • 第一次返回 a,
  • 修改返回的结果为 aa,作为第二次网络请求返回的结果。
  • 修改结果为 aaa,作为第三次返回结果。
new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("a");
  }, 1000);
})
  .then((res) => {
    console.log("1秒后打印, res1", res); //1秒后打印 a
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(res + "a");
      }, 1000);
    });
  })
  .then((res) => {
    console.log("2秒后打印, res", res); //2秒后打印 aa
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(res + "a");
      }, 1000);
    });
  })
  .then((res) => {
    console.log("3秒后打印, res3", res); //3秒后打印 aaa
  });

这种情况其实就是接口的多层嵌套调用,Promise 可以把多层嵌套按照线性的方式进行书写,非常优雅。我们把 Promise 的多层嵌套调用就叫做链式调用

3.2 Promise 嵌套使用的简写

promise传入的函数参数reject是一个非必传的参数,如果不需要处理失败时的结果时,我们可以省略掉 reject 。代码如下:

// 简化1
new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("a");
  }, 1000);
})
  .then((res) => {
    console.log("res1", res);
    return new Promise((resolve) => resolve(res + "a"));
  })
  .then((res) => {
    console.log("res", res);
    return new Promise((resolve) => resolve(res + "a"));
  })
  .then((res) => {
    console.log("res3", res);
  });

Promise 嵌套使用时,内层的 Promise 可以省略不写,所以我们可以直接把 Promise 相关的去掉,直接返回,代码如下:

//简化2
new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("a");
  }, 1000);
})
  .then((res) => {
    return res + "a";
  })
  .then((res) => {
    return res + "a";
  })
  .then((res) => {
    console.log("res3", res);
  });

有的同学就在想,怎么都是成功状态的举例和简写,我们的失败状态catch可以简写吗?

答案是肯定的,我们简化为2层嵌套,与上述功能一致。

new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("a");
  }, 1000);
})
  .catch((err) => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject(err + "a");
      }, 1000);
    });
  })
  .catch((err) => {
    console.log("err", err);
  });

//简写1
new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("a");
  }, 1000);
})
  .catch((err) => {
    return new Promise((resolve, reject) => reject(err + "a"));
  })
  .catch((err) => {
    console.log("err", err);
  });

//简写2
new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("a");
  }, 1000);
})
  .catch((err) => {
    throw err + "a"; // 失败简写省略掉Promise时,使用的 throw 抛出异常。
  })
  .catch((err) => {
    console.log("err", err);
  });

注意:失败简写省略掉Promise时,使用的 throw 抛出异常。

四、手写 Promise 的实现

有空的时候再研究补上...

资料来源