Promise期约
Promise期约
偏原理向,方法使用移步 https://www.cnblogs.com/xt112233/p/15137255.html
链式写法
期约的错误写法(fetch会返回一个期约),大量的嵌套回调函数,违背了期约的初衷
fetch("/api/user/profile").then(response =>{ response.json().then(profile => { displayUserProfile(profile) }) })
期约链的正确写法
fetch("/api/user/profile") .then(response=>{ return response.json() }).then(profile => { displayUserProfile(profile) })
期约链剖析
代码简化如下:
fetch(theUrl) // 任务1,返回期约1 .then(callback1) // 任务2,返回期约2 .then(callback2) // 任务3,返回期约3
理解工作流程(并不复杂,耐心!):
1.第一行,调用fetch方法,向指定URL发送请求,我们称HTTP请求为 任务1,返回的期约为 期约1
2.第二行,调用 期约1 的then()方法,传入callback回调函数,我们希望这个回调函数在 期约1 被兑现时调用,任务2 在callback被调用时开始,并返回一个新期约 期约2
3.第三行,调用 期约2 的then()方法,传入callback回调函数,我们希望这个回调函数在 期约2 被兑现时调用,任务3 在callback被调用时开始,并返回一个新期约 期约3,但由于 期约3 不会再被使用,所以 期约3 无意义
此时看上面的详细代码(非简化代码):
4.当这个表达式开始执行时,前3步将同步发生。然后再第1步创建的HTTP请求通过互联网发出时有一个异步暂停
5.HTTP响应开始到达。fetch()调用的异步逻辑将HTTP状态和头部包装到一个Response对象中,并将这个对象作为值兑现 期约1
6.期约1 兑现后,它的值(Response对象)会传给callback1()函数,此时 任务2 开始,这个任务是以给定Response对象作为输入,获取JSON格式的响应体
7.假设 任务2 正常结束,成功解析HTTP响应体并生成一个JSON对象。然后这个JSON对象被用于兑现 期约2
8.兑现 期约2 的值传给callback2()函数,做为 任务3 的输入,然后 任务3 以某种形式把数据显示给用户。任务3 完成时,期约3 也会兑现,但由于我们未注册回调,所以什么也不会发生,异步计算链结束
返回结果
此时引出了一个问题,当任务2计算完毕,返回一个值时,期约2得以兑现,开始执行任务3。但任务2计算完毕,返回一个期约,会发生什么呢?
此时,期约2的确得到了解决,但并未兑现,而是将“命运”交给了另一个期约,即一个期约与另一个期约发生了关联,此时我们无法预知结果,任务3也无法进行。
如下代码所示,当c1返回p4时,p2期约得到了解决,但解决并不代表兑现,.json()方法要等到HTTP响应体全部可用时,才可以执行并解析,才可以返回p4,才可以兑现p2,执行c2,任务3才开始
function c1(response){ let p4 = response.json() return p4 } function c2(profile){ displayUserProfile(profile) } let p1 = fetch("/api/user/profile") let p2 = p1.then(c1) let p3 = p2.then(c2)
catch和finally
catch
在同步代码中,发生错误通常可以追踪,但在异步代码中,未处理的异常往往不会得到报告,期约的catch()方法实际上是对以null为第一个参数、以错误处理回调为第二个参数的.then()调用的简写,对于任何期约p和回调c,以下两行代码是等价的
p.then(null, c) p.catch(c)
finally
在ES2018中,期约对象还定义了finally,.finally()的回调无需传参,无论上一任期约兑现还是拒绝,都会调用,假如需要做一些清理工作,.finally()是个很好的方式
catch使用
运用catch捕捉错误,先上代码
fetch("/api/user/profile") .then(response =>{ if(!response.ok){ return null } // 检查头部 let type = response.headers.get("content-type") if(type !== "application/json"){ throw new TypeError(`Expected JSON, got ${type}`) } return response.json() }).then(profile => { if(profile){ displayUserProfile(profile) }else{ // 如果遇到了404错误或返回null,则会走这里 displayLoggedOutProfilePage() } }).catch(e => { if(e instanceof NetworkError){ // fetch()在连接互联网时故障会走这里 displayErrorMessage("Check your internet connection") }else if(e instanceof TypeError){ // 在上面抛出TypeError时会走这里 displayErrorMessage("Something is wrong with server") }else{ console.error(e) } })
简化代码:
let p1 = fetch(url) let p2 = p1.then(c1) let p3 = p2.then(c2) p3.catch(c3)
分析:
正常线路:
p1是fetch返回的期约,p2是第一个.then()返回的期约,c1是其回调,p3是第二个.then返回的期约,c2是其回调,最后c3是传给.catch()的回调,这个回调也会返回期约但我们并没有使用
异常捕获:
1.fetch出现网络故障,此时p1会以NetworkError对象被拒绝,然后一路拒绝到最后被catch捕获
2.HTTP请求返回了404或其他HTTP错误,这些都是有效的HTTP响应,都会被封装到Response对象中并用来兑现p1,c1被调用,检查response.ok,发现并未收到一个正常的HTTP响应,返回null,因为null不是期约,所以会立即调用c2,c2发现profile是null,向用户反馈,这是将反常条件做为非错误处理程序处理的一个示例
3.在c1中,我们拿到了HTTP响应码,但响应头格式错误,会发生更严重的错误,所以此处检查响应头,抛出TypeError,如果传给.then()的回调抛出一个值,则该.then()返回的期约会以这个抛出的值被拒绝,最后被catch捕获
一些错误处理方法
传给catch()的回调只会在上一环抛出错误时才会被调用,否则就会被跳过直接执行下一个then(),所以,catch()可以放在中间使用帮助解决一些问题,从而停止错误的传播。
如下,假如数据库访问会因为网络问题有一定几率失败,则可以再次重新访问一次
queryDatabase() .catch(e => { return wait(500).then(queryDatabase) }) .then(displayTable) .catch(displayDatabaseError)
并行期约
Promise.all
Promise.all(),传入一个期约对象的数组,返回每个期约兑现值组成的数组,若其中一个期约拒绝,则全部拒绝
const urls = [/* 多个url */] const promises = urls.map(url => { return fetch(url) }) Promise.all(promises) .then(bodies=>{ /* 处理返回值的数组 */ }) .catch(e => console.error(e))
Promise.allSettled
Promise.allSettled()也接收一个输入期约的数组,与Promise.all类似,但不会拒绝返回的期约,等所有期约全部兑现完毕,返回一个对象数组,数组中对象都有status属性,代表兑现或拒绝,如果兑现,则有value属性,若拒绝,则有reason属性,包含拒绝的理由
Promise.allSettled([Promise.resolve(1), Promise.reject(2), 3]).then(result=>{ result[0] // {status:"fulfilled", value:1} result[1] // {status:"rejected", reason:2} result[2] // {status:"fulfilled", value:3} })
promise.race
若同时运行多个期约但只关心第一个返回的期约值,可以使用Promise.race(),会在输入数组中的期约有一个兑现或拒绝时马上兑现或拒绝(或第一个非期约值被直接返回)
创建一个期约
Promise(resolve, reject)构造函数用来创建一个全新的期约对象,第一个参数resolve代表解决或兑现返回的期约或值,第二个参数reject代表拒绝兑现期约,我们来实现上面的wait()函数
const wait = function(duration){ return new Promise(res, rej=>{ // 如果传入的时间小于0,拒绝兑现期约 if(duration < 0){ rej(new Error("Time error")) } // 否则,异步等待相应时间后,解决期约 // resolve未传值,所以兑现值为undefined,但这不重要 setTimeout(res, duration) }) }
串行期约
Promise.all()可以并行执行任意数量的期约,期约链则可以表达一连串固定数量的期约,不过,按顺序运行任意数量的期约有点棘手,需要动态构建
function fetchSequentially(urls){ // 存储兑现结果 const bodies = [] // 这个函数返回一个待兑现的期约 function fetchOne(url){ return fetch(url) .then(response => response.text()) .then(body =>{ // 兑现结果保存数组中,无需返回值(undefined) bodies.push(body) }) } // 创建一个初始期约 let p = Promise.resolve(undefined) // 通过期约动态的增加期约构建期约链,但顺序不会乱,是依次执行的 for(let url of urls){ p = p.then(()=>{ fetchOne(url) }) } // 此处为期约链最后一个期约,会兑现按顺序执行兑现的值的数组 return p.then(()=>{ return bodies }) }