详解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的结果是undefined
    console.log(res)
    return '123'
  }).then(function(res) {
    //该res的结果是123
    console.log(res)
    return new Promise(function(resolve) {
        resolve(456)
    })
  }).then(function(res) {
    //该res的结果是456
    console.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()。

  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值