浅谈Promise原理与应用

54 阅读9分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

由于JavaScript是单线程的。在处理网络操作、事件操作时都是需要进行异步执行的。AJAX就是一个典型的异步操作

对于异步操作,有传统的利用回调函数和使用 Promise,二者的对比如下:

// 传统回调方式
函数1(function(){
    //代码执行...(ajax1)
    
    函数2(function(){
        //代码执行...(ajax2)
        
        函数3(function(data3){
              //代码执行...(ajax3)
        });
        ...
    });
});

//Promise回调方式:链式调用,可构建多个回调函数。
//例如请求一个ajax之后,需要这个拿到这个ajax的数据去请求下一个ajax
promise().then().then()...catch()

对比可知,使用传统回调函数方式处理异步操作很复杂。为了解决这样的问题,commonJS引入了Promise概念,很好的解决了JavaScript的异步操作。

概念

Promise是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更简单易理解且实用,所以Promise简单来说就是一个容器,里面保存着某个未来才会执行事件(通常为一个异步操作)的结果。

特点

  1. 对象的状态不受外界影响
  2. 一旦状态改变,就不会再变化,任何时候都可以得到这个结果    

语法

//创建Promise实例
    let promise = new Promise( (resolve,reject) =>{
        // 执行相应代码并根据情况调用resolve或者reject
        ...
    } )
    // 在promise的then方法中执行回调
    Promise.then( function(){
        // 第一个参数是返回resolve状态时执行的回调函数
    },function(){
        // 第二个参数是返回reject状态时执行的回调函数
    } )

状态

Promise对象代表一个异步操作,有三种状态:

pending(等待)fulfilled(已完成)rejected(已拒绝)

两种状态改变方式: pending => resolvedpending => rejected

: 只有异步操作的结果才可以决定当前是哪一种状态,任何其他操作都无法改变这个状态

局限性 

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

用法   

先看下面这个例子:

setTimeout( ()=>{
    console.log('123');
 },0 )
 console.log('456');

执行结果相信大家都知道,上面console处于异步代码中(即使延迟为0),下面console处于同步代码中,如果想要 ‘456’ 在 ‘123’ 执行结束后再输出呢?

传统回调函数方式:

setTimeout( ()=>{
     console.log('123');
     fn();
},0 )
 function fn(){
     console.log('456')
 }
// 123
// 456

使用 Promise 方式:

let promise = new Promise( (resolve,reject) =>{
     setTimeout( ()=>{
          console.log('123');
          resolve('456');
      } ,0)
  } )
  promise.then(function(data){
      // resolve状态
      console.log(data);
 },function(error){
     // reject状态
 })
// 123
// 456

Promise.prototype.then()

Promise 实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数。也就是说,状态由实例化时的参数(两个不同状态的回调函数)执行来决定的,根据不同的状态来执行具体哪个函数。

Promise.prototype.catch()

catch 作为 promise 原型上的方法, 用于执行返回 rejected 状态时传入的回调函数, 可以用于捕获错误。

: resolve() 和 reject() 的执行会传递到对应的回调函数的 data 或者 error,并且 then 方法返回的是一个新的 Promise 实例,可以继续调用 then 方法, 一般情况下是在 then 方法中执行成功状态时的回调函数,在 catch 方法中执行失败状态时的回调函数

链式操作用法

从表面上看,我们或许会觉得 Promise 只是能够简化传统的层层回调写法。然而,Promise的精髓是 ‘状态’,用维护状态,传递状态的方式来使得回调函数能够及时调用,它比传递 callback 函数要灵活、简单的多。下面为一个 Promise 的使用场景:

async1()
  .then(function(data){
      console.log(data);
      return async2();
  })
  .then(function(data){
      console.log(data);
      return async3();
  })
 .then(function(data){
     console.log(data);
 });

 function async1(){
     let p = new Promise(function(resolve,reject){
         // 异步操作
         setTimeout(()=>{
             console.log('异步任务1执行完成!');
             resolve('数据1');
         },1000);
     });
     return p;
 };
 function async2(){
     let p = new Promise((resolve,reject)=>{
         // 异步操作
         setTimeout(()=>{
             console.log('异步任务2执行完成!');
             resolve('数据2');
         },2000);
     });
     return p;
 };
 function async3(){
     let p = new Promise((resolve,reject)=>{
         // 异步操作
         setTimeout(()=>{
             console.log('异步任务3执行完成!');
             resolve('数据3');
         },3000);
     });
     return p;
 }
 // 1秒后...
 // 异步任务1执行完成!
 // 数据1
 // 2秒后...
 // 异步任务2执行完成!
 // 数据2
 // 3秒后...
 // 异步任务3执行完成!
 // 数据3

在 then 方法中,可以不用 return Promise实例对象,直接返回数据在后面的 then 中也能够接收到数据

reject用法   

在前面的例子中只有 resolve(成功状态)的回调,实际应用中还会有失败状态,reject 就是把 Promise 的状态设置为 rejected ,这样我们就能够在 then 中捕捉到,然后执行相应的回调

let num = 10;
  let p1 = function(){
      return new Promise((resolve,reject)=>{
          if(num <= 5){
              resolve("<=5,走resolve");
              console.log("resolve不能结束Promise");
          }
          else{
              reject(">5,走reject");
             console.log("reject不能结束Promise")
         }
     })
 }

 p1()
     .then(function(data){
         console.log(data)
     },function(error){
         console.log(error)
     })
 // reject不能结束Promise
 // >5,走reject

resolvereject 永远在当前环境的最后面执行,所以后面的同步代码会先执行

如果 resolvereject 之后还有代码需要执行,最好放在 then 里,然后在 resolve 和 reject 前面写上 return

Promise.prototype.all()

Promise.all() 方法用于将多个 Promise 实例包装成一个新的 Promise 实例

const p = Promise.all( [p1,p2,p3] );

p 的状态由 p1、p2、p3 决定,分为两种情况:

  1. 只有 p1、p2、p3 的状态都为 resolved 时,p 状态才会变成 resolved,此时 p1、p2、p3 的返回值组成一个数组传递给 p 的回调函数
  2. 只要 p1、p2、p3之中有一个状态为 rejected ,p 的状态就变成 rejected,此时第一个状态为 rejected 的实例的返回值会传递给 p 的回调函数

由于p 是包含3个 Promise 实例的数组,只有这三个实例状态都为 resolved,或者其中的实例状态为 rejected 时,才会调用 Promise.all 方法后面的回调函数

如果作为参数的 Promise 实例自己定义了 catch 方法,那么它一旦被 rejected,并不会触发 Promise.all 的 catch 方法,如果没有实例参数定义自己的 catch,就会调用 Promise.all 的 catch 方法

Promise.prototype.race()  

Promise.race() 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例

const p = Promise.race( [p1,p2,p3] )

使用该方法时,只要 p1、p2、p3 之中有一个实例率先改变状态(不管是成功还是失败状态),p 的状态就会跟着该实例变化,该实例的返回值传递给 p 的回调函数

Promise.resolve() 

在实际应用中有时需要将一个对象转为 Promise 对象,Promise.resolve() 方法就能够实现,该实例的状态为 resolved

const promise = Promise.resolve( '123' );

// 等价于
new Promise( resolve => resolve( '123' ) )

Promise.resolve 方法的参数类型有四种情况:

  • 参数为一个 Promise 实例

    • 如果参数就是 Promise 实例,那么 Promise.resolve 将不做任何修改,依然返回该实例
  • 参数为一个 thenable 对象

    • thenable 对象指具有 then 方法的对象,如:
let thenable = {
     then: function( resolve,reject ){
          resolve( 'aaa' );
    }  
}
  • Promise.resolve 方法会将这个对象转为 Promise 对象,然后立即执行 thenable 对象的 then 方法
let thenable = {
  then: function(resolve, reject) {
    resolve( 'aaa' );
  }
};

let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value);  // aaa
})

上面代码中,thenable 对象的 then 方法执行后,对象 p1 的状态就变为 resolved,从而立即执行后面那个 then 方法指定的回调函数,输出 aaa

  • 参数不是具有 then 方法的对象,或者根本不是一个对象

    • 如果参数是一个原始值,或者不是一个具有 then 方法的对象,则 Promise.resolve 方法返回一个新的 Promise 对象,状态为 resolved
 const p = Promise.resolve('Hello');
 
 p.then(function (s){
   console.log(s)
 });
 // Hello

上面代码生成一个新的 Promise 对象的实例 p,由于字符串 Hello 不属于异步操作( String 对象不具有 then 方法),返回 Promise 实例的状态生成就为 resolved,所以回调函数会立即执行,且 Promise.resolve 方法的参数会同时传给回调函数

  • 参数为空

    • Promise.resolve 方法可以不带参数使用,此时直接返回一个 resolve 状态的 Promise 对象

    • 如果希望得到一个 Promise 对象,比较方便的方法就是直接调用不带参数的 Promise.resolve 方法

 const p = Promise.resolve();
 
 p.then(function () {
   // ...
 });

 

  • 上面变量 p 就是一个 Promise 对象,注: 立即 resolve 的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时
  setTimeout(function () {
    console.log('three');
  }, 0);
  
  Promise.resolve().then(function () {
    console.log('two');
  });
  
  console.log('one');
 
 // one
 // two
 // three

上面代码中,setTimeout(fn,0)在下一轮“事件循环”开始时执行,Promise。resolve() 在本轮执行,console.log('one')则是立即执行,因此最先输出

Promise.reject()

Promise.reject( reason ) 方法也会返回一个新的 Promise 实例,该实例的状态为 rejected

 const p = Promise.reject('出错了');
 // 等同于
 const p = new Promise((resolve, reject) => reject('出错了'))
 
 p.then(null, function (s) {
   console.log(s)
 });
 // 出错了

上面代码生成一个 Promise 对象的实例 p,状态为rejected,回调函数会立即执行

: Promise.reject() 方法的参数,会原封不动的作为 reject 的理由,变成后续方法的参数。这一点与 Promise.resolve 方法不一致

  const thenable = {
    then(resolve, reject) {
      reject('出错了');
    }
  };
  
  Promise.reject(thenable)
  .catch(e => {
    console.log(e === thenable)
 })
 // true

上面代码中, Promise.reject 方法的参数为一个 thenable 对象,执行以后,后面 catch 方法的参数不是 reject 抛出的 ‘出错了’ 这个字符串,而是 thenable 对象

Promise.prototype.done

  Promise 对象的回调链,不管以 then 方法或者 catch 方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(Promise内部的错误不会冒泡到全局)。因此,可以提供一个 done 方法,总是处于回调链的末端,保证抛出任何可能出现的错误被捕捉

 asyncFunc()
   .then(f1)
   .catch(r1)
   .then(f2)
   .done();

上面代码可见,done 方法的使用,可以像 then 方法那样用,提供 resolved 和 rejected 状态的回调函数,也可以不提供任何参数。总之,done 都能够捕捉到任何可能出现的错误,并向全局抛出

Promise.prototype.finally

finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。它与 done 方法最大的区别是 finally 方法能够接收一个普通的回调函数作为参数,该函数不管怎样都必须执行

在项目中我们发起请求时,可以使用 finally 方法来对该请求做最终处理,比如关闭对话框等。

asyncFun()
    .then(()=>{
        // 接收收据
        ...
    })
    .catch(()=>{
        // 捕获错误
        ...
    })
    .finally(()=>{
        // 关闭对话框
        ...
    });

上面代码中,不管前面的 Promise 是resolved 状态还是 rejected 状态,都会执行回调函数callback