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
    })
}

 

posted @ 2021-12-19 22:34  邢韬  阅读(80)  评论(0编辑  收藏  举报