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真的很重要,谢谢大家啦!
相关文章:

JavaScript异步编程——08-Promise的链式调用【万字长文,感谢支持】
前言 实际开发中,我们经常需要先后请求多个接口:发送第一次网络请求后,等待请求结果;有结果后,然后发送第二次网络请求,等待请求结果;有结果后,然后发送第三次网络请求。以此类推。…...

现代制造之数控机床篇
现代制造 有现代技术支撑的制造业,即无论是制造还是服务行业,添了现代两个字不过是因为有了现代科学技术的支撑,如发达的通信方式,不断发展的互联网,信息化程度加强了,因此可以为这两个行业增加了不少优势…...

Rust的协程机制:原理与简单示例
在现代编程中,协程(Coroutine)已经成为实现高效并发的重要工具。Rust,作为一种内存安全的系统编程语言,也采用了协程作为其并发模型的一部分。本文将深入探讨Rust协程机制的实现原理,并通过一个简单的示例来…...

学习成长分享-以近红外光谱分析学习为例
随着国家研究生招生规模的扩大,参与或接触光谱分析方向的研究生日益增多,甚至有部分本科生的毕业设计也包含以近红外光谱分析内容。基于对近红外光谱分析的兴趣,从2018年开始在CSDN博客(陆续更新自己学习的浅显认识,到…...

Linux makefile进度条
语法 在依赖方法前面加上就不会显示这一行的命令 注意 1.make 会在当前目录下找名为“makefile” 或者 “Makefile” 的文件 2.为了生成第一依赖文件,如果依赖文件列表有文件不存在,则会到下面的依赖关系中查找 3..PHONY修饰的依赖文件总是被执行的 …...

Ollama 可以设置的环境变量
Ollama 可以设置的环境变量 0. 引言1. Ollama 可以设置的环境变量 0. 引言 在Ollama的世界里,环境变量如同神秘的符文,它们是控制和定制这个强大工具的关键。通过精心设置这些环境变量,我们可以让Ollama更好地适应我们的需求,就像…...

基于Python+Django+MySQL实现Web版的增删改查
Python Web框架Django连接和操作MySQL数据库学生信息管理系统(SMS),主要包含对学生信息增删改查功能,旨在快速入门Python Web。 开发环境 开发工具:Pycharm 2020.1开发语言:Python 3.8.0Web框架:Django 3.0.6数据库:…...

Map、Set和Object的区别
Set ES6提供了新的数据结构Set,类似于数组,但成员值是唯一的,没有重复的值 Set本身是一个构造函数(要 new),用来生成Set数据结构 Set 对象允许你储存任何类型的唯一值,无论是原始值或者是对象引用 每个值在 Set 中…...

Web 安全之盗链(Hotlinking)攻击详解
目录 什么是盗链 盗链原理 盗链类型 盗链的危害 如何发现盗链 盗链防范措施 法律法规与应对策略 小结 盗链(Hotlinking)攻击,作为互联网安全领域的一个重要话题,涉及到侵犯版权、滥用资源和网络安全等多个层面。盗链现象普…...
leetcode算法笔记-算法复杂度
对于时间复杂度,主要包括三种情况: 渐进紧确界: O渐进上界: 渐进下界: 加法原则:不同的时间复杂度相加取阶数最高的 乘法原则:不同的时间复杂度相乘,结果为时间复杂度的乘积 阶乘…...

推荐算法详解
文章目录 推荐算法引言基于内容的推荐原理算法步骤注意点可以优化的地方示例代码讲解 协同过滤推荐原理算法步骤注意点可以优化的地方示例代码讲解 混合推荐系统原理算法步骤注意点可以优化的地方示例1代码讲解1示例2代码讲解2 基于知识的推荐原理算法步骤注意点可以优化的地方…...

Java找不到包解决方案
在跟着教程写Spingboot后端项目时,为了加快效率,有时候有的实体文件可以直接粘贴到目录中,此时运行项目会出现Java找不到包的情况,即无法找到导入的实体文件,这是项目没有更新的原因。解决方法: 刷新Maven:…...

vue的css深度选择器 deep /deep/
作用及概念 当 <style> 标签有 scoped 属性时,它的 CSS 只作用于当前组件中的元素,父组件的样式将不会渗透到子组件。在vue中是这样描述的: 处于 scoped 样式中的选择器如果想要做更“深度”的选择,也即:影响到子…...

2024年华为OD机试真题-计算三叉搜索树的高度-(C++)-OD统一考试(C卷D卷)
题目描述: 定义构造三叉搜索树规则如下: 每个节点都存有一个数,当插入一个新的数时,从根节点向下寻找,直到找到一个合适的空节点插入。 查找的规则是: 1. 如果数小于节点的数减去500,则将数插入节点的左子树 2. 如果数大于节点的数加上500,则将…...

# ERROR: node with name “rabbit“ already running on “MS-ITALIJUXHAMJ“ 解决方案
ERROR: node with name “rabbit” already running on “MS-ITALIJUXHAMJ” 解决方案 一、问题描述: 1、启动 rabbitmq-server.bat 服务时,出错 Error 2、查询 rabbitmqctl status 状态时,出错 Error 3、停止 rabbitmqctl stop 服务时&a…...

class常量池、运行时常量池和字符串常量池详解
类常量池、运行时常量池和字符串常量池这三种常量池,在Java中扮演着不同但又相互关联的角色。理解它们之间的关系,有助于深入理解Java虚拟机(JVM)的内部工作机制,尤其是在类加载、内存分配和字符串处理方面。 类常量池…...

Meilisearch使用过程趟过的坑
Elasticsearch 做为老牌搜索引擎,功能基本满足,但复杂,重量级,适合大数据量。 MeiliSearch 设计目标针对数据在 500GB 左右的搜索需求,极快,单文件,超轻量。 所以,对于中小型项目来说…...

全面升级企业网络安全 迈入SASE新时代
随着数字化业务、云计算、物联网和人工智能等技术的飞速发展,企业的业务部署环境日渐多样化,企业数据的存储由传统的数据中心向云端和SaaS迁移。远程移动设备办公模式的普及,企业多分支机构的加速设立,也使得企业业务系统的用户范…...

2024.1IDEA 到2026年
链接:https://pan.baidu.com/s/1hjJEV5A5k1Z9JbPyBXywSw?pwd9g4i 提取码:9g4i解压之后,按照 操作说明.txt 操作; IntelliJ IDEA 2024.1 (Ultimate Edition) Build #IU-241.14494.240, built on March 28, 2024 Licensed to gurgles tumbles You have…...

uniapp——点赞、取消点赞
案例 更新点赞状态,而不是每次都刷新整个列表。避免页面闪烁,提升用户体验 代码 <view class"funcBtn zan" click"onZan(index,item.id)"><image src"/static/images/circle/zan.png" mode"aspectFill&…...

react经验15:拖拽排序组件dnd-kit的使用经验
应用场景 列表中的成员可鼠标拖拽改变顺序 实施步骤 前置引入 import type { DragEndEvent } from dnd-kit/core import { DndContext } from dnd-kit/core import {arrayMove,/*垂直列表使用verticalListSortingStrategy,横向列表使用horizontalListSortingStrategy*/vert…...

Webpack模块联邦:微前端架构的新选择
Webpack模块联邦(Module Federation)是Webpack 5引入的一项革命性特性,它彻底改变了微前端架构的实现方式。模块联邦允许不同的Web应用程序(或微前端应用)在运行时动态共享代码,无需传统的打包或发布过程中…...

CMake 学习笔记(访问Python)
CMake 学习笔记(访问Python) 利用Python可以做很多事情。比如: 利用 Python 自动生成一些代码。 在我们的程序中植入一个 Python 解释器。 为了做这些事情。就需要 CMake 能够知道 python 装在哪里,装的是什么版本的 python&a…...

【ruoyi】docker部署 captchaImage接口 FontConfiguration空指针异常
后台服务报错captchaImage接口空指针异常,无法启动项目: [http-nio-4431-exec-27] ERROR c.r.f.w.e.GlobalExceptionHandler - [handleRuntimeException,93] - 请求地址/captchaImage,发生未知异常.java.lang.NullPointerException: nullat sun.awt.Font…...

P1443 马的遍历
题目描述: 有一个 𝑛𝑚nm 的棋盘,在某个点 (𝑥,𝑦)(x,y) 上有一个马,要求你计算出马到达棋盘上任意一个点最少要走几步。 代码: package lanqiao;import java.util.*;public class Main {static int n,m…...

AI学习指南概率论篇-贝叶斯推断
AI学习指南概率论篇-贝叶斯推断 概述 在人工智能中,贝叶斯推断是一种基于贝叶斯统计理论的推理方法。它通过使用概率论的知识,结合先验信息和观测数据,来更新对未知变量的推断。贝叶斯推断提供了一种合理的方法来处理不确定性,并…...

大数据测试
1、前言 大数据测试是对大数据应用程序的测试过程,以确保大数据应用程序的所有功能按预期工作。大数据测试的目标是确保大数据系统在保持性能和安全性的同时,平稳无差错地运行。 大数据是无法使用传统计算技术处理的大型数据集的集合。这些数据集的测试涉…...

金融业开源软件应用 管理指南
金融业开源软件应用 管理指南 1 范围 本文件提供了金融机构在应用开源软件时的全流程管理指南,对开源软件的使用和管理提供了配套 组织架构、配套管理规章制度、生命周期流程管理、风险管理、存量管理、工具化管理等方面的指导。 本文件适用于金融机构规范自身对开…...

SolidWorks 齿轮配合
SolidWorks 齿轮配合 在SolidWorks中,齿轮配合是一种特殊的配合类型,用于模拟两个或多个齿轮之间的旋转关系。这种配合确保当一个齿轮旋转时,其他齿轮按照特定的比例旋转,非常适合模拟机械传动系统。以下是使用齿轮配合的详细步骤…...

鸿蒙开发-ArkTS语言-XML
鸿蒙开发-UI-web 鸿蒙开发-UI-web-页面 鸿蒙开发-ArkTS语言-基础类库 鸿蒙开发-ArkTS语言-并发 鸿蒙开发-ArkTS语言-并发-案例 鸿蒙开发-ArkTS语言-容器 鸿蒙开发-ArkTS语言-非线性容器 文章目录 前言 一、XML概述 二、XML生成 三、XML解析 1.解析XML标签和标签值 2.解析XML属性…...