一文搞懂JS系列(八)之轻松搞懂Promise

写在最前面:这是我写的一个一文搞懂JS系列专题。文章清晰易懂,会将会将关联的只是串联在一起,形成自己独立的知识脉络整个合集读完相信你也一定会有所收获。写作不易,希望您能给我点个赞

合集地址:一文搞懂JS系列专题

概览

  • 食用时间: 15-20分钟

  • 难度: 简单,别跑,看完再走

  • 食用价值: 循序渐进了解Promise的概念,使用方法以及特性,还有Promise的痛点,以及最新的Promise.any()以及Promise.allSettled()。

  • 铺垫知识:

    ① 如果关于同步任务、异步任务不懂的同学可以移步看下我的这一篇博客,一文搞懂JS系列(六)之微任务与宏任务,Event Loop,

    ② 如果关于实例对象,原型,原型链不懂的同学可以参考下我的这篇文章 一文搞懂JS系列(七)之构造函数,new,实例对象,原型,原型链,ES6中的类

使用环境

Promise 本质上是一个构造函数,因为使用 new 关键字创建,它是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大,属于 ES6 中的一个概念。

简单来说,Promise 其实就是一个异步操作的容器,可以获取用来优雅地获取异步操作的结果。当然你也可以放同步操作,可以,但是不建议。毕竟同步任务一般直接正常书写即可,没必要套个 Promise 的壳子。

所以, Promise 一般用于 $ajax 数据请求,即用于封装 http 请求,例如下面的方式(用的 axios 做的数据请求)

// 向后台发送数据
export const postData= (params) => {
  return new Promise((resolve, reject) => {
    axios.post('/xxxUrl', params).then(res=>{
      resolve(res)})
      .catch(error=>{reject(error)});
  })
};

三种状态

Promise 实例有三种状态:

  • pending

    进行中,这是 Promise 实例创建后的一个初始态。

  • fulfilled

    已成功。在执行器中调用 resolve 后,达成的状态。

  • rejected

    已失败。在执行器中调用 reject 后,达成的状态。

这当然也很好理解,就和你做事情一样,一开始是进行中,最后,只有成功或者失败。

就拿上面的例子来说, postData 就是一个 Promise的实例对象,new Promise 之后,它的状态就是 pending ,只有当 axios.post 即网络请求成功或者失败了以后,它的状态才会变更,变更为 fulfilledrejected ,而这个第二种状态,取决于网络请求的结果。

Promise.prototype

  • Promise.prototype.then()

    then 方法简单理解就是 resolve() 回调之后的产物,即 已成功 状态下的回调函数。

  • Promise.prototype.catch()

    catch 方法简单理解就是 reject() 回调之后的产物,即 已失败 状态下的回调函数。

  • Promise.prototype.finally()

    finally 方法简单理解就是 无论成功或失败 ,都会执行的回调函数。

特性

1. 立即执行

Promise 实例创建后,执行器里的逻辑会立刻执行,在执行的过程中,根据异步返回的结果,决定如何使用 resolvereject 来改变 Promise 实例的状态。如何理解下面的这句话,先来看一个例子

  const promise = new Promise(function(resolve, reject) {
    console.log('start');
    if (true){
      resolve('success');
    } else {
      reject('error');
    }
    console.log('end');
  });
  
  promise.then(res=>{
    console.log(res);
  })

根据上面的学习,我们可以知道,在一开始控制台便会输出 start ,毕竟 Promise 在创建以后便会立即执行,然后输出 end ,等到处理完所有同步任务以后,再进行处理异步任务,因为走的是 resolve() ,所以最后输出 success,结果如下

start => end => success

2. 承诺

它另外也有自己一个很大的特点,那就是不受外界的影响。

只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

3. 结果唯一性

成功会触发 then,而失败会触发 catch ,状态不一致则会获取不到结果。可以看下下面的例子

const promise = new Promise(function(resolve, reject) {
    console.log('start');
    if(true){
        resolve('success');
    }else{
        reject('error');   
    }
    console.log('end');
});

promise.catch(error=>{
    console.log(error);
})

由于 Promise 中是 resolve 回调,即成功状态,所以,只会走 then() 而不会触发 catch

所以,程序的运行结果为 start => endcatch() 根本不会执行。

当然,不仅如此, Promise 一旦状态改变,就不会再变。我们来改写下上面的代码,为了加以区分,我们先使用 reject() 然后再调用 resolve()

  const promise = new Promise(function(resolve, reject) {
    console.log('start');
    reject('error');
    resolve('success');
    console.log('end');
  });
  
  promise.then(res=>{
    console.log(res);
  }).catch(error=>{
    console.log(error);
  })

由于状态一但改变,就不会再变,所以,它的状态有且只有且一直是初次改变的结果,而首次执行 reject ,即失败状态。

所以,上面的输出结果为 start => end => error

Promise.all()

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

例如定义了三个 postData 的实例方法,代码如下

const dataList = Promise.all([postData1(),postData2(),postData3()])

只有所有请求的状态都成功 ,dataList的状态才会变成 fulfilled ,此时 postData1, postData2, postData3的返回值组成一个数组,传递给dataList的回调函数。

只要有一个请求失败,那么,dataList的状态就会变成 rejected ,此时第一个被reject的实例的返回值,会传递给dataList的回调函数。

使用场景:

举个例子,三个接口分别是拉取语文,数学,英语三科成绩的接口,那么,我们需要通过这三个接口的返回,计算学生最后成绩的总分,此时用 all() 就很合理,因为三个接口的值缺一不可,如果有一个发生错误,就得不到总分,就会走 catch() ,然后,提示是哪一科的成绩数据得不到,影响了最终的总分计算。

Promise.race()

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

const dataList = Promise.rece([postData1(),postData2(),postData3()])

上面代码中,三个 Promise 实例,只要谁率先改变状态,那么,dataList 的状态也就跟着改变,相当于就是谁快,我就用谁

使用场景:

① 当一个接口有三个请求接口地址,请求的数据是一致的时候,为了保证接口的最快速度匹配,可以使用这个方法。

② 在Promise实例中,放入一个延时器函数, setTimeout(() => reject(new Error('request timeout')), 5000) ,可以通过它来设置这个接口的,相当于 postData1() 必须在5秒内完成,否则会直接失败

const dataList = Promise.race([
  postData1(),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);

Promise.allSettled()

Promise.allSettled() 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。(ES 2020 特性)

只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,最终包装成一个数组返回。并且它无论参数实例是成功或失败,始终只会走 .then() ,没有 .catch() ,成功之后返回的结果如下所示:


[
   { status: 'fulfilled', value: 42 },
   { status: 'rejected', reason: -1 }
]

每个对象都有 status 属性,该属性的值只可能是字符串 fulfilled 或字符串 rejectedfulfilled时,对象有 value 属性, rejected 时有 reason 属性,对应两种状态的返回值。

所以,可以通过 status 方法进行区分参数实例的成功或失败。

success = results.filter(p => p.status === 'fulfilled')
error = results.filter(p => p.status === 'rejected')

使用场景:

并不关心接口的结果,只关心这些操作有没有结束。

Promise.any()

Promise.any() 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。(ES 2021 特性)

只要参数实例有一个成功,包装实例就会变成成功状态;只有当所有参数实例都失败,包装实例才会变成失败状态。

使用场景:

未想出,待补充

回调地狱

Promise 的功能确实很强大,但有的时候,我们的接口是要按顺序执行,比方说我们要先拉取第一个接口,用第一个接口的参数去拉取第二个接口,然后再去拉第三个接口,此时,必须按顺序执行

getData1('').then(res1=>{
    getData2(res1.data.id).then(res2)=>{
        getData3(res2.data.id).then(res3=>{

        })
    }
})

当然,随着项目的复杂度,有时候可能需要四层五层,虽然这种情况应该比较少,这也就是所谓的回调地狱,其中又夹杂着闭包的概念,内部可以访问外层的结果,一层又一层的接口返回数据,维护的时候头都看晕了。

修改的时候还要先看到底是改第几层的代码,以防止改错地方。

当然,下一篇博客会来讲述下 Generator ,以及最终最优雅地方式 async await

最后,欢迎大家关注我的个人公众号 前端大食堂

参考

阮一峰 ECMAScript 6 入门 - Promise

系列目录

  • 一文搞懂JS系列(一)之编译原理,作用域,作用域链,变量提升,暂时性死区

  • 一文搞懂JS系列(二)之JS内存生命周期,栈内存与堆内存,深浅拷贝

  • 一文搞懂JS系列(三)之垃圾回收机制,内存泄漏,闭包

  • 一文搞懂JS系列(四)之闭包应用-柯里化,偏函数

  • 一文搞懂JS系列(五)之闭包应用-防抖,节流

  • 一文搞懂JS系列(六)之微任务与宏任务,Event Loop

  • 一文搞懂JS系列(七)之构造函数,new,实例对象,原型,原型链,ES6中的类

  • 一文搞懂JS系列(八)之轻松搞懂Promise

  • 一文搞懂JS系列(九)之更优雅地解决回调地狱

你可能感兴趣的:(一文搞懂JS系列(八)之轻松搞懂Promise)