详解JavaScript异步编程之Promise(二)

在上文《详解JavaScript异步编程之Promise(一)》中,详细介绍了异步编程、回调函数以及构造Promise和Promise的构造函数,接下来我们继续介绍Promise的其它用法。

一、链式调用

链式调⽤这个⽅式最经典的体现是在JQuery框架上,比如:

$("#box").css("background", "pink").siblings().css("background", "red");

到现在仍然很多语⾔都在使⽤这种优雅的语法(不限前端还是后台),所以我们来简单认识⼀下什么是链式调⽤,为什么Promise对象可以.then().catch()这样调⽤。为什么还能.then().then()这样调⽤,它的原理是什么呢?

图片

每一次执行then 都会产生一个新的Promise,onFulfilled是promise成功的回调,onRejected 是执行失败的回调,当内部的/前一个Promise的状态发生改变时会通知外部Promise 以此类推,从而实现链式调用并且结果以此向外传递。

function MyPromise(){return this
}
MyPromise.prototype.then = function(){console.log('触发了then')return this
}
new MyPromise().then().then().then()

其本质就是在调⽤这些⽀持链式调⽤的函数的结尾时,它⼜返回了⼀个包含它⾃⼰的对象或者是⼀个新的⾃⼰,这些⽅式都可以实现链式调⽤。

二、Promise使用注意事项

var p = new Promise(function(resolve,reject){resolve('我是Promise的值')
})
console.log(p)

运⾏上面代码查看控制台上会得到如下内容:

图片

[[Prototype]]代表Promise的原型对象;

[[PromiseState]]代表Promise对象当前的状态;

[[PromiseResult]]代表Promise对象的值,分别对应resolve或reject传⼊的结果。

1. 链式调⽤的注意事项

const p = new Promise(function(resolve, reject) {resolve('我是Promise的值')
})
console.log(p)
p.then(function(res) {//该res的结果是resolve传递的参数console.log(res)}).then(function(res) {//该res的结果是undefinedconsole.log(res)return '123'}).then(function(res) {//该res的结果是123console.log(res)return new Promise(function(resolve) {resolve(456)})}).then(function(res) {//该res的结果是456console.log(res)return '我是直接返回的结果'}).then().then('我是字符串').then(function(res) {//该res的结果是“我是直接返回的结果”console.log(res)})

运⾏上面代码查看控制台上会得到如下内容:
图片

根据以上代码和输出结果分析,可以得到:

(1) 只要有then()并且触发了resolve,整个链条就会执⾏到结尾,这个过程中的第⼀个回调函数的参数是resolve传⼊的值。

(2) 后续每个函数都可以使⽤return返回⼀个结果,如果没有返回结果的话下⼀个then中回调函数的参数就是undefined。

(3) 返回结果如果是普通变量,那么这个值就是下⼀个then中回调函数的参数。

(4) 如果返回的是⼀个Promise对象,那么这个Promise对象resolve的结果会变成下⼀次then中回调的函数的参数。

(5) 如果then中传⼊的不是函数或者未传值,Promise链条并不会中断then的链式调⽤,并且在这之前最后⼀次的返回结果,会直接进⼊离它最近的正确的then中的回调函数作为参数。

2. 中断链式调⽤

当promise状态改变时,它的链式调用都会生效,那如果我们有这个一个实际需求:有5个then(),但其中有条件判断,如当不符合第三个then条件时,要直接中断链式调用,不再走下面的then,链式调⽤可以中断吗?答案是肯定的。有两种形式可以让.then的链条中断,一种是抛出异常错误,另一种是return Promise.reject()中断还会触发⼀次.catch的执⾏。

const p = new Promise(function(resolve, reject) {resolve('我是Promise的值')
})
console.log(p)
p.then(function(res) {console.log(res)
}).then(function(res) {// 两种方法意思都代表报错,中断下一步,直接报错// 第一种方法// throw new Error('不符合条件,终端执行')// 第二种方法return Promise.reject('不符合条件,终端执行')
}).then(function(res) {console.log(res)
}).then(function(res) {console.log(res)
}).catch(function(err) {console.log(err)
})

图片

分析上面代码发现中断链式调⽤后会触发catch函数执⾏,并且从中断开始到catch中间的then都不会执⾏,这样链式调⽤的流程就结束了,中断的⽅式可以使⽤抛出⼀个异常或返回⼀个rejected状态的Promise对象。

3. 中断链式调⽤是否违背了Promise的精神?

在介绍Promise的时候强调了它是绝对保证的意思,并且Promise对象的状态⼀旦变更就不会再发⽣变化。当使⽤链式调⽤的时候正常都是then函数链式调⽤,但是当触发中断的时候catch却执⾏了。按照约定规则then函数执⾏,就代表Promise对象的状态已经变更为fulfilled了,但是catch函数执⾏时,Promise对象应该是rejected状态!

const p = new Promise(function(resolve,reject){resolve('我是Promise的值')
})
const p1 = p.then(function(res){})
console.log(p)
console.log(p1)
console.log(p1===p)

图片

分析上面代码会发现返回的 p 和 p1 的状态本身就不⼀样,并且他们的对⽐结果是false,这就代表他们在堆内存中开辟了两个空间,p和p1对象分别保存了两个Promise对象的引⽤地址,所以then函数虽然每次都返回Promise对象,来实现链式调⽤,但是then函数每次返回的都是⼀个新的Promise对象。这样便解释的通了!也就是说每⼀次then函数在执⾏时,我们都可以让本次的结果在下⼀个异步步骤执⾏时,变成不同的状态,⽽且这也不违背Promise对象最初的约定。

根据以上的分析已经掌握了Promise在运⾏时的规则,这样就能解释的通,为什么最初通过Promise控制setTimeout每秒执⾏⼀次的功能可以实现,这是因为当使⽤then函数进⾏链式调⽤时,可以利⽤返回⼀个新的Promise对象来执⾏下⼀次then函数,⽽下⼀次then函数的执⾏,必须等待其内部的resolve调⽤。这样在new Promise时,放⼊setTimeout来进⾏延时,保证1秒之后让状态变更,这样就能不编写回调嵌套来实现连续的执⾏异步流程了。

三、Promise常用API

1、Promise.all()

当我们在代码中需要使⽤异步流程控制时,可以通过Promise.then来实现让异步流程⼀个接⼀个的执⾏,假设实际案例中,某个模块的⻚⾯需要同时调⽤3个服务端接⼝,并保证三个接⼝的数据全部返回后,才能渲染⻚⾯。这种情况如果a接⼝耗时1s、b接⼝耗时0.8s、c接⼝耗时1.4s,如果只⽤Promise.then来执⾏流程控制,可以保证三个接⼝按顺序调⽤结束再渲染⻚⾯,但是如果通过then函数的异步控制,必须等待每个接⼝调⽤完毕才能调⽤下⼀个,这样总耗时就是1+0.8+1.4 = 3.2s。这种累加显然增加了接⼝调⽤的时间消耗,所以Promise提供了⼀个all⽅法来解决这个问题:

Promise.all([promise对象,promise对象,…]).then(回调函数)
回调函数的参数是⼀个数组,按照第⼀个参数的promise对象的顺序展示每个promise的返回结果。可以借助 Promise.all 来实现,等最慢的接⼝返回数据后,⼀起得到所有接⼝的数据,那么这个耗时将会只会按照最慢接⼝的消耗时间1.4s执⾏,总共节省了1.8s。

Promise.all相当于统⼀处理了多个Promise任务,保证处理的这些所有Promise对象的状态全部变成为fulfilled之后才会出发all的.then函数来保证将放置在all中的所有任务的结果返回。

let p1 = new Promise((resolve, reject) => {setTimeout(() => {resolve('第一个promise执行完毕')}, 1000)
})
let p2 = new Promise((resolve, reject) => {setTimeout(() => {resolve('第二个promise执行完毕')}, 2000)
})
let p3 = new Promise((resolve, reject) => {setTimeout(() => {resolve('第三个promise执行完毕')}, 3000)
})
Promise.all([p1, p3, p2]).then(res => {console.log(res)
}).catch(function(err) {console.log(err)
})

2、Promise.race()

Promise.race⽅法与Promise.all⽅法使⽤格式相同:

Promise.all([promise对象,promise对象,...]).then(回调函数)

回调函数的参数是前⾯数组中最快⼀个执⾏完毕的promise的返回值。

race的意思是比赛、赛跑,所以使⽤race⽅法主要的使⽤场景是什么样的呢?举个例⼦,假设我们的⽹站有⼀个播放视频的⻚⾯,通常流媒体播放为了保证⽤户可以获得较低的延迟,都会提供多个媒体数据源。希望⽤户在进⼊⽹⻚时,优先展示的是这些数据源中针对当前⽤户速度最快的那⼀个,这时便可以使⽤Promise.race()来让多个数据源进⾏竞赛,得到竞赛结果后,将延迟最低的数据源⽤于⽤户播放视频的默认数据源,这个场景便是race的⼀个典型使⽤场景。

Promise.race()相当于将传⼊的所有任务进⾏了⼀个竞争,他们之间最先将状态变成fulfilled的那⼀个任务就会直接的触发race的.then函数并且将他的值返回,主要⽤于多个任务之间竞争时使⽤。

let p1 = new Promise((resolve, reject) => {setTimeout(() => {resolve('第⼀个promise执⾏完毕')}, 5000)
})
let p2 = new Promise((resolve, reject) => {setTimeout(() => {reject('第⼆个promise执⾏完毕')}, 2000)
})
let p3 = new Promise(resolve => {setTimeout(() => {resolve('第三个promise执⾏完毕')}, 3000)
})
Promise.race([p1, p3, p2]).then(res => {console.log(res)
}).catch(function(err) {console.error(err)
})

3、Promise.any()

Promise.any()接收一个 promsie 可迭代对象,但只要其中有一个 promise 成功,就返回那个已经成功的 promise。本质上,它和Promise.all()刚好相反。

const promiseA = new Promise((resolve, reject) => {setTimeout(() => {resolve('A');}, 0);
});const promiseB = new Promise((resolve, reject) => {setTimeout(() => {reject('B error');}, 1000);
});Promise.any([promiseA, promiseB]).then((res) => {// promiseA 成功,因此返回promiseA的结果console.log(res); // A
}).catch((error) => {console.log(error);
});

这个方法用于返回第一个成功的 promise 。只要有一个 promise 成功此方法就会终止,它不会等待其他的 promise 全部完成。

4、Promise.allSettled()

ES2020引入的用于确定一组异步操作是否都结束了(不管成功或失败)。
Promise.allSettled()方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。Promise.allSettled()和Promise.all()类似,只不过它只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更。
注意:返回的新的promise实例的状态总是resolved,它的回调函数会接收到一个数组作为参数,该数组的每个成员对应前面数组的每个promise对象。

const promiseA = new Promise((resolve, reject) => {setTimeout(() => {resolve('A');}, 0);
});const promiseB = new Promise((resolve, reject) => {setTimeout(() => {reject('B error');}, 1000);
});Promise.allSettled([promiseA, promiseB]).then((res) => {const [a, b] = res;// 返回的对象包含promise的状态,以及结果console.log(a, b); //{status: "fulfilled", value: "A"},{status: "rejected", reason: "B error"}
})
.catch((error) => {console.log(error);
});

相比之下,Promise.all()适合多个异步操作之间相互依赖的场景,而Promise.allSettled()更适合多个异步操作相互独立的场景。

四、总结

多个异步并行,且相互没有关联,使用Promise.allSettled();多个异步并行,相互之间有依赖,使用Promise.all();多个异步并行,最终结果根据第一个出结果(不论成功还是失败)的 promise 而定,使用Promise.race();多个异步并行,最终结果根据第一个成功的 promise 而定,使用Promise.any()。

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

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

相关文章

VC++添加菜单学习

新建一个单文档工程; 完成以后看一下有没有出现如下图的 资源视图 的tab;如果没有,在文件列表中找到xxx.rc2文件; 点击 资源视图 的tab,或者双击 .rc2 文件名,就会转到如下图的资源视图;然后展…

视频融合平台EasyCVR推流成功但平台显示不在线是什么原因?

TSINGSEE青犀视频监控汇聚平台EasyCVR可拓展性强、视频能力灵活、部署轻快,可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等,以及支持厂家私有协议与SDK接入,包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安防视频监控的能力&…

docker部署docker管理工具easydockerweb

重要提示 功能比较少,建议体验一下即可 安装 docker run -it -d -p 10041:3000 -e EDW_USERNAMEadmin -e EDW_PASSWORDadmin -v /var/run/docker.sock:/var/run/docker.sock qfdk/easydockerweb 使用 概览 镜像管理 容器管理

NuxtJs安装Sass后出现ERROR:Cannot find module ‘webpack/lib/RuleSet‘

最近了解NuxtJs时,发现问题比较多,对于初学者来说是件比较头痛的事。这次是安装sass预处理器,通过命令安装后,出现了ERROR:Cannot find module webpack/lib/RuleSet 错误,于是根据之前经验,对版…

Feign请求头丢失的解决方法及其分析

发现问题 在写多服务互相调用的时候,发现远程feign调用方法正常情况下是无法将请求头的信息(例如token等)顺带传播的。 我们可以添加远程 feign 远程调用拦截器,来获取token 数据。 如上图:因为微服务之间并没有传…

两次NAT

两次NAT即Twice NAT,指源IP和目的IP同时转换,该技术应用于内部网络主机地址与外部网络上主机地址重叠的情况。 如图所示,两次NAT转换的过程如下: 内网Host A要访问地址重叠的外部网络Host B,Host A向位于外部网络的DNS服务器发送…

SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式 基础(持续更新~)

具体操作: day2: 作用: 出现跨域问题 配相对应进行配置即可解决: IDEA连接的,在url最后加参数?useSSLfalse注意链接密码是123(docker中mysql密码) 注意,虚拟机中设置的密码和ip要和主机上…

vue2使用echarts自定义tooltip内容

先上最终效果图 # 实现过程&#xff1a; 一、下载引入echarts 下载 npm install echarts --save在main.js中引入 import * as echarts from "echarts"; Vue.prototype.$echarts echarts;二、使用 <template><div id"myechart" style"…

神经网络激活函数到底是什么?

激活函数 其实不是很难啦&#xff0c;归结一下就是大概这样几个分类&#xff0c;详情请参考【神经网络】大白话直观理解&#xff01;_哔哩哔哩_bilibili神经网络就是干这个事的~ 如果队伍不长&#xff0c;一个ykxb就可以了&#xff0c;如果 如果 队伍太长 就加一个激活函数也…

前端开发之deepmerge的使用和示例(对象的深度合并)

前端开发之deepmerge的使用和示例 前言使用场景链接效果图vue中简单案例1、安装插件2、示例结果 前言 在平时的项目中经常会涉及到对象除了第一层以及下层进行深度合并&#xff0c;本问讲解的是深度合并的插件deepmerge&#xff0c;使用此插件避免通过递归实现一些深度合并所带…

新手指南:Postman 旧版本(历史版本)下载

随着技术的不断发展&#xff0c;有时候我们发现自己需要退回到使用 Postman 某个以往的版本。这篇文章旨在指导你如何安全地移除当前版本的 Postman、查找并获取旧版本。 从你的系统中移除 Postman 为了确保旧版本的 Postman 可以无障碍地安装&#xff0c;首先得从你的系统中…

LlamaIndex 入门实战

文章目录 LlamaIndex 入门实战1. 基本概念2. 优劣势分析3. 简单代码示例4. Index持久化5. 使用场景6. 总结 LlamaIndex 入门实战 LlamaIndex是一个连接大型语言模型&#xff08;LLMs&#xff09;与外部数据的工具&#xff0c;它通过构建索引和提供查询接口&#xff0c;使得大模…