草庐IT

ES6 Promise详解

迪迪滴 2023-03-28 原文

前言

本文主要是对Promise本身的用法做一个全面解析而非它的原理实现,如果你对Promise的用法还不是很熟悉或者想加深你对Promise的理解,我相信这篇文章一定会帮到你。

首先让我们先了解一下JavaScript为什么会引入Promise

回调地狱

让我们先看这样一段代码,JQuery中ajax请求:

  $.ajax({
      url: "url1",
      data: {},
      success(res1) {
        //获取到第一个数据
        console.log(res1);
        //根据第一个数去去获取第二个数据
        $.ajax({
          url: "url2",
          data: {
            query: res1.xxx,
          },
          success(res2) {
            //获取到第二个数据
            console.log(res2);
            //根据第二个数去去获取第三个数据
            $.ajax({
              url: "url3",
              data: {
                query: res2.xxx,
              },
              success(res3) {
                //获取到第三个数据
                console.log(res3);
                //...
              },
            });
          },
        });
      },
      error(err) {
        console.log(err);
      },
    });

我们会发现这些回调一层又一层,这就被称为回调地狱(callback hell),尤其业务逻辑复杂的时候这些回调就会变得难以维护。于是Promise就出现了。我们再看一个使用promise封装的axios请求:

 axios
      .get(url1, {})
      .then((res1) => {
        //获取到第一个数据
        console.log(res1);
        //根据第一个数去去获取第二个数据
        return axios.get(url2, { query: res1.xxx });
      })
      .then((res2) => {
        //获取到第一个数据
        console.log(res2);
        //根据第二个数去去获取第三个数据
        return axios.get(url3, { query: res2.xxx });
      })
      .then((res3) => {
        //获取到第三个数据
        console.log(res3);
        //...
      })
      .catch((err) => {
        console.log(err);
      });

通过对比我们会发现Promise解决了传统的回调函数的回调地狱问题,使得业务逻辑显得更加清晰了。接下来我们就开始正式介绍Promise了。

概述

Promise是现代异步编程的基础,在Promise返回给我们的时候操作其实还没有完成,但Promise对象可以让我们操作最终完成时对其进行处理,无论成功还是失败。

Promise的返回有三种状态分别是等待(pending), 成功(fulfilled),拒绝(rejected),我们看以下示例(后续我们将用延时器setTimeout来代表我们的异步操作)

 const promise1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(1);
      },1000);
    });
    console.log(promise1);

此时我们可以看到我们获取的Promise是pending(等待的状态)。

同样当我们一秒钟过后再去获取Promise

 const promise1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(1);
      }, 1000);
    });
    setTimeout(() => {
      console.log(promise1);
    }, 1000);

它得到的就是成功(fulfilled)状态

然后我们将resolve换成reject

const promise1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject(1);
      }, 1000);
    });
    setTimeout(() => {
      console.log(promise1);
    }, 1000);

它得到的便是拒绝(rejected)状态,同时给你抛出了一个错误

基本使用

Promise构造函数只有一个函数作为参数,这个函数会在一个Promise被实例化出来后会被立即执行

 new Promise((resolve, reject) => {
      console.log(1);
    });
    console.log(2);

此时输出的结果是:1 2

Promise接收的函数有两个参数,分别是resolve和reject,其中resolve代表一切正常的时候所调用的函数,reject则代表我们程序异常的时候所调用的函数。resolve函数传入的参数用于向下一个then传递一个值,而reject函数传入的参数则会被.catch捕捉。而Promise.finally则是在Promise状态完成后触发的一个回调,即无论是resolve还是reject都会触发

    //成功示例
    new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("成功的值");
      });
    })
      .then((res) => {
        console.log(res); //成功的值
      })
      .catch((err) => {
        //不会触发
        console.log(err);
      })
      .finally(() => {
        console.log("end"); //end
      });

    //失败示例
    new Promise((resolve, reject) => {
      setTimeout(() => {
        reject("失败的的值");
      });
    })
      .then((res) => {
        //不会触发
        console.log(res);
      })
      .catch((err) => {
        console.log(err); ////失败的值
      })
      .finally(() => {
        console.log("end"); //end
      });

以上便是Promise的基本使用,但是只掌握它的基本使用可不行,我们还需要对其更深入的研究

链式调用

当我们使用Promise的时候,只要我们在.then的回调函数中返回一个成功状态(resolve)的Promise,则在下一个.then的回调函数中便可获取到这个成功函数(resolve)的参数,基于这个特性便有了Promise的链式调用。

    new Promise((resolve, reject) => {
      //这里一般会有一个网络请求或其它异步操作
        resolve("成功的值1");
    })
      .then((res) => {
        console.log(res); //成功的值1
        return new Promise((resolve, reject) => {
        //这里一般会有一个网络请求或其它异步操作
          resolve("成功的值2");
        });
      })
      .then((res) => {
        console.log(res); //成功的值2
        return new Promise((resolve, reject) => {
          //这里一般会有一个网络请求或其它异步操作
          resolve("成功的值3");
        });
      })
      .then((res) => {
        console.log(res); //成功的值3
        //以此类推...
      });

我们可以对其进行简写,比如

    new Promise((resolve, reject) => {
      //这里一般会有一个网络请求或其它异步操作
      resolve("成功的值");
    });

可以简写为

Promise.resolve('成功的值')

所以我们的链式调用可以简写为

    new Promise((resolve, reject) => {
      //这里一般会有一个网络请求或其它异步操作
      resolve("成功的值1");
    })
      .then((res) => {
        console.log(res); //成功的值1
        return Promise.resolve("成功的值2");
      })
      .then((res) => {
        console.log(res); //成功的值2
        return Promise.resolve("成功的值3");
      })
      .then((res) => {
        console.log(res); //成功的值3
        //以此类推...
      });

同样的reject的简写方式也和resolve一样

    new Promise((resolve, reject) => {
      //这里一般会有一个网络请求或其它异步操作
      reject("失败的值");
    });
    
    //简写为
    Promise.reject('失败的值')

一般我们在实际项目中一般会这样写

      ...
      //网络请求中获取到数据后
      if(xxx){
        //成功
        return Promise.resolve('请求的值')
      }
      return Promise.reject('失败原因')
      ...

其实.then中也会自动返回Promise的封装,也就是说这个链式调用我们可以直接这样写

    new Promise((resolve, reject) => {
      //这里一般会有一个网络请求或其它异步操作
      resolve("成功的值1");
    })
      .then((res) => {
        console.log(res); //成功的值1
        return "成功的值2";
      })
      .then((res) => {
        console.log(res); //成功的值2
        return "成功的值3";
      })
      .then((res) => {
        console.log(res); //成功的值3
        //以此类推...
      });

以上便是Promise的链式调用,Promise的链式调用一般用于这些步骤间有先后顺序的操作,比如开头举的例子,需要使用前一个接口请求的数据作为参数去请求另一个接口的情形。

Promise中的all函数

在实际项目中你是否遇到过这样一个情况:你有A、B、C三个接口(或则更多),C接口的参数需要用到A和B两个接口的结果值,此时你为怎么做?

  • 做法1

先请求A接口再请求B接口最后再根据AB接口的结果去请求C接口

    new Promise((resolve, reject) => {
      //请求A接口,这里用setTimeout模拟请求
      setTimeout(() => {
        resolve("A的结果");
      }, 100);
    })
      .then((res) => {
        //根据A结果请求B接口
        setTimeout(() => {
          return "B的请求结果";
        }, 100);
      })
      .then((res) => {
        //根据A和B结果请求C接口
        setTimeout(() => {
          console.log("C的请求结果");
        }, 100);
      })
      .catch((err) => {
        //这里暂不做错误考虑
      });

这种写法逻辑上是没问题的,但是B和A的请求之间是完全没有交集的,而浏览器的http请求是可以同时发起多个请求的,所以这种写法很明显增加了接口请求时间

  • 做法2

在每个请求结束后都去调用请求C的函数,在这个函数中判断两个请求的数据是否都获取到了,然后再进行处理

    let isResultA = false;
    let isResultB = false;

    //请求A接口,这里用setTimeout模拟请求
    setTimeout(() => {
      isResultA = true;
      getC()
    }, 100);

    //请求B接口,这里用setTimeout模拟请求
    setTimeout(() => {
      isResultB = true;
      getC()
    }, 100);
    function getC() {
      if (isResultA && isResultB) {
        //根据A和B的结果请求C接口数据
        setTimeout(() => {
          console.log("C的请求结果");
        }, 100);
      }
    }

很显然这种在写法上是很麻烦的,所以Promise提供了all方法

  • 做法3

Promise.all接收一个iterable类型(Array,Map,Set 都属于 ES6 的 iterable 类型),可以放多个Promise实例,最后.then中获得的是这些输入的Promise的resolve回调的结果数组。同时只要任何一个输入的Promise的reject回调执行或者输入不合法的Promise就会立即抛出错误

    Promise.all([
      new Promise((resolve, reject) => {
        //请求A接口,这里用setTimeout模拟请求
        setTimeout(() => {
          resolve("A的结果");
        }, 2000);
      }),
      new Promise((resolve, reject) => {
        //请求B接口,这里用setTimeout模拟请求
        setTimeout(() => {
          resolve("B的结果");
        }, 1000);
      }),
    ])
      .then((res) => {
        console.log(res[0]); //A的结果
        console.log(res[1]); //B的结果
        //根据A和B的结果请求C接口数据
        setTimeout(() => {
          console.log("C的请求结果");
        }, 100);
      })
      .catch((err) => {
        console.log(err);
      });

Promise中的race函数

Promise.race方法返回一个promise,一旦迭代器中的某个promise完成,返回的promise就会被完成。简单来说就是它接收的promise实例中谁快就用谁的结果,不管你的结果是resove的还是reject

    Promise.race([
      new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve("结果1");
        }, 1000);
      }),
      new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve("结果2");
        }, 500);
      }),
      new Promise((resolve, reject) => {
        //请求B接口,这里用setTimeout模拟请求
        setTimeout(() => {
          reject("结果3");
        }, 100);
      }),
    ])
      .then((res) => {
        //不会触发
        console.log(res);
      })
      .catch((err) => {
        console.log(err); //结果3
      });

上面示例很显然第三个Promise示例最先返回结果,所以Promise.race便使用了第三个Promise的结果

Promise中的any函数

Promise.any函数它也接收一个Promise实例的可迭代对象,只要其中的一个promise实例成功,就返回那个已经成功的promise,只有所有的promise实例都失败才会返回失败的(reject)的数组

    Promise.any([
      new Promise((resolve, reject) => {
        setTimeout(() => {
          reject("结果1");
        }, 1000);
      }),
      new Promise((resolve, reject) => {
        setTimeout(() => {
          reject("结果2");
        }, 500);
      }),
      new Promise((resolve, reject) => {
        //请求B接口,这里用setTimeout模拟请求
        setTimeout(() => {
          reject("结果3");
        }, 100);
      }),
    ])
      .then((res) => {
        //不会触发
        console.log(res);
      })
      .catch((err) => {
        console.log(err); //AggregateError: All promises were rejected
      });

这个函数适用的场景可能不是很多,在这里我大概想到的一个场景就是:有三个接口A,B,C,这三个接口很不稳定但是它们返回的成功结果都一样,所以我们需要对这三个接口进行同时请求,只要它们其中有一个接口返回成功,那么我们便用这个接口的值。所以这三个接口只要有一个可用我们便可拿到想要的结果

async和await

async和await其实就是promise的语法糖形式,它可以让我们的异步代码包装成同步的形式理解。await顾名思义就是等待的意思,它必须使用在一个async的函数中,await后面跟的是一个实例化Promise,它返回的值则是这个Promise成功返回的 resolve 状态值。其实它的用法很简单,如下

      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve("结果");
        }, 1000);
      });
    };

    const getData = async () => {
      const res = await promiseFun();
      console.log(res);//结果
    };
    getData();

如果我们把文章开头的axios请求例子改为async,await的形式它将会是这个样子

    const getAxiosData = async () => {
      try {
        const res1 = await axios.get(url1, {});
        const res2 = await axios.get(url2, { query: res1.xxx });
        const res3 = await axios.get(url2, { query: res2.xxx });
        console.log(res3);
      } catch (err) {
        console.log(err);
      }
    };
    getAxiosData();

此时的代码逻辑看起来就会清晰很多

写在最后

Promise的大致用法基本也就介绍完了,其实Promise还涉及到另一个方面的知识事件循环(Event Loop) 还有宏任务微任务等,由于篇幅原因,这部分我会抽时间单独写一篇关于这方面的文章。同时如果你发现文中有错误或不妥的地方欢迎指出,一定及时修改,感谢~

创作不易,你的点赞就是我的动力!如果感觉这篇文章对你有所帮助的话就请点个赞吧,感谢orz

有关ES6 Promise详解的更多相关文章

  1. 使用canal同步MySQL数据到ES - 2

    文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co

  2. ES基础入门 - 2

    ES一、简介1、ElasticStackES技术栈:ElasticSearch:存数据+搜索;QL;Kibana:Web可视化平台,分析。LogStash:日志收集,Log4j:产生日志;log.info(xxx)。。。。使用场景:metrics:指标监控…2、基本概念Index(索引)动词:保存(插入)名词:类似MySQL数据库,给数据Type(类型)已废弃,以前类似MySQL的表现在用索引对数据分类Document(文档)真正要保存的一个JSON数据{name:"tcx"}二、入门实战{"name":"DESKTOP-1TSVGKG","cluster_name":"elasticsear

  3. 物联网MQTT协议详解 - 2

    一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su

  4. Tcl脚本入门笔记详解(一) - 2

    TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是

  5. 【详解】Docker安装Elasticsearch7.16.1集群 - 2

    开门见山|拉取镜像dockerpullelasticsearch:7.16.1|配置存放的目录#存放配置文件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/config#存放数据的文件夹mkdir-p/opt/docker/elasticsearch/node-1/data#存放运行日志的文件夹mkdir-p/opt/docker/elasticsearch/node-1/log#存放IK分词插件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/plugins若你使用了moba,直接右键新建即可如上图所示依次类推创建

  6. 【Elasticsearch基础】Elasticsearch索引、文档以及映射操作详解 - 2

    文章目录概念索引相关操作创建索引更新副本查看索引删除索引索引的打开与关闭收缩索引索引别名查询索引别名文档相关操作新建文档查询文档更新文档删除文档映射相关操作查询文档映射创建静态映射创建索引并添加映射概念es中有三个概念要清楚,分别为索引、映射和文档(不用死记硬背,大概有个印象就可以)索引可理解为MySQL数据库;映射可理解为MySQL的表结构;文档可理解为MySQL表中的每行数据静态映射和动态映射上面已经介绍了,映射可理解为MySQL的表结构,在MySQL中,向表中插入数据是需要先创建表结构的;但在es中不必这样,可以直接插入文档,es可以根据插入的文档(数据),动态的创建映射(表结构),这就

  7. 最强Http缓存策略之强缓存和协商缓存的详解与应用实例 - 2

    HTTP缓存是指浏览器或者代理服务器将已经请求过的资源保存到本地,以便下次请求时能够直接从缓存中获取资源,从而减少网络请求次数,提高网页的加载速度和用户体验。缓存分为强缓存和协商缓存两种模式。一.强缓存强缓存是指浏览器直接从本地缓存中获取资源,而不需要向web服务器发出网络请求。这是因为浏览器在第一次请求资源时,服务器会在响应头中添加相关缓存的响应头,以表明该资源的缓存策略。常见的强缓存响应头如下所述:Cache-ControlCache-Control响应头是用于控制强制缓存和协商缓存的缓存策略。该响应头中的指令如下:max-age:指定该资源在本地缓存的最长有效时间,以秒为单位。例如:Ca

  8. IDEA 2022 创建 Spring Boot 项目详解 - 2

    如何用IDEA2022创建并初始化一个SpringBoot项目?目录如何用IDEA2022创建并初始化一个SpringBoot项目?0. 环境说明1.  创建SpringBoot项目 2.编写初始化代码0. 环境说明IDEA2022.3.1JDK1.8SpringBoot1.  创建SpringBoot项目        打开IDEA,选择NewProject创建项目。        填写项目名称、项目构建方式、jdk版本,按需要修改项目文件路径等信息。        选择springboot版本以及需要的包,此处只选择了springweb。        此处需特别注意,若你使用的是jdk1

  9. 详解Unity中的粒子系统Particle System (二) - 2

    前言上一篇我们简要讲述了粒子系统是什么,如何添加,以及基本模块的介绍,以及对于曲线和颜色编辑器的讲解。从本篇开始,我们将按照模块结构讲解下去,本篇主要讲粒子系统的主模块,该模块主要是控制粒子的初始状态和全局属性的,以下是关于该模块的介绍,请大家指正。目录前言本系列提要一、粒子系统主模块1.阅读前注意事项2.参考图3.参数讲解DurationLoopingPrewarmStartDelayStartLifetimeStartSpeed3DStartSizeStartSize3DStartRotationStartRotationFlipRotationStartColorGravityModif

  10. 关于ES集群信息的一些查看 - 2

    文章目录查看ES信息查看节点信息查看分片信息实际场景下ES分片及副本数量应该怎么分关于ES的灵活使用查看ES信息查看版本kibana:GET/查看节点信息GET/_cat/nodes?v解释:ip:集群中节点的ip地址;heap.percent:堆内存的占用百分比;ram.percent:总内存的占用百分比,其实这个不是很准确,因为buff/cache和available也被当作使用内存;cpu:cpu占用百分比;load_1m:1分钟内cpu负载;load_5m:5分钟内cpu负载;load_15m:15分钟内cpu负载;node.role:上图的dilmrt代表全部权限master:*代表

随机推荐