该系列文章连载于公众号coderwhy和掘金XiaoYu2002中
- 对该系列知识感兴趣和想要一起交流的可以添加wx:coderwhy666,拉你进群参与共学计划,一起成长进步
- 课程对照进度:JavaScript高级系列141-151集(coderwhy)
- 后续JavaScript高级知识技术会持续更新,如果喜欢我们的文章,欢迎关注、点赞、转发、评论,大家的支持是我们最大的动力
脉络探索
Promise是ES6之后新增的一个重要知识点(也可以说是最重要的知识点之一),是每个前端开发都必须要掌握的知识点,因为这与我们正式编写项目时,需要与后端交互的网络请求息息相关
- 但是对于Promise的学习来说,很多同学会觉得有点迷茫,刚开始不知所云、困难重重
- 在本章节中,我们会认识Promise究竟是一个怎么样的API,在Promise出现之前,我们都是怎么进行书写的,以及当Promise出现后,都有哪些方法以及如何使用
- Promise的类方法与对象方法有什么区别,为什么要区分开为两种方法?在本章节中都能得到解惑
一、认识Promise
1.1 异步请求处理
- 在ES6出来之后,有很多关于Promise的讲解、文章,也有很多经典的书籍讲解Promise
- 虽然等我们学会Promise之后,会觉得Promise不过如此,但是在初次接触的时候都会觉得这个东西不好理解
- 但是在初次接触的时候都会觉得这个东西不好理解
- 任何新技术的出现,大多数都是为了解决原有问题技术上的痛点,痛点给我们明确的优化方向,从而不断优化迭代,量变产生质变从而催生新的技术,进而促进发展(例如初代计算机与现代计算机的差距)
- 在不了解痛点的情况下,我们就很难对该技术的出现以及必要性有深刻的理解,这也是初次接触不好理解的原因,因为缺乏理解的土壤
- 那么这里我从一个实际的例子来作为切入点:
- 我们调用一个函数,这个函数中发送网络请求(我们可以用定时器来模拟)
- 如果发送网络请求成功了,那么告知调用者发送成功,并且将相关数据返回过去
- 如果发送网络请求失败了,那么告知调用者发送失败,并且告知错误信息
//request.js
function requestData(url) {
// 模拟网络请求
setTimeout(() => {
// 拿到请求的结果
// url传入的是juejin.cn, 请求成功
if (url === "juejin.cn") {
//成功
} else { // 否则请求失败
// 失败
}
}, 3000);
}
//main.js
requestData('juejin.cn')
- 在上方案例中,我们采用定时器来模拟发送网络请求,3秒后返回结果
- requestData目前接收一个参数,也就是发送模拟请求的URL地址
- 此时在模拟请求中有两种情况:调用成功和调用失败
- 但不管最终的结果如何,我们都需要返回处理结果,才能进行下一步的操作
- 但这里有问题了,我们要怎么返回结果呢?
- 有的同学可能会认为,直接return结果不就好了
- 但其实这是行不通的,我们可以来试一下
//request.js
function requestData(url) {
// 模拟网络请求
setTimeout(() => {
// 拿到请求的结果
// url传入的是juejin.cn, 请求成功
if (url === "juejin.cn") {
//成功
let names = ['小余','coderwhy']
return names
} else { // 否则请求失败
// 失败
let errMessage = '请求失败,url错误'
return errMessage
}
}, 3000);
}
//main.js
const response = requestData('juejin.cn')
console.log(response);
- 返回的response结果为undefined,这也是必然的,我们先来观察其过程
- 在终端运行的一瞬间,结果就已经返回,但进程依旧没有结束,直到定时器的3秒结束才随之结束
- 因为定时器的结果需要3秒后返回,而JS的运行是不会等待的,而response没有等到返回的结果,JS引擎将给出默认值undefined
- 但需要注意的是,结果是有生效的,只是尚未等来,我们可以在成功或者失败的条件后面加一个log打印数据的结果,会发现undefined立刻打印出来,而3s后,真正的返回结果也打印了出来
- 这很有意思,通过上一章节响应式原理的学习,大家可能会恍然大悟,这不就可以使用响应式了吗?
- 将names转为响应式对象,当3s后的赋值一发生,就通知依赖重新调用
- 我们这里不将其转为响应式,因为通过网络请求,我们拿到来自服务器的数据(外部数据),一旦要修改数据的话,通常会使用后端所提供的修改或删除接口(接口更新数据),然后作用于服务器上,此时再让服务器返回最新的数据。这样的逻辑会更加清晰
- 但我们可以借鉴响应式实现依赖调用的部分原理,在真正处理结果出来后,手动通知依赖进行调用
if (url === "juejin.cn") {
//成功
let names = ['小余','coderwhy']
console.log(names);
} else { // 否则请求失败
// 失败
let errMessage = '请求失败,url错误'
console.log(errMessage);
}
//返回结果
//undefined
//[ '小余', 'coderwhy' ]
- 这需要我们手写一个通知方法,在请求成功或者失败后,将返回的结果传递到通知方法中进行调用
- 和log打印不同的地方在于:一个是在控制台打印了方便测试,另一个则是真正返回结果
- 并且在这里,我们需要手写两个通知方法,一个是请求成功的方法,另一个是请求失败的方法
- 我们为什么不将其合为一个方法呢?首先在于让开发者明白当下所调用的是成功还是失败,便于阅读代码和调试,不同的方法更方便针对失败或者成功的情况进行独立的扩展和修改,而不会影响到另一个逻辑
- 并且请求失败后,需要对错误进行更细致的处理,例如重新发起请求、错误提示等等,这和成功的处理逻辑是不同的
- 在清楚要实现两个方法后,我们还需要明确一点:成功或者失败的函数调用方法要作为回调函数传递进requestData
- 我们之所以不直接在全局定义这两个方法,主要有几个原因
- 尽量保持函数本身内部的纯粹性(纯函数),外界影响因素不过多涉及,防止耦合度过高,导致该requestData必须依赖于全局的两个方法,否则将无法正常使用,如图26-
- 我们对网络请求返回的数据会根据应用场景,针对性进行处理,该处理并不通用,因此不适合封装成通用函数
图26-1 函数方法的独立性
- 最终我们采用将成功回调以及失败回调,作为requestData函数的参数2与参数3,然后在调用传入URL网址时,回调分别接收对应数据进行处理,而这就是我们ES5时期的处理方式
function requestData(url, successCallback, failtureCallback) {
// 模拟网络请求
setTimeout(() => {
// 拿到请求的结果
// url传入的是juejin.cn, 请求成功
if (url === "juejin.cn") {
// 成功
let names = ['小余','coderwhy']
successCallback(names)
} else { // 否则请求失败
// 失败
let errMessage = "请求失败, url错误"
failtureCallback(errMessage)
}
}, 3000);
}
// main.js
requestData("baidu.com", (res) => {
console.log(res)
}, (err) => {
console.log(err)
})
- 但该回调方式有很多弊端:
- 如果是我们自己封装的requestData,那么我们在封装的时候必须要自己设计好callback名称, 并且使用好
- 如果我们使用的是别人封装的requestData或者一些第三方库, 那么我们必须去看别人的源码或者文档, 才知道它这个函数需要怎么去获取到结果
- 在这个过程中,没有一个明确的规范,导致说获取结果的方式以及顺序都有所不同,这对开发者的心智负担较大,使用成本也较高,而这也就是我们所说的痛点之一
1.2 Promise基础使用
- 更好的异步方案是返回一个"承诺",这是什么意思?
- 承诺类似于一种契约精神,通常用于无法当下完成,于未来回应的一种行为
- 且承诺需要双方的配合,一方发出承诺,另一方接收承诺,在时间的见证下是否完成其承诺
- 正如"常比翼,白头誓",当下发出承诺,等待白头时来兑现
- 在JS编程的世界中,承诺是具象化的Promise API,规范好了所有的代码编写逻辑,不需要我们再去自己封装对应的处理与回调,开发者只需要看到返回的是一个承诺(Promise)就可以立刻使用,而无需查找对应文档
//更规范/更好的方案 Promise承诺(规范好了所有的代码编写逻辑)
function requestData(){
return '承诺'
}
const chengnuo = requestData()
1.2.1 什么是Promise
- 在上面的解决方案中,我们确确实实可以解决请求函数得到结果之后,获取到对应的回调,但是它存在两个主要的问题:
- 第一,我们需要自己来设计回调函数、回调函数的名称、回调函数的使用等
- 第二,对于不同的人、不同的框架设计出来的方案是不同的,那么我们必须耐心去看别人的源码或者文档,以便可以理解它这个函数到底怎么用
- 因此该解决方案注定只是一个过渡,随机技术发展,会有更加完善的方式出现,而Promise就是为了解决该问题而出现的
- 此时,我们如果想利用Promise来重构我们一开始使用定时器模拟的异步请求处理,这需要我们了解一下Promise的API是怎么样的:
- Promise是一个类,可以翻译成 承诺、许诺 、期约
- 当我们需要给予调用者一个承诺:待会儿我会给你回调数据时,就可以创建一个Promise的对象
- 在通过new创建Promise对象时,我们需要传入一个回调函数,我们称之为executor
- 这个回调函数会被立即执行,并且给传入另外两个回调函数resolve、reject
- 当我们调用resolve回调函数时,会执行Promise对象的then方法传入的回调函数
- 当我们调用reject回调函数时,会执行Promise对象的catch方法传入的回调函数
图26-2 Promise流程
- 理解这些概念是掌握Promise的主要难点,我们一步步来,首先我们可以将一开始的理论承诺替换为具体的API
function requestData(){
return new Promise
}
//const promise = new Promise()
const promise = requestData()
- Promise在一开始调用时,会先立即执行一次传入的回调函数,这是一开始发出承诺的地方
- 该立即执行,我们可以利用class类来模拟实现,从而观察其立即执行的原理方式
- 该立即执行的函数就被称为executor(执行者)
class Person {
constructor(callback) {
callback()
}
}
const p = new Person(()=>{
console.log('立即执行-p实例');
})
const promise = new Promise(()=>{
console.log('立即执行-promise');
})
- executor为承诺提供了空间,尚缺的是承诺内容,我们将承诺放入该回调函数中
- 承诺不能埋在心里,要表露出来,因此会进行第一次调用,我们可以将该阶段认为是发出承诺的开始,从这一刻起,承诺生效,直到承诺兑现或者被拒绝为止
- 该承诺生效阶段被称为待定(pending),指初始状态,既没有被兑现,也没有被拒绝
- 而还有另外两个阶段分别为承诺兑现与承诺拒绝,这是不可共存的两阶段两岔口,同一时间只会生效一个
- 做出该决定的方式是通过executor所提供的两参数决定(resolve(决定) or reject(否决))
- 在做出决定或者否决前的内容为待定阶段
图26-3 Promise的三个状态阶段
//resolve和reject都是回调函数
const myPromise = new Promise((resolve, reject) => {
//成功调用resolve
resolve()
//失败调用reject
reject()
});
- 我们可以将
resolve
和reject
这两个回调函数理解为一个通知宣告,宣告我完成了该承诺或者失败了,但不管是哪一个回调,都会给对方带来信息承诺方
通过两个回调发出了具体消息,受诺方
要如何拿到该消息呢?- 答案是通过then方法与catch方法,这两个方法是
受诺方
所具备的 - 当承诺方resolve回调(信息传出),受诺方then方法接收(参数信息)
- 当承诺方reject回调(信息传出),受诺方catch方法接收(参数信息)
- 但我们不推荐分开处理then与catch,首先不通过链式调用会失去关联性,并且导致报错问题
const myPromise = new Promise((resolve, reject) => {
//成功调用resolve
resolve('我兑现了承诺')
//失败调用reject
reject('我拒绝了承诺')
});
//p是受诺方
//导致的报错信息问题会在后续说明
const p1 = myPromise.then(v => console.log(v))//我兑现了承诺
const p2 = myPromise.catch(err => console.log(err))//我拒绝了承诺
- 通过案例的控制台打印,可以确切的知道信息从
承诺方
的回调实参中传入,从受诺方
的方法形参中接收- 因此resolve回调、reject回调与then方法、catch方法无非就是另一种收集信息、通知信息的表达形式
- 和曾经在手写响应式中所表达的部分原理思想一致,能够看出其具备一定的共通性
图26-4 承诺方向受诺方的信息传递方式
- 根据Promise信息的传导方式,可以看出已经规范好传递信息的代码应该如何编写,我们不需要关系数据的传递过程,只要专注于处理数据本身
- 在此之后,我们使用Promise重构一开始的异步请求处理,看其中的区别
- 我们通常将异步的信息放入Promise的executor内,在该回调内部,依旧是从上至下执行
- 可以先假定内部全是待定(pending)状态,直到遇到resolve或者reject调用时(Executor回调的两参数),待定状态结束,确定返回的诺言(PS:说出口的承诺叫做诺言)
- 所以我们会在Executor中确定我们的Promise状态:
- 通过resolve,可以兑现(fulfilled)Promise的状态,我们也可以称之为已决议(resolved)
- 通过reject,可以拒绝(reject)Promise的状态
- 在本案例中,url由if进行判断,3s后返回结果,我们使用两个测试案例,分别传递正确和错误的URL,观察其返回结果,确定Promise正确处理其请求成功与请求失败的情况
function requestData(url) {
//返回一个承诺
return new Promise((resolve, reject) => {
// 模拟网络请求(异步)
setTimeout(() => {
// 拿到请求的结果
// url传入的是juejin.cn, 请求成功
if (url === "juejin.cn") {
// 成功
let names = ['小余', 'coderwhy']
resolve(names)
} else { // 否则请求失败
// 失败
let errMessage = "请求失败, url错误"
reject(errMessage)
}
}, 3000);
})
}
// main.js
const promise1 = requestData('juejin.cn')//正确URL
const promise2 = requestData('xiaoyu2002.cn')//错误URL
//3s后返回结果
promise1.then(v => console.log(v)).catch(v => console.log(v))//[ '小余', 'coderwhy' ]
promise2.then(v => console.log(v)).catch(v => console.log(v))//请求失败, url错误
1.2.3 链式调用
- 并且在这里,我们采用了链式调用,在then方法之后,接着调用了catch方法,那这又是什么意思呢?
- 我们来学习一下其中的演化过程,在异步请求处理阶段所封装的回调,分别位于requestData函数的参数2、3之中,该使用方式用在Promise上一样是能够生效的
- 因为
.then()
方法接受两个参数(最多);第一个参数是 Promise 兑现时的回调函数,第二个参数是 Promise 拒绝时的回调函数。每个.then()
返回一个新生成的 Promise 对象,这个对象可被用于链式调用 - 但该使用方式并不推荐,最明显的在于可阅读性差,结构不清晰,当代码量多了之后,并不好区分处理
//1.最初版本
requestData("baidu.com", (res) => {
console.log(res)
}, (err) => {
console.log(err)
})
//1.Promise对应(最初版本)
promise1.then((v) => {
console.log(v);
}, (err) => {
console.log(err);
})
-
除阅读性差外,还有几点主要原因:
- 错误处理不全面,容易导致未捕获的异常:传入第二参数作为失败回调,则将失败回调视为与成功回调统一时间的整体,则当成功回调中抛出了异常,这个异常将 不会被第二个参数捕获,也不会被后续的
.catch()
捕获,有些错误就容易悄无声息被忽略,导致难以调试。所以在没有迫切需要的情况下,最好将错误处理留到最后一个.catch()
语句。.catch()
其实就是一个没有为 Promise 兑现时的回调函数留出空位的.then()
- 一旦采用该方式,如果还想再Promise链中传递错误,则需要手动在第二失败回调参数中抛出错误或者返回一个被拒绝的Promise,否则错误没办法沿着Promise链继续传递下去
- 为了更好配合异步函数async/await,使用.catch()处理错误会更好,这一点在后续还会说明
- 将.then与.catch进行链式调用处理符合
Promise/A+ 规范
,关于这一点,在后续一样会进行说明,这里先简单了解
- 错误处理不全面,容易导致未捕获的异常:传入第二参数作为失败回调,则将失败回调视为与成功回调统一时间的整体,则当成功回调中抛出了异常,这个异常将 不会被第二个参数捕获,也不会被后续的
-
所以链式调用作为一种
连续处理多个异步操作,每个操作都可以基于前一个操作的结果
的方式,非常适合Promise -
因此Promise 其实是 JS 中用于处理异步操作的一种模式。它代表一个可能在未来某个时间点完成的操作
-
发送网络请求到接收成功或失败的中间,是有一段路程的(定时器模拟3s路程)
-
该路程所耗费的时间主要在以下流程中:
-
DNS解析: 将域名转换为IP地址,需要时间
-
建立连接: TCP连接建立需要进行三次握手,增加延迟
-
发送请求: 将请求数据发送到服务器,需要时间,尤其是数据量大时
-
服务器处理: 服务器接收请求并处理逻辑,响应时间因服务器性能和负载而异
-
数据传输: 将响应数据从服务器发送回客户端,传输速度受网络状况影响
-
接收与解析: 客户端接收到响应并解析数据,也会耗费时间
- 这些环节共同构成了从请求到响应的延迟,在我们上方定时器模拟案例中,主要从流程3起步,发送请求在当下,接收与解析在未来
当下
与未来
的时间错位,我们称为异步,而Promise就是一种在当下操作,在未来接收结果的异步API
-
图26-5 网络请求的过程
- 在讲解什么是Promise时,简单介绍了其中的三个状态:
状态 | 描述 | 状态转换 |
---|---|---|
Pending(待定) | 初始状态,异步操作尚未完成,结果未知 | 可以转换为 Fulfilled 或 Rejected 状态 |
Fulfilled(已兑现) | 异步操作成功完成,Promise 获得了期望的结果 | 状态固定,不再改变 |
Rejected(已拒绝) | 异步操作失败,Promise 被拒绝,并携带失败的原因 | 状态固定,不再改变 |
- 而在我们案例中,这三状态,对应了三个执行函数
- 划分状态,是为了辨别当下的Promise处于哪一阶段
- 不同阶段所需处理的事情不同,明确该点有助于明确当前需要处理哪些工作
- 一旦状态被确定下来(非待定状态),Promise的状态会被
锁死
,该Promise的状态是不可更改的- 在我们调用resolve的时候,如果resolve传入的值本身不是一个Promise,那么会将该Promise的状态变成
兑现(fulfilled)
- 在锁定之后我们去调用reject时,已经不会有任何的响应了(并不是这行代码不会执行,而是无法改变Promise状态)
- 在我们调用resolve的时候,如果resolve传入的值本身不是一个Promise,那么会将该Promise的状态变成
图26-6 Promise三状态对应三执行函数
//执行函数1:(resolve,reject) => {}
new Promise((resolve, reject) => {
// pending状态: 待定/悬而未决的
console.log("--------")
reject() // 处于rejected状态(已拒绝状态)
resolve() // 处于fulfilled状态(已敲定/兑现状态)
console.log("++++++++++++")
}).then(res => {//执行函数2:.then
console.log("res:", res)
}, err => {//执行函数3:.catch
console.log("err:", err)
})
- 锁定状态无法改变,但正常代码可以执行
- 通过验证:在reject、resolve下添加一行
console.log("++++++++++++")
代码 - 结果是能够显示,说明其正常往后执行
- 通过验证:在reject、resolve下添加一行
2.1 Promise的resolve方法参数
- 在Promise的executor回调中,resolve方法可以传递成功信息value,事实上该方法还能单独从Promise中调用
const promise1 = Promise.resolve(123);
promise1.then((value) => {
console.log(value);//123
});
-
参数虽然只有一个value,但传递值所表达形式却有所不同
-
正常情况所传递内容为普通值或对象,表达形式相同
-
但除正常情况外,还能够传递其他Promise进去
- 一旦传递Promise,那么当前的Promise状态,则由传递进去的Promise所决定,相当于状态进行移交
- 关于该点特性,开发中使用较少,面试较为常见
-
-
在MDN文档中这样描述:
Promise.resolve()
方法特殊处理了原生Promise
实例。如果value
属于Promise
或其子类,并且value.constructor === Promise
,那么Promise.resolve()
直接返回value
,而不会创建一个新的Promise
实例。否则,Promise.resolve()
实际上相当于new Promise((resolve) => resolve(value))
的简写形式 -
让我们来简要实现一个基础案例,方便理解:
- 一个promise之间的接力棒,前者(promise1)把接力棒传给后者(promise),接下来由后者(promise)执行,前者停下休息了
const promise = new Promise((resolve) => {
resolve('我是promise,不是promise1')
})
const promise1 = Promise.resolve(promise);
promise1.then((value) => {
//value为promise,则执行决定权由promise1传递给promise
console.log(value);//我是promise,不是promise1
});
- 除了以上两种传递方式(正常传递以及Promise传递)之外,还有第三种方式:
- 传入一个对象,并且该对象有实现then方法,那么也会执行该then方法,并且由该then方法决定后续状态
- Promise不会区分该then方法是由谁实现的,都会进行交接执行
- 在resolve接收该带有then方法的obj对象时,一样会进行接力棒交接,因此正常Promise的then方法内所执行的是obj对象的then方法
// 传入一个对象, 这个兑现有then方法
new Promise((resolve, reject) => {
// pending -> fulfilled
const obj = {
then: function(resolve, reject) {
// resolve("resolve message")
reject("reject message")
}
}
resolve(obj)
}).then(res => {
console.log("res:", res)
}, err => {
console.log("err:", err)
})
- 在其他语言中,对象有一个eat方法,会被视为实现了一个对象的eatable,通常以接口形式存在
- 在编程中,特别是在涉及接口(interface)或协议(protocol)的语言中,使用 "-able" 作为接口或抽象类型的命名后缀是一种常见的命名约定。这种命名方式用于表示一个对象具备某种能力或特性
- Eatable 接口:表示对象具有 "eat"(吃) 的能力
- Runnable 接口:表示对象具有 "run"(运行) 的能力
- 而then方法也是如此,也能够被称为
thenable
,因此thenable 对象指的是任何具有then
方法的对象- thenable 对象可以被 Promise 机制识别,并被处理为 Promise,这就是它的作用,也是Promise识别其对象中自己实现then方法的原因
- 因此开发者可以创建自定义的 thenable 对象,而不必严格继承或依赖于特定的 Promise 类,拥有更大的灵活性。通过实现
then
方法,简单的对象也可以参与到 Promise 的链式调用和异步处理过程中,统一了异步操作的接口
// eatable/runable
const obj = {
eat: function() {
},
run: function() {
}
}
- 尽管所有的 Promise 都是 thenable,但并非所有的 thenable 对象都是规范的 Promise。因此,thenable 可以被视为 Promise 的一个子集或简化版本,在此基础上能去进一步理解图26-7的MDN文档所说明的含义
图26-7 MDN文档对thenable的描述
- Promise分为对象方法和类方法,在MDN文档中分别表示为
实例方法
和静态方法
- 关于两者的区别,在17章的Class类与构造函数中有进行说明
- 但是,为什么要将这些方法区分开?为什么有些方法要放在静态方法里,有些方法要放在实例方法里?
- 了解这一点对于我们等下的学习有较大帮助
- 最主要的原因在于
静态方法不依赖于具体的Promise实例
- 因此静态方法可以直接调用,不用基于new出来的Promise调用,非常适合封装各种工具函数
- 而三个实例方法则必须基于executor锁定状态后返回的二阶段Promise才能调用,无法基于已经存在的Promise构造函数直接调用
- 因为该三个方法由于需要访问和操作与特定 Promise 实例相关的状态(例如,pending、fulfilled 或 rejected)且允许链式调用的特性,因此无法独立调用才会归类到实例方法中,前者是最主要的原因
//静态方法 resolve
const promise1 = Promise.resolve('coderwhy');
//实例方法 then
//因为resolve本身返回Promise二阶段,因此可以直接在该返回值上调用实例方法
promise1.then((value) => {
console.log(value);//coderwhy
});
图26-8 Promise的实例方法和静态方法
- 对于Promise中的实例方法,想要查看可以通过
Object.getOwnPropertyDescriptor()
获取其Promise原型上的内容,也能够印证实例方法的数量
const v = Object.getOwnPropertyDescriptors(Promise.prototype)
console.log(v);
// then: {
// value: [Function: then],
// writable: true,
// enumerable: false,
// configurable: true
// },
// catch: {
// value: [Function: catch],
// writable: true,
// enumerable: false,
// configurable: true
// },
// finally: {
// value: [Function: finally],
// writable: true,
// enumerable: false,
// configurable: true
// }
3.1 then方法
- then方法是Promise对象上的一个方法:它其实是放在Promise的原型上的 Promise.prototype.then
- 最多接收两个参数:onFulfilled与onRejected,分别对应兑现状态与拒绝状态
- 一般情况下,我们只会接收一个兑现状态的参数,其拒绝状态交给catch处理
//fulfilled的回调函数:当状态变成fulfilled时会回调的函数
//reject的回调函数:当状态变成reject时会回调的函数
then(onFulfilled, onRejected)
- 一个Promise的then方法是可以被
多次调用
的,这里的多次调用并非指多次链式调用
,需要进行区分- 当我们promise的resolve方法被回调时,所有基于该promise的then方法都会被调用
const promise = new Promise((resolve, reject) => {
resolve("小余")
})
//多次调用
promise.then(res => {
console.log("res1:", res)
})
promise.then(res => {
console.log("res2:", res)
})
promise.then(res => {
console.log("res3:", res)
})
// res1: 小余
// res2: 小余
// res3: 小余
- 传入then方法的回调函数具备返回值的功能,我们可以返回一个具体内容
- 该内容可以是一个普通值,那该值会被作为一个新的Promise的resolve值
const promise = new Promise((resolve, reject) => {
resolve("小余")
})
//多次调用
promise.then(res => {
return res
})
//return res 等价以下代码
return new Promise((resolve) => {
resolve('小余')
})
- 由于返回的普通值会被包装为一个Promise,所以可以在该基础上继续调用then方法,这是能够使用链式调用的原因
- 如果不设置返回值,则返回值默认由JS引擎设置为undefined,不能在undefined基础上进行任何调用,否则报错
- 由于该返回值是一个Promise,如果中间阶段有其余多个用途,则可以拆分出来用变量进行接收,从而调用。因此多次调用和链式调用是可以结合使用的,但拆分开的链式调用,在代码层面上分开容易造成误解,而我们有必要让开发者知道这是耦合关系
- 如果多次结合使用,会如同一个大树生长不断开出分支,一旦根部有问题将出现多米诺效应,小问题容易引发大后果,因此需要谨慎使用该结合形式
//链式调用
promise.then(res => {
return 'promise1'
}).then(res => {
return console.log(`${res} + promise2`);
})
//promise1 + promise2
//拆分的链式调用
const promise1 = promise.then(res => {
return 'promise1'
})
const promise2 = promise1.then(res => {
return console.log(`${res} + promise2`);
})
//在拆分的链式调用基础上,分支出多次调用
promise2.then()
promise2.then()
promise2.then()
- 除了作为普通值返回再被包裹为Promise进行使用而言,另外一种返回值则是直接自行编写新的Promise进行返回,该返回具备更高自由度和调整空间
- 例如我们在返回值内继续延迟3s,后续then方法链式调用所接收的过程同样会延迟3s
// 2> 如果我们返回的是一个Promise
promise.then(res => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(111111)
}, 3000)
})
}).then(res => {
console.log("res:", res)
})
- 除以上两种,还有一种thenable的返回形式,在对象上实现then方法,则同样会被返回。该返回相对于Promise而言,属于将一切都交给开发者进行自定义,在自由程度拉到最高的同时,也对开发者的技术能力要求达到最高
// 3> 如果返回的是一个对象, 并且该对象实现了thenable
promise.then(res => {
return {
then: function(resolve, reject) {
resolve(222222)
}
}
}).then(res => {
console.log("res:", res)
})
- 到这为止,then方法参数一(resolve)则算掌握基础使用,而参数二内容其实就是catch方法,日常使用会将其区分开,具体原因在前面有进行说明,因此我们这里的参数二讲解会直接放在catch方法的讲解中
- 在then方法中,我们学习了两种调用方式以及三种返回值形式,来做一个总结回顾,如表26-1与表26-2
表26-1 then 方法的调用方式
调用方式 | 描述 |
---|---|
多次调用 | 在同一个 Promise 上可以多次调用 then ,每个调用都会注册一个回调函数,当 Promise 解决时都会执行 |
链式调用 | then 方法返回一个新的 Promise ,允许进行链式调用。通过返回值来链接后续的 then 调用 |
拆分的链式调用 | then 方法的结果可以被赋值给变量,再基于这些变量进行链式调用,有助于管理复杂的异步流程 |
表26-2 then 方法的返回值形式
返回值类型 | 描述 |
---|---|
普通值 | 返回的普通值(如字符串、数字等)会被包装成一个新的 Promise ,并自动解决为该值 |
Promise | 如果直接返回一个新的 Promise ,后续的 then 调用会等待这个 Promise 解决后继续执行。这提供了对异步操作更细粒度的控制 |
thenable | 返回一个具有 then 方法的对象(thenable 对象)。这允许自定义解决过程,JS 引擎会像处理正常 Promise 那样处理 thenable 对象 |
3.2 catch方法
- catch方法具备两种调用形式,隐式与显式
- 隐式调用:then方法第二参数
- 显式调用:then方法之后接着链式调用catch方法
- 隐式调用存在一开始说明的阅读性较差等问题,推荐使用显示调用写法
Promise A+规范
中规定的写法时隐式调用写法,该规范规定了Promise应该如何书写- 但ES6中在实现Promise时,为了让代码阅读性更强,编写更方便,提供了第二种显式写法,而这两种效果的写法是一致的
- 在
Promise A+规范
中,甚至是没有catch这个方法的,该方法是ES6为了方便开发者处理而实现的特殊方法,相当于then方法的一个语法糖,在MDN文档中说明该方法是Promise.prototype.then(undefined, onRejected)
的一种简写形式,catch()
只是单纯的调用了then()
,这一点在ECMAScript2025版本的规范文档中也有说明
图26-9 ECMAScript规范文档对catch方法的调用步骤描述
const promise = new Promise((resolve, reject) => {
reject('err')
})
// 隐式调用
promise.then(undefined,err => {
console.log(err);
})
// 显式调用
promise.then().catch(err => {
console.log(err);
})
- 调用catch方法时,如下方代码所示,相当于手动返回了一个Promise二阶段(rejected状态)
const promise = new Promise((resolve, reject) => {
reject('err')
})
promise.then(resolve => {
return new Promise((resolve, reject) => {
reject('then rejected status')
})
}).catch(err => {
console.log(err);
})
- Promise阶段一在返回错误信息给阶段二时,也具备两种方式
- reject调用:返回错误信息
- throw new Error():抛出异常
- 第一种日常使用,主要说明第二种方式
- 当在executor抛出异常时,该错误会导致
Promise
的状态变为rejected
,并可以被后续的catch
方法捕获 - 我们写在
new Error
内的信息通常为错误类型及描述,和注释作用类似 - 而终端显示的信息主要来自
堆栈跟踪
,这部分信息提供了错误发生时的具体位置和调用栈路径。主要目的在于帮我们了解错误发生的上下文,以及调用过程中涉及的函数。例如,at F:\前端项目文件夹\coderwhy系列\JS\demo1.js:2:9
指明错误发生在demo1.js
文件的第 2 行,第 9 个字符位置,也就是我们抛出异常的位置 - 可以通过分析堆栈跟踪和错误信息,识别处理代码中的潜在问题或设计缺陷。而根据报错的情况,我们也可以进行生成日志记录、错误恢复等流程,以供后续处理分析,这在众多项目中都是存在的
- 当在executor抛出异常时,该错误会导致
- 这通常被认为是一种良好的实践;否则,执行捕获的部分将不得不对参数进行检查,以查看它是字符串还是错误,并且可能会丢失有用的信息,例如堆栈跟踪
const promise = new Promise((resolve, reject) => {
throw new Error('reject status')
})
promise.then().catch(err => {
console.log(err);
console.log('以上为错误信息');
})
图26-10 真实的报错信息
- 在不同地方所抛出的错误信息丰富度也不同,在executor阶段所抛出的是整个调用栈,而在then方法中抛出的错误信息就只有我们所抛出的部分:错误类型、错误信息(开发者给出的注释)、错误发生的具体位置
const promise = new Promise((resolve, reject) => {
resolve()
})
promise.then(res => {
throw new Error('reject status')
}).catch(err => {
console.log(err);
console.log('以上为错误信息');
})
// Error: reject status
// at F:\前端项目文件夹\coderwhy系列\JS\demo1.js:6:9
// 以上为错误信息
3.2.1 堆栈跟踪
堆栈跟踪(Stack Trace),也称为堆栈回溯(Stack Backtrace)或调用栈(Call Stack),是一个非常重要的调试工具,用于展示程序执行时函数调用过程中的函数栈帧。程序中发生异常或错误时,堆栈跟踪负责提供一系列信息,指明错误发生时程序调用链的具体点。让开发者快速定位问题原因,理解错误发生时程序的状态和上下文
- 想要看懂堆栈跟踪的信息,需要了解其内部包含几个组成部分:
- 函数调用列表:显示导致异常或错误的函数调用序列。这通常是从发生错误的函数开始,逐步回溯到程序执行的起点
- 文件信息:显示导致错误的代码所在的文件名,有时还包括路径
- 行号:指出错误发生的具体代码行
- 列号(在某些语言中可用):指出错误发生的具体列
- 错误信息:提供了抛出异常或错误的描述
- 堆栈跟踪的列表通常从发生错误的点开始,然后一层层向上追溯到调用栈的更早位置。这种结构可以帮我们看到错误发生时的直接原因,以及哪些函数调用导致了该错误
- JS堆栈跟踪的入口点在堆栈的最顶部,我们可以找到程序的入口点,即最初触发执行的函数,通常从这部分开始阅读
- 而在调用链中一般包含很多库函数或系统调用,重点关注自己项目中的函数通常更为重要,是定位错误最关键的部分
- 在我们这个抛出Eerror错误案例中,需要主动关注
错误描述
、错误发送具体位置
、错误上下文
,从上到下进行阅读,通过他们所在的行列号找到所在位置进行分析处理- 信息数量最多的Node内部模块加载则暂时忽略,这部分是库函数调用或系统调用(因为我们使用Node在终端运行,因此调用与Node有关的部分),显示了 Node.js 内部模块(如
cjs/loader
)如何处理 JavaScript 文件 - 初学阶段很容易因看不懂这部分庞大的内容而被劝退,事实上大多数时候我们并不需要关心它,因为我们不需要研究或调试Node.js本身的内部行为,和业务逻辑也无关
- 我们只需要聚焦应用级别的错误,而这通常与具体的业务逻辑、API使用不当或资源管理错误有关,也就是我们所说明需要主动关注的部分
- 信息数量最多的Node内部模块加载则暂时忽略,这部分是库函数调用或系统调用(因为我们使用Node在终端运行,因此调用与Node有关的部分),显示了 Node.js 内部模块(如
图26-11 堆栈跟踪的错误信息解析
- 除非错误明显来源于Node.js本身或者相关的系统层面的问题,否则普通开发者通常不具备修改Node.js内部实现的能力或权限。处理这类问题通常需要更深层次的系统编程知识或对Node.js内核有深入了解
3.2.2 拒绝捕获错误
- 在讲解什么是Promise时,我们有一个案例是分开调用then方法与catch方法,会出现报错问题,这是什么原因呢?
- 我们如果将then方法与catch方法分开调用,这两次调用是不会相互影响,而是互为独立
//p1与p2之间并没有任何联系,也不会相互影响
const p1 = myPromise.then(v => console.log(v))//我兑现了承诺
const p2 = myPromise.catch(err => console.log(err))//我拒绝了承诺
- 一旦互为独立,p1与p2就都是残缺版的处理方式,一个只处理兑现阶段返回的内容,一个只处理拒绝阶段返回的内容
- 一旦要
只能处理兑现阶段的p1去处理拒绝阶段内容
或者只能处理拒绝阶段的p2去处理兑现阶段内容
,就会出现无法处理的异常情况- 如果不为
Promise
提供正确的处理回调,任何由reject
或resolve
产生的错误都不会被处理,继续向下传播,最终导致运行时错误从而异常退出Node运行程序
- 如果不为
const myPromise1 = new Promise((resolve, reject) => {
//拒绝调用reject
reject('我拒绝了承诺')
});
const myPromise2 = new Promise((resolve, reject) => {
//兑现调用reject
resolve('我兑现了承诺')
});
//只能处理兑现阶段的p1去处理拒绝阶段内容
const p1 = myPromise1.then(v => console.log(v))//报错
//只能处理拒绝阶段的p2去处理兑现阶段内容
const p2 = myPromise2.catch(err => console.log(err))//报错
3.2.3 返回值
-
通过
拒绝捕获错误
,我们理解了诺言传递到Promise二阶段所具备的传递逻辑:then方法与catch方法都只能处理对应状态的信息,当下无法处理会继续往后传递-
因此像如下代码的返回值,在这次处理中,分为四阶段:
then(阶段一) => catch(阶段二) => then(阶段三) => catch(阶段四)
-
阶段一无法处理错误,直接跳到阶段二,如果在第二阶段已经捕获错误了,此时再返回内容,那打印出来的是阶段三的内容还是阶段四的内容?
-
// 4.catch方法的返回值
const promise = new Promise((resolve, reject) => {
reject("111111")//应该被catch所捕获
})
promise.then(res => {
console.log("res:", res)
}).catch(err => {
console.log("err:", err)
return "catch return value"
}).then(res => {
console.log("res result:", res)
}).catch(err => {
console.log("err result:", err)
})
- 答案是输出阶段三,在第二阶段处理好错误之后,catch方法会返回一个新的
Promise
,无论当前的 promise 状态如何,这个新的 promise 在返回时总是处于待定(pending)状态- 而由于我们在阶段二调用了onRejected,返回的 promise 将进入兑现状态(使用此调用的错误则进入拒绝状态)
- 因为阶段二正常返回内容(返回的Promise是兑现状态),触发阶段三而非阶段四
- 如果阶段二此调用引发了错误,则返回Promise拒绝状态,此时就会跳过阶段三,直接跳到符合接收拒绝状态的阶段四中,被跳过的阶段三所在内容将不再执行
//onRejected:一个在此 Promise 对象被拒绝时异步执行的函数。它的返回值将成为 catch() 返回的 Promise 对象的兑现值。此函数被调用时将传入以下参数:Promise 对象的拒绝值
catch(onRejected)
3.3 finally方法
- finally的含义是
最后,终于
,配合前两个所学实例方法,共同组成如下:- then(然后,那么) => catch(捕获) => finally(最后终于)
- 可以发现这些词汇具备一定的连贯性,也暗示了一定的使用顺序,then的含义作为"然后",具备一定的转折连贯效果,表示接着某种动作或情况之后,事实在Promise中也是如此,然后要进入Promise二阶段(fulfilled或rejected),而catch本身是then方法的一种语法糖,所代表的含义捕获就脱离不开then方法的一部分,而效果为捕获then的
rejected阶段
- 那finally方法的含义最后终于,听上去像是收尾,又像是最后登场的重要角色,那具体是怎么样的呢?让我们来学习一下吧!
- finally是在ES9(ES2018)中新增的一个特性:表示无论Promise对象无论变成fulfilled还是rejected状态,最终都会被执行的代码
- finally方法内只有一个异步执行函数,调用该函数时不带任何参数,因为无论前面是fulfilled状态,还是rejected状态,它都会执行,不管上一个阶段返回什么状态的Promise
- 该
finally规范
在Promise A+规范中也是没有的,和catch方法一样,依旧是ES6及之后由ECMA实现Promise时所扩展的特殊方法
const promise = new Promise((resolve, reject) => {
reject("coderwhy")//应该被catch所捕获
})
promise.then(res => {
console.log("res:", res)
}).catch(err => {
console.log("err:", err)
}).finally(() => {
console.log('一个最终一定会执行的方法--finally');
})
图26-12 一定会执行的finally方法
- 如果我们想在 promise 敲定时进行一些处理或者清理,无论其结果如何,那么
finally()
方法会很有用finally()
方法类似于调用then(onFinally, onFinally)
,因此无论是哪个状态都会执行- 同时需要注意
finally()
调用通常是透明的,不会更改原始 promise 的状态
//兑现状态
Promise.resolve(2).then(() => 77, () => {})//返回77
Promise.resolve(2).finally(() => 77)//返回2 finally不改变最后传递过来的Promise状态,也包括值
//拒绝状态
Promise.reject(3).then(() => {}, () => 88)//返回88
Promise.reject(3).finally(() => 88)//返回3 finally不改变最后传递过来的Promise状态,也包括值
- 因此所学的三实例方法,都是处于Promise阶段二,位于异步结束,信息传递后的过程
四、Promise类方法
前面我们学习的then、catch、finally方法都属于Promise的实例方法,都是存放在Promise的prototype上的
- 下面我们再来学习一下Promise的类方法
4.1 resolve/reject
- 有时候我们已经有一个现成的内容了,希望将其转成Promise来使用,这时候普通实现方式的代码会很冗余
//代码冗余
function foo() {
const obj = { name: "coderwhy" }
return new Promise((resolve, reject) => {
resolve(obj)
})
}
foo().then(res => {
console.log('res:', res);
})
- 这个时候我们可以使用 Promise.resolve 方法来完成
- Promise.resolve的用法相当于new Promise,并且执行resolve操作,相当于一种快捷方式来创建一个已经处于兑现(fulfilled)状态的 Promise
- 这个方法对于将现有数据转换为 Promise 对象,或者处理返回值需要是 Promise 的函数时非常方便
- 传入resolve的参数形态也分为三种:参数是普通值或对象、参数本身是Promise,参数是一个thenable
const promise = Promise.resolve({ name: "小余" })
//等价于
new Promise((resolve, reject) => resolve({ name: "小余" }))
//使用起来更加方便
promise.then(res => console.log('res:', res))
使用场景 | 描述 |
---|---|
转换普通值为 Promise | 当需要一个立即解决的 Promise 时,非常方便。常用于测试异步代码,模拟异步 API 的调用 |
链式调用 | 可用于启动一个 Promise 链,帮助避免多层嵌套的 Promise 代码结构,使代码更加简洁和易于管理 |
处理 thenable 对象 | 如果不确定一个对象是否是 Promise,使用 Promise.resolve 可以确保得到一个 Promise 对象,统一处理所有的异步或同步操作 |
确保 Promise 完整性 | 在与第三方库交互时,如果不确定返回的是不是一个 Promise,使用 Promise.resolve 可以保证继续操作的是一个 Promise,确保异步操作的完整性和可预测性 |
reject类方法
在表达形式上与resolve类方法
相似,使用方式也一致
const promise = Promise.reject("rejected message")
//相当于
const promise2 = new Promsie((resolve, reject) => {
reject("rejected message")
})
- 但reject类方法的参数与resolve类方法不同,不分三种情况,因此返回任何值,都是一样的流程
- 无论
reason
是什么类型的值(包括 Promise、thenable,或任何其他类型),Promise.reject()
都会返回一个状态为 rejected 的新 Promise,并且这个 Promise 的拒绝理由将是传入的reason
- 与
Promise.resolve()
需要根据不同类型的值做出不同响应的复杂逻辑不同,Promise.reject()
总是简单地把任何传入的值当做拒绝的理由,并且不进行任何额外的处理或状态检查
- 无论
// 注意: 无论传入什么值都是一样的,都当作一个Promise
const promise = Promise.reject(new Promise(() => {}))
promise.then(res => {
console.log("res:", res)
}).catch(err => {
console.log("err:", err)
})
- 这种设计选择可能是为了确保当开发者需要明确表示一个操作失败时,可以直接和无歧义地表达这一点,而不必担心输入值的类型可能影响到 Promise 的行为,所以哪怕传递进一个thenable也不必担心被当作Promise处理
4.2 all/allSettled
Promise.all()
方法用于将多个 Promise 实例包装成一个新的 Promise 实例,是Promise并发方法之一- 输入:接收一个 Promise 对象的数组或可迭代对象作为参数
- 输出:返回一个新的 Promise 实例
- 处理:只有当所有输入的 Promise 实例都变为
fulfilled
状态时,返回的 Promise 才会变为fulfilled
状态,此时输入的所有 Promise 的结果组成一个数组传递给fulfilled
回调 - 错误处理:如果任意一个输入的 Promise 实例变为
rejected
状态,Promise.all()
返回的 Promise 实例立即变为rejected
状态,且第一个被reject
的实例的返回值传递给rejected
回调
- 通俗的说,将多个Promise合在一起,都有结果了再一起返回,这个"合"在一起,将多个Promise视为一个整体,一旦有一个Promise是拒绝状态,则一起返回时只走接收拒绝状态的catch方法
- 关系上类似
连坐
,一人出事,众人并罚 - 返回值除了兑现或拒绝状态外,当传入内容为空,视为已兑现
- 在实际应用中也是有经常使用的,假设有三个网络请求,我们希望这三个网络请求都要有结果的时候,就可以通过这种方式
- 关系上类似
const p1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("p1 resolve")
},3000)
})
const p2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("p2 resolve")
},2000)
})
const p3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("31 resolve")
},5000)
})
//all:所有
Promise.all([p1,p2,p3]).then(res => {
//这条内容会在5秒后执行,因为要等所有的promise都决议了,这个才会决议,且结果是数组格式
console.log("all promise res:",res);//all promise res: (3) ['p1 resolve', 'p1 resolve', 'p1 resolve']
})//将内容放入可迭代的数组中
//上面一共创建了4个promise,前三个是p1,p2,p3,最后一个则是promise.all,这最后一个的状态由前三个决定(三个都出结果后才会执行最后一个决议的结果)
//如果p1就出现了错误reject,会怎么进行?
const p1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
reject("p1 reject error")
},3000)
})
const p2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("p2 resolve")
},2000)
})
const p3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("p3 resolve")
},5000)
})
Promise.all([p1,p2,p3]).then(res => {
//这条内容会在5秒后执行,因为要等所有的promise都决议了,这个才会决议,且结果是数组格式
console.log("all promise res:",res);//all promise res: (3) ['p1 resolve', 'p1 resolve', 'p1 resolve']
}).catch(err =>{
console.log("all promise err:",err)//如果中途出现了reject错误,就会马上中止继续往下读取并通过catch返回错误信息
})
- 如果所有Promise都处于兑现状态,通过判断,返回数据是数组形式,数据顺序由传入all方法的Promise顺序为准,而非异步调用得出结果速度决定
- 但all方法有一个缺陷:当有其中一个Promise变成reject状态时,新Promise就会
立即变成对应的reject状态
- 那么对于resolved的,以及依然处于pending状态的Promise,我们是获取不到对应的结果的
- 在ES11(ES2020)中,添加了新的API
Promise.allSettled
- 该方法会在所有的Promise都有结果(settled),无论是fulfilled,还是rejected时,才会有最终的状态
- 并且这个Promise的结果一定是fulfilled的
Promise.allSettled([p1,p2,p3]).then(res => {
console.log("all promise res:",res)
}).catch(err =>{//allSettled永远不会走catch方法
console.log("all promise err:",err)
})
- 与all方法的返回结果不同,allSettled的结果在一个数组的基础上,继续存放着每一个Promise的结果,并且是对应每一个对象的
- 这个对象中兑现状态的结果为status状态,以及对应的value值,拒绝状态的结果为status状态,以及对应的reason值(错误理由),在此基础上我们就能够通过判断status状态进行针对性处理我们所需要的部分
// all promise res: [
// { status: 'rejected', reason: 'p1 reject error' },
// { status: 'fulfilled', value: 'p2 resolve' },
// { status: 'fulfilled', value: 'p3 resolve' }
// ]
4.3 # race/any
- 如果有一个Promise有了结果,我们就希望决定最终新Promise的状态,那么可以使用race方法
- race是竞技、竞赛的意思,表示多个Promise相互竞争,谁先有结果,那么就使用谁的结果
- 在处理多个相互独立的异步操作时,
Promise.race()
允许在多个操作中快速选出胜者。这在多源数据获取或具有多个备用请求的网络请求中特别有用,可以减少等待时间快速响应,提高应用性能
const p1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
reject("p1 reject error")
},3000)
})
const p2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("p2 resolve")
},2000)
})
const p3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("p3 resolve")
},5000)
})
//类方法,reve方法
Promise.race([p1,p2,p3]).then(res => {
console.log("res:",res)
}).catch(err =>{
console.log("err:",err)
})
//res: p2 resolve,输出最早打印出来的
- 且
Promise.race()
非常适合用于实现超时机制。可以将一个异步操作的 Promise 和一个超时的 Promise 放在一起,如果原始操作在超时前完成,则继续处理结果;如果超时 Promise 先完成,则可以实施超时逻辑,如取消操作或返回超时错误 - 这里也能够看出race方法的一个特性:一旦其中一个Promise有了结果就会立刻返回,不管该结果是Promise的哪个状态
//自定义超时时间的Promise
let timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Operation timed out')), 1000));
//真正需求的异步操作
let asyncOperation = fetch('/data');
Promise.race([asyncOperation, timeoutPromise])//超时则调用超时逻辑
.then(data => console.log(data))
.catch(error => console.error(error));
-
因此race方法通常用来在原有基础上用来构建更加健壮的代码
-
但此时会延伸出来另一个问题,race方法一旦报错就整个运行都结束了,如果我希望能够返回兑现的结果,只是为了筛选其中最快的部分,我又应该怎么做?
-
这就需要说到any方法了,any方法是
ES12中新增的方法
,和race方法是类似的:- any方法会等到一个fulfilled状态,才会决定新Promise的状态
- 如果所有的Promise都是reject的,那么也会等到所有的Promise都变成rejected状态
Promise.any([p1,p2,p3]).then(res => {
console.log("res:",res)
}).catch(err =>{
console.log("err:",err)
})
- 因此race与any的区别,在于目标导向不同
- race追求第一个结果
- any方法追求第一个好的结果
- 只要追求到所需结果,就立刻返回内容。如果没有所需结果,比如any方法内的所有的Promise都是reject的,那么会报一个AggregateError的错误
- 以上就是所有所学的对象方法与类方法了,日常大多运用以对象三方法为主,但类方法也会有所使用,尤其在编写工具函数阶段,更多的学习使用就需要放在编写正式项目中的网络请求中进行
后续预告
- 在掌握了Promise的基础使用方式后,我们在下一章节中,会进行手写Promise
- 手写Promise的难度很大吗?其实未必见得,但其中的
规范
一定很重要 - 在本章节中,不断的提到了Promise A+规范,但到目前为止我们还没有接触到,因为该规范规定了一个Promise应该如何书写,这是想要手写Promise所避不开的事情
- 只有统一的规范,才能让大家一上手我所写的Promise就知道如何使用,而非再去探索该Promise的内部源码
- 手写Promise的难度很大吗?其实未必见得,但其中的