在上文《详解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()。