JavaScript异步编程——08-Promise的链式调用【万字长文,感谢支持】

前言

实际开发中,我们经常需要先后请求多个接口:发送第一次网络请求后,等待请求结果;有结果后,然后发送第二次网络请求,等待请求结果;有结果后,然后发送第三次网络请求。以此类推。

比如说:在请求完接口 1 的数据data1之后,需要根据data1的数据,继续请求接口 2,获取data2;然后根据data2的数据,继续请求接口 3。换而言之,现在有三个网络请求,请求 2 必须依赖请求 1 的结果,请求 3 必须依赖请求 2 的结果。

如果按照往常的写法,会有三层回调,陷入“回调地狱”的麻烦。

这种场景其实就是接口的多层嵌套调用,在前端的异步编程开发中,经常遇到。有了 Promise 以及更高级的写法之后,我们可以把多层嵌套调用按照线性的方式进行书写,非常优雅。也就是说:Promise 等ES6的写法可以把原本的多层嵌套写法改进为链式写法

我们来对比一下嵌套写法和链式调用的写法,你会发现后者的非常优雅。

Promise 链式调用:封装多次网络请求

ES5 中的传统嵌套写法

伪代码举例:

 // 封装 ajax 请求:传入请求地址、请求参数,以及回调函数 success 和 fail。function requestAjax(url, params, success, fail) {var xhr = new xhrRequest();// 设置请求方法、请求地址。请求地址的格式一般是:'https://api.example.com/data?' + 'key1=value1&key2=value2'xhr.open('GET', url);// 设置请求头(如果需要)xhr.setRequestHeader('Content-Type', 'application/json');xhr.send();xhr.onreadystatechange = function () {if (xhr.readyState === 4 && xhr.status === 200) {success && success(xhr.responseText);} else {fail && fail(new Error('接口请求失败'));}};}​// ES5的传统写法,执行 ajax 请求,层层嵌套requestAjax('https://api.qianguyihao.com/url_1', params_1,res1 => {console.log('第一个接口请求成功:' + JSON.stringify(res1));// ajax嵌套调用requestAjax('https://api.qianguyihao.com/url_2', params_2, res2 => {console.log('第二个接口请求成功:' + JSON.stringify(res2));// ajax嵌套调用requestAjax('https://api.qianguyihao.com/url_3', params_3, res3 => {console.log('第三个接口请求成功:' + JSON.stringify(res3));});});},(err1) => {console.log('qianguyihao 请求失败:' + JSON.stringify(err1));});

上面的代码层层嵌套,可读性很差,而且出现了我们常说的回调地狱问题。

Promise 的嵌套写法

改用 ES6 的 Promise 之后,写法上会稍微改进一些。代码举例如下:

 // 【公共方法层】封装 ajax 请求的伪代码。传入请求地址、请求参数,以及回调函数 success 和 fail。function requestAjax(url, params, success, fail) {var xhr = new xhrRequest();// 设置请求方法、请求地址。请求地址的格式一般是:'https://api.example.com/data?' + 'key1=value1&key2=value2'xhr.open('GET', url);// 设置请求头(如果需要)xhr.setRequestHeader('Content-Type', 'application/json');xhr.send();xhr.onreadystatechange = function () {if (xhr.readyState === 4 && xhr.status === 200) {success && success(xhr.responseText);} else {fail && fail(new Error('接口请求失败'));}};}​// 【model层】将接口请求封装为 Promisefunction requestData1(params_1) {return new Promise((resolve, reject) => {requestAjax('https://api.qianguyihao.com/url_1', params_1, res => {// 这里的 res 是接口返回的数据。返回码 retCode 为 0 代表接口请求成功。if (res.retCode == 0) {// 接口请求成功时调用resolve('request success' + res);} else {// 接口请求异常时调用reject({ retCode: -1, msg: 'network error' });}});});}​​// requestData2、requestData3的写法与 requestData1类似。他们的请求地址、请求参数、接口返回结果不同,所以需要挨个单独封装 Promise。function requestData2(params_2) {return new Promise((resolve, reject) => {requestAjax('https://api.qianguyihao.com/url_2', params_2, res => {if (res.retCode == 0) {resolve('request success' + res);} else {reject({ retCode: -1, msg: 'network error' });}});});}​function requestData3(params_3) {return new Promise((resolve, reject) => {requestAjax('https://api.qianguyihao.com/url_3', params_3, res => {if (res.retCode == 0) {resolve('request success' + res);} else {reject({ retCode: -1, msg: 'network error' });}});});}​// 【业务层】Promise 调接口的嵌套写法。温馨提示:这段代码在接下来的学习中,会被改进无数次。// 发送第一次网络请求requestData1(params_1).then(res1 => {console.log('第一个接口请求成功:' + JSON.stringify(res1));​// 发送第二次网络请求requestData1(params_2).then(res2 => {console.log('第二个接口请求成功:' + JSON.stringify(res2));​// 发送第三次网络请求requestData1(params_3).then(res3 => {console.log('第三个接口请求成功:' + JSON.stringify(res3));})})})

上方代码非常经典。在真正的实战中,我们往往需要嵌套请求多个不同的接口,它们的接口请求地址、要处理的 resolve 和 reject 的时机、业务逻辑往往是不同的,所以需要分开封装不同的 Promise 实例。也就是说,如果要调三个不同的接口,建议单独封装三个不同的 Promise 实例:requestData1、requestData2、requestData3。

这三个 Promise 实例,最终都需要调用底层的公共方法 requestAjax()。每个公司都有这样的底层方法,里面的代码会做一些公共逻辑,比如:封装原生的 ajax请求,用户登录态的校验等等;如果没有这种公共方法,你就自己写一个,为组织做点贡献。

但是,细心的你可能会发现:上面的最后10行代码仍然不够优雅,因为 Promise 在调接口时出现了嵌套的情况,实际开发中如果真这么写的话,是比较挫的,阅读性非常差,我不建议这么写。要怎么改进呢?这就需要用到 Promise 的链式调用

Promise 的链式调用写法(重要)

针对多个不同接口的嵌套调用,采用 Promise 的链式调用写法如下:(将上方代码的最后10行,改进如下)

 requestData1(params_1).then(res1 => {console.log('第一个接口请求成功:' + JSON.stringify(res1));// 【关键代码】继续请求第二个接口。如果有需要,也可以把 res1 的数据传给 requestData2()的参数return requestData2(res1);}).then(res2 => {console.log('第二个接口请求成功:' + JSON.stringify(res2));// 【关键代码】继续请求第三个接口。如果有需要,也可以把 res2 的数据传给 requestData3()的参数return requestData3(res2);}).then(res3 => {console.log('第三个接口请求成功:' + JSON.stringify(res3));}).catch(err => {console.log(err);})

上面代码中,then 是可以链式调用的,一旦 return 一个新的 Promise 实例之后,后面的 then() 就可以作为这个新 Promise 在成功后的回调函数。这种扁平化的写法,更方便维护,可读性更好;并且可以更好的管理请求成功和失败的状态。

这段代码很经典,你一定要多看几遍,多默写几遍,倒背如流也不过分。如果你平时的异步编程代码能写到这个水平,说明你对 Promise 已经入门了,因为绝大多数人都是用的这个写法。

其实还有更高级、更有水平的写法,那就是用生成器、用 async ... await 来写Promise的链式调用,也就是改进上面的十几行代码。你把它掌握了,编程水平才能更上一层楼。我们稍后会讲。

Promise 链式调用举例:封装 Node.js 的回调方法

代码结构与上面的类似,这里仅做代码举例,不再赘述。

传统写法:

 fs.readFile(A, 'utf-8', function (err, data) {fs.readFile(B, 'utf-8', function (err, data) {fs.readFile(C, 'utf-8', function (err, data) {console.log('qianguyihao:' + data);});});});

上方代码多层嵌套,存在回调地狱的问题。

Promise 写法:

 function read(url) {return new Promise((resolve, reject) => {fs.readFile(url, 'utf8', (err, data) => {if (err) reject(err);resolve(data);});});}​read(A).then((data) => {return read(B);}).then((data) => {return read(C);}).then((data) => {console.log('qianguyihao:' + data);}).catch((err) => {console.log(err);});

用 async ... await 封装链式调用

前面讲的 Promise 链式调用是用 then().then().then() 这种写法。其实我们还可以用更高级的写法,也就是用生成器、用 async ... await 改写那段代码。改进之后,代码写起来非常简洁。

在学习这段内容之前,你需要先去《JavaScript进阶/迭代器和生成器》那篇文章里去学习迭代器、生成器相关的知识。生成器是一种特殊的迭代器,async ... await 是生成器的语法糖。

用生成器封装链式调用

代码举例:

 // 封装 Promise 链式请求function* getData(params_1) {// 【关键代码】const res1 = yield requestData1(params_1);const res2 = yield requestData2(res1);const res3 = yield requestData3(res2);}​// 调用 Promise 链式请求const generator = getData(params_1);​generator.next().value.then(res1 => {generator.next(res1).value.then(res2 => {generator.next(res2).value.then(res3 => {generator.next(res3);})})})

生成器在执行时,是分阶段执行的,每次遇到 next()方法后就会执行一个阶段,遇到 yield 就会结束当前阶段的执行并暂停。 上方代码中,yield 后面的内容是当前阶段产生的 Promise 对象;yield 前面的内容是要传递给下一个阶段的参数。

用 async ... await 封装链式调用(重要)

上面的生成器代码有些晦涩难懂,实际开发中,通常不会这么写。我们更喜欢用 async ... await 语法封装 Promise 的链式调用。async ... await 是属于生成器的语法糖,写起来更简洁直观、更容易理解。

代码举例:

 // 封装:用 async ... await 调用 Promise 链式请求async function getData() {const res1 = await requestData1(params_1);const res2 = await requestData2(res1);const res3 = await requestData3(res2);}​getData();

代码解释:requestData1()、requestData2()、requestData3() 这三个函数都是一个Promise对象,其内部封装的代码写法已经在前面「Promise 的嵌套写法」这一小段中讲过了。

上面的代码非常简洁。实际开发中也经常用到,非常实用。暂时我们先记住用法,下一章我们会学习 async ... await 的详细知识。

链式调用,如何处理任务失败的情况

在链式调用多个异步任务的Promise时,如果中间有一个任务失败或者异常,要怎么处理呢?是继续往下执行?还是停止执行,直接抛出异常?这取决于你的业务逻辑是怎样的。

常见的处理方案有以下几种,你可以根据具体情况按需选择。

统一处理失败的情况,不继续往下走

针对 a、b、c 这三个请求,不管哪个请求失败,我都希望做统一处理。这种代码要怎么写呢?我们可以在最后面写一个 catch。

由于是统一处理多个请求的异常,所以只要有一个请求失败了,就会马上走到 catch,剩下的请求就不会继续执行。比如说:

  • a 请求失败:然后会走到 catch,不执行 b 和 c

  • a 请求成功,b 请求失败:然后会走到 catch,不执行 c。

代码举例如下:

 getPromise('a.json').then((res) => {console.log(res);return getPromise('b.json'); // 继续请求 b}).then((res) => {// b 请求成功console.log(res);return getPromise('c.json'); // 继续请求 c}).then((res) => {// c 请求成功console.log('c:success');}).catch((err) => {// 统一处理请求失败console.log(err);});

中间的任务失败后,如何继续往下走?

在多个Promise的链式调用中,如果中间的某个Promise 执行失败,还想让剩下的其他 Promise 顺利执行的话,那就请在中间那个失败的Promise里加一个失败的回调函数(可以写到then函数的第二个参数里,也可以写到catch函数里)。捕获异常后,便可继续往下执行其他的Promise。

代码举例:

const promise1 = new Promise((resolve, reject) => {resolve('qianguyihao fulfilled 1');
});const promise2 = new Promise((resolve, reject) => {reject('qianguyihao rejected 2');
});const promise3 = new Promise((resolve, reject) => {resolve('qianguyihao fulfilled 3');
});promise1.then(res => {console.log('res1:', res);// return 一个 失败的 Promisereturn promise2;}).then(res => {console.log('res2:', res);return promise3;}, err => {// 如果 promise2 为失败状态,可以通过 then() 的第二个参数(即失败的回调函数)捕获异常,然后就可以继续往下执行其他 Promiseconsole.log('err2:', err);// 关键代码:即便 promise2 失败了,也要继续执行 Promise3return promise3;}).then(res => {console.log('res3', res);}, err => {console.log('err3:', err);});

打印结果:

res1: qianguyihao fulfilled 1
err2: qianguyihao rejected 2
res3 qianguyihao fulfilled 3

上方代码中,我们单独处理了 promise2 失败的情况。不管promise2 成功还是失败,我们都想让后续的 promise3 正常执行。

希望各位可以点个赞点个关注,这对up真的很重要,谢谢大家啦!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/10513.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【ARM 嵌入式 C 入门及渐进 1.2 -- 是否为 n 字节对齐】

文章目录 是否为 n 字节对齐 是否为 n 字节对齐 在C语言中,你可以定义一个宏来检查一个给定地址是否是n字节对齐的。这里的n应该是2的幂(例如,2、4、8、16等)。要做到这一点,可以利用位操作的特性。具体地&#xff0c…

品鉴中的平衡之美:如何欣赏红酒的口感与风格和谐

品鉴云仓酒庄雷盛红酒的过程,是对其口感与风格和谐的追求和欣赏。平衡是红酒品质的重要标志之一,它体现在红酒的色泽、香气、口感和余味等多个方面。通过欣赏红酒的平衡之美,我们可以更好地领略其精妙之处,感受其带来的美妙滋味。…

【Unity Animation 2D】Unity Animation 2D骨骼绑定与动画制作

一、图片格式为png格式,并且角色各部分分离 图片参数设置 需要将Sprite Mode设置为Single,否则图片不能作为一个整体 1、创建骨骼 1.1 旋转Create Bone,点击鼠标左键确定骨骼位置,移动鼠标再次点击鼠标左键确定骨骼&#xff0c…

大数据面试题第一期*4

题1、HDFS存储机制 (1)客户端向namenode请求上传文件 ,namenode检查目标文件是否已存在 ,父目录是否存在。 (2)namenode返回是否可以上传。 (3)客户端请求第一个 block上传到哪几个d…

Docker in Docker 的原理与实践

概述 Docker in Docker(DinD)是一个让 Docker 容器内可以运行另一个 Docker 沙箱环境的技术。常用于持续集成(CI)工作流程,其中需要构建和推送 Docker 镜像,而不污染主宿主机的 Docker 环境。 Docker in …

JS_jq选择器合集

基本选择器 选择器返回示范描述#id单个元素$("#test").class 集合元素 $(".test")element集合元素$("p") 所有p原素*集合元素$("*")所有原素select1,select2,selectn集合元素$("div,span,p.myClass")所有匹配原素 层次选择…

嵌入式C语言高级教程:实现基于STM32的智能健康监测手环

智能健康监测手环能够实时监控用户的生理参数,如心率、体温和活动量,对于健康管理和疾病预防非常有帮助。本教程将指导您如何在STM32微控制器上实现一个基本的智能健康监测手环。 一、开发环境准备 硬件要求 微控制器:STM32L476RG&#xf…

RS3236-3.3YUTDN4功能和参数介绍及PDF资料

RS3236-3.3YUTDN4功能和参数介绍及PDF资料-公司新闻-配芯易-深圳市亚泰盈科电子有限公司 品牌: RUNIC(润石) 封装: XDFN-4-EP(1x1) 描述: 带过温保护 输出类型: 固定 最大输入电压: 7.5V 输出电压: 3.3V 最大输出电流: 500mA RS3236-3.3YUTDN4 是一款低压差线性稳压器&#x…

kkfileview部署踩坑记录——kkfile部署启动失败、预览出错、乱码问题的处理

预览失败:报错 org.jodconverter.core.office.OfficeException: Could not store document: testdoc.pdf java.lang.NullPointerException: Could not open document: 1691539546735.docx 错误原因 由于kkfileView在linux上默认使用openOffice来实现转换 解决方…

leetcode 2316.统计无向图中无法互相到达点对数

思路:并查集 其实就是连通块的一个变形题目,一般的连通块题目要我们求的是连通个数,或者能不能到达,这里反过来问了。 首先,我们用dfs也是可以做到的,在dfs中统计每一个连通块的个数,然后用乘…

Spring原理分析--获取Environment资源对象

1.使用getEnvironment()获取环境信息 ApplicationContext接口继承了EnvironmentCapable接口,可以通过getEnvironment()获取Environment配置信息,例如: SpringBootApplication public class A01 {public static void main(String[] args) th…

解决方案:对数据进行负采样随机抽取1000W,用Pandas如何实现

文章目录 一、现象二、解决方案 一、现象 做建模的时候,有时候需要对数据进行负采样,就需要随机抽取数据,之前用SQL实现过order by rand(),附上链接解决方案:用户号出现多行,如何从中取其一并随机抽取100个…

PX4FMU和PX4IO最底层启动过程分析(下)

PX4FMU和PX4IO最底层启动过程分析(下) PX4FMU的系统启动函数为nash_main(int argc,char *argv[]) PX4IO的系统启动函数为nash_start(int argc,char *argv[]) PX4FMU启动函数nash_main(int argc,char *argv[]) 首先分析一下nash_main(int argc,char *a…

高效视频剪辑:视频批量调色,如何利用色调调整提升效率

在视频剪辑的后期处理中,调色是一个至关重要的环节。它不仅能够改变视频的整体氛围和风格,还能够突出视频的重点,增强观众的视觉体验。然而,对于大量的视频素材进行逐个调色处理,无疑会耗费大量的时间和精力。我们可以…

软件安装及YOLOv8环境配置及验证

先附上本章中所用到的软件及环境安装包,还有YOLOv8各任务权重: 软件及环境配置链接:https://pan.baidu.com/s/1-n2HJybicA6vW1YXfGRtcA 提取码:6vh8 YOLOv8各权重:链接:https://pan.baidu.com/s/1ApYUrJ_s…

C++相关概念和易错语法(12)(迭代器、string容量调整)

1.迭代器(以string为例) (1)基本理解:在我们刚接触迭代器的时候,我们可以将迭代器理解为改造过的“指针”,这是一个新的类型,指向对应容器中的各个元素。我们可以像指针那样对迭代器…

Lombok介绍、使用方法和安装

目录 1 Lombok背景介绍 2 Lombok使用方法 2.1 Data 2.2 Getter/Setter 2.3 NonNull 2.4 Cleanup 2.5 EqualsAndHashCode 2.6 ToString 2.7 NoArgsConstructor, RequiredArgsConstructor and AllArgsConstructor 3 Lombok工作原理分析 4. Lombok的优缺点 5. 总结 1 …

Idea入门:一分钟创建一个Java工程

一,新建一个Java工程 1,启动Idea后,选择 [New Project] 2,完善工程信息 填写工程名称,根据实际用途取有意义的英文名称选择Java语言,可以看到还支持Kotlin、Javascript等语言选择包管理和项目构建工具Mav…

Java中使用Comparable接口实现自然排序

Java中使用Comparable接口实现自然排序 在Java中,当我们需要对自定义对象进行排序时,Comparable接口就派上了大用场。Comparable接口定义了一个名为compareTo的方法,该方法用于比较两个对象。通过实现Comparable接口,我们可以定义…

力扣110:平衡二叉树

作者介绍:10年大厂数据\经营分析经验,现任大厂数据部门负责人。 会一些的技术:数据分析、算法、SQL、大数据相关、python 欢迎加入社区:码上找工作 作者专栏每日更新: LeetCode解锁1000题: 打怪升级之旅 python数据分析…