前言

         在实际项目中我们常会去用已经封装好的promise如axios,或者也会自己去封装promise,甚至在面试中,关于promise的面试题也层出不穷,promise的重要性不言而喻,故写该文章记录并加深自身对promise的理解。

背景

        JavaScript是单线程语言,所以在执行耗时较大的任务时会导致页面阻塞。为了解决页面阻塞,异步执行被提了出来。但是异步带来一个执行顺序的问题,如果现在想要异步任务按照顺序依次执行,该怎么实现呢?

        我们知道,通过回调函数的嵌套,可以实现异步任务的依次执行,但是嵌套的回调函数代码耦合性强,阅读起来不直观,一旦嵌套过多,就会产生所谓的回调地狱,会带来以下问题:

  1. 代码臃肿
  2. 可读性差
  3. 代码复用性差
  4. 耦合高,维护性差
  5. 只能在回调中处理异常
doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

什么是promise?

        那如何让我们的异步编程看起来更优雅,易读,易维护呢,ES6为我们提供了其标准内置对象Promise。我们可以通过.then().catch()等方法,采用链式编程的方式获取异步任务的执行结果或者执行下一个异步任务,下面的代码是否更加优雅易读了呢?

doSomething().then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback)

当然,.then()里的回调函数也可以用箭头函数的形式来简写 

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
  console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);

如何使用?

1.创建一个Promise的实例。

        我们可以通过new一个Promise的构造函数,传入的参数为一个回调函数,回调函数传入了两个参数,分别为resolve回调函数和reject回调函数,当异步任务执行成功时,可以将我们需要的执行结果作为resolve的参数传递出来,当异步任务请求失败时,将执行失败的结果作为reject的参数传递出来。

2.通过.then()或者.catch()等方法捕获promise的执行结果。

        .then()可以传入两个回调函数作为参数,promise会将执行成功或者失败的结果的返回值作为参数,分别传给第一个和第二个回调函数,我们就可在回调函数中拿到他们。.catch()则是直接捕获promise失败的结果。那如何在promise中设置成功和失败结果的返回值呢?详见第二个示例代码。

下面是关于promise的两个案例,有助于理解promise的使用。

示例代码一 promise的简单使用

<script type="text/javascript">
      /*
       1. Promise基本使用
             我们使用new来构建一个Promise  Promise的构造函数接收一个参数,是函数,并且传入两个参数:		   resolve,reject, 分别表示异步操作执行成功后的回调函数和异步操作执行失败后的回调函数
      */


      var p = new Promise(function(resolve, reject){
        //2. 这里用于实现异步任务  setTimeout
        setTimeout(function(){
          var flag = false;
          if(flag) {
            //3. 正常情况
            resolve('hello');
          }else{
            //4. 异常情况
            reject('出错了');
          }
        }, 100);
      });
      //  5 Promise实例生成以后,可以用then方法指定resolved状态和reject状态的回调函数 
      //  在then方法中,你也可以直接return数据而不是Promise对象,在后面的then中就可以接收到数据了  
      p.then(function(data){
        console.log(data)   // 成功的值
      },function(info){
        console.log(info)  // 失败的值
      });
    </script>

   示例代码二 基于promise的ajax请求

<script type="text/javascript">
    /*
      基于Promise发送Ajax请求
    */
    function queryData(url) {
     #   1.1 创建一个Promise实例
      var p = new Promise(function(resolve, reject){
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function(){
          if(xhr.readyState != 4) return;
          if(xhr.readyState == 4 && xhr.status == 200) {
            # 1.2 处理正常的情况
            resolve(xhr.responseText);
          }else{
            # 1.3 处理异常情况
            reject('服务器错误');
          }
        };
        xhr.open('get', url);
        xhr.send(null);
      });
      return p;
    }
	# 注意:  这里需要开启一个服务 
    # 在then方法中,你也可以直接return数据而不是Promise对象,在后面的then中就可以接收到数据了
    queryData('http://localhost:3000/data')
      .then(function(data){
        console.log(data)
        #  1.4 想要继续链式编程下去 需要 return  
        return queryData('http://localhost:3000/data1');
      })
      .then(function(data){
        console.log(data);
        return queryData('http://localhost:3000/data2');
      })
      .then(function(data){
        console.log(data)
      });
  </script>

Promise 基本API 

实例方法

.then()

  • 得到promise内部任务的执行结果。

.catch()

  • 得到promise内部任务执行失败的结果。

.finally()

  • 成功与否都会执行 在项目中 我们基于axios发送请求时(axios其实也是一个基于ajax用promise封装的http请求库),往往会开启loading,我们可以通过finally,不管请求成功与否,关闭loading,而不需要在成功和失败的时候写两次关闭loading代码。)

  •  <script type="text/javascript">
        /*
          Promise常用API-实例方法
        */
        // console.dir(Promise);
        function foo() {
          return new Promise(function(resolve, reject){
            setTimeout(function(){
              // resolve(123);
              reject('error');
            }, 100);
          })
        }
        // foo()
        //   .then(function(data){
        //     console.log(data)
        //   })
        //   .catch(function(data){
        //     console.log(data)
        //   })
        //   .finally(function(){
        //     console.log('finished')
        //   });
    
        // --------------------------
        // 两种写法是等效的
        foo()
          .then(function(data){
            # 得到异步任务正确的结果
            console.log(data)
          },function(data){
            # 获取异常信息
            console.log(data)
          })
          # 成功与否都会执行(不是正式标准) 
          .finally(function(){
            console.log('finished')
          });
      </script>

.all()

  • Promise.all方法接受一个数组作参数,数组中的对象(p1、p2、p3)均为promise实例(如果不是一个promise,该项会被用Promise.resolve转换为一个promise)。它的状态由这三个promise实例决定

小案例 

现在有这样的一个需求,执行按钮的触发条件是必须以上两个表单都验证成功,那么可以通过将每个表单的验证结果包装成promise对象,通过Promise.all 实现

// 这是一段伪代码

function promise1() {
            return new Promise(function (resolve, reject) {
               this.$refs.form1.validate(v => {
                   if(v) resolve()
               })

            })
        }

        function promise2() {
            return new Promise(function (resolve, reject) {
               this.$refs.form2.validate(v => {
                   if(v) resolve()
               })

            })
        }

        Promise.all([promise1(), promise2()]).then(resolve => console.log('按钮触发事件')
        ).catch((e) => console.log(e))

.race()

  • Promise.race方法同样接受一个数组作参数。当p1, p2, p3中有一个实例的状态发生改变(变为fulfilledrejected),p的状态就跟着改变。并把第一个改变状态的promise的返回值,传给p的回调函数。

<script type="text/javascript">
    /*
      Promise常用API-对象方法
    */
    // console.dir(Promise)
    function queryData(url) {
      return new Promise(function(resolve, reject){
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function(){
          if(xhr.readyState != 4) return;
          if(xhr.readyState == 4 && xhr.status == 200) {
            // 处理正常的情况
            resolve(xhr.responseText);
          }else{
            // 处理异常情况
            reject('服务器错误');
          }
        };
        xhr.open('get', url);
        xhr.send(null);
      });
    }

    var p1 = queryData('http://localhost:3000/a1');
    var p2 = queryData('http://localhost:3000/a2');
    var p3 = queryData('http://localhost:3000/a3');
     Promise.all([p1,p2,p3]).then(function(result){
       //   all 中的参数  [p1,p2,p3]   和 返回的结果一 一对应["HELLO TOM", "HELLO JERRY", "HELLO SPIKE"]
       console.log(result) //["HELLO TOM", "HELLO JERRY", "HELLO SPIKE"]
     })
    Promise.race([p1,p2,p3]).then(function(result){
      // 由于p1执行较快,Promise的then()将获得结果'P1'。p2,p3仍在继续执行,但执行结果将被丢弃。
      console.log(result) // "HELLO TOM"
    })
  </script>

以上基本涵盖了promise的常见用法。

 promise 的状态改变

Promise在执行时,其状态也会发生相应的改变。

peding: Promise执行时,其内部任务还未执行,.then还未拿到执行结果。(初始化时,执行器函数执行(同步执行),如果同步的直接改变状态,则可以看成一个段同步代码)

resolved:内部任务执行完毕,状态为成功。(resolved被调用时)

rejected:内部任务执行完毕,状态为失败。(rejected被调用时)

1. pending 变为 resolved

2. pending 变为 rejected

说明: 只有这 2 种, 且一个 promise 对象只能改变一次 无论变为成功还是失败, 都会有一个结果数据 成功的结果数据一般称为 vlaue, 失败的结果数据一般称为reason。

是不是有些懵逼?咱们不废话,直接上代码。

// 由于Promise传入的回调函数是同步任务 故会在执行栈立即执行
        const p = new Promise((resolve, reject) => { 
            // 发现宏任务,放入浏览器宿主环境等待执行
            setTimeout(() => {
                console.log('异步执行完了');
                resolve('成功')
            }, 2000)
        })

        // .then()为微任务 等待当前所有同级的同步任务和宏任务执行完再执行
        p.then((res) => console.log('res', res)) 

        // 同步任务立即执行
        console.log('p', p);

上面的代码 我们通过console面板输出以下结果:

 是不是发现了什么不得了的东西?没错,代码执行输出p时,Promise的异步任务还未执行完,.then也还没拿到执行结果,故处于peding状态。那么同理,在resolved状态和rejected状态代码如下:

  const p = new Promise((resolve, reject) => {
            // 同步任务,立即执行
                console.log('异步执行完了');
                resolve('成功')
        })

        p.then((res) => console.log('res', res)) 

        // 执行此行代码时 promise内的任务已经执行完毕 且状态为成功
        console.log('p', p);

  const p = new Promise((resolve, reject) => {
            // 同步任务,立即执行
                console.log('异步执行完了');
                reject('失败')
        })

        p.catch((res) => console.log('res', res)) 

        // 执行此行代码时 promise内的任务已经执行完毕 且状态为成功
        console.log('p', p);

 

promise的基本流程 

关于promise的执行顺序

当我们new一个Promise时,传入的回调函数为同步代码 ,会立即执行,而.then() .catch()里的为异步微任务。

console.log(1)
setTimeout(function(){
  console.log(2)
}, 0)
new Promise(function(resolve){ //这里的回调是同步的
  console.log(3)
  resolve()
}).then(function(){  //异步微任务
  console.log(4)
})
console.log(5)


//输出结果 1,3,5,4,2

Promise的三个缺点

  • 1.无法取消Promise,一旦新建它就会立即执行,无法中途取消

  • 2.如果不设置回调函数,Promise内部抛出的错误,不会反映到外部

  • 3.当处于pending状态时,无法得知目前进展到哪一个阶段,是刚刚开始还是即将完成

注意:如果promise不通过.then或.catch方法捕获错误,在promise执行为reject状态时,会抛出一个reject错误,导致console面板报红,后面代码不在执行 ,我们也可通过配合async await 及try catch方法捕获错误执行相关逻辑操作。

 try {
        const res = await getArticleById(this.articleId) // getArticleById()是封装了的axios的api请求方法 本质也为一个promise
        this.articleInfo = res.data
        this.isLoading = false
        this.previewImage()
      } catch (err) {
        this.isLoading = false
        if (err.response && err.response.status === 404) {
          this.is404 = true
        }
      }

Logo

基于 Vue 的企业级 UI 组件库和中后台系统解决方案,为数万开发者服务。

更多推荐