Promise小记

一、前言

以往的经验告诉我,在接触自己比较陌生的名词和技术前首先要问三个问题:

  1. 它是用来做什么的?
  2. 它是如何实现的?
  3. 没有它,应该怎么办?

今天我们主要关注第一个问题点,浅析一下Promise的使用场景和一些特点。

二、定义

A Promise is an object representing the eventual completion or failure of an asynchronous operation.

这是MDN上对Promise的描述:Promise 是一个对象,它表现了一个异步操作最终的完成状态或者失败状态。
简单点说,Promise是为了更好地写异步操作而产生的,它保存着一个未来才会结束的事件的结果。

三、特点

  1. 对象状态不受外界影响,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

说明:

  1. Promise是一个构造函数,使用new可以生产Promise实例;
  2. 构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
  3. resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 fullfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
  4. Promise实例生成以后,可以用then方法分别指定fullfilled状态和rejected状态的回调函数。(then的参数是两个回调函数)

2. 使用Promise.prototype.catch()捕获错误

Promise有3个缺点:

  1. 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  2. 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  3. 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

现在我们针对第二点展开Promise实例的catch方法的使用
eg4:

makePromise()
    .then(res=>$.ajax1(res))
    .then(res=>$.ajax2(res))
    .catch(err=>console.log(err)) //可以捕获前面所有Promise对象的error

说明:

  1. catch一般在Promise链的最后一步调用,它可以捕获前面任何一个Promise对象的error;
  2. 虽然then的第二个参数可以获取上一个Promise的error,但是不提倡这么写;应为catch可以捕获上面多个Promise的error,写法也更容易理解;
  3. 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共同决定;
分以下两种情况:

  1. 只有promise1、promise2、promise3的状态都变成fulfilled(完成),newPromise的状态才会变成fulfilled,此时promise1、promise2、promise3的返回值组成一个数组,传递给newPromise的回调函数。
  2. 只要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后面的回调:

  1. 如果3个Promise实例的定型状态为fullfilled那么,Promise.all()得到的实例状态也会是fullfilled,3个Promise实例的返回值会放在一个数组中,作为入参传给Promise.all后面then的第一个回调函数;
  2. 如果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的更多讲解可以参考

  1. 阮一峰老师的ECMAScript 6 入门
  2. 手写一个Promise
    原文作者:JRG_Orange
    原文地址: https://www.jianshu.com/p/1ab01ee4102a
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞