为什么我推荐使用智能化async?

12,964 阅读12分钟

我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!

摘要

开发中无论怎样都会产生网络请求,这样一来自然也就避免不了大量使用thencatchtry catch来捕获错误,而捕获错误的代码量是随着网络请求的增多而增多,那应该如何优雅的系统性捕获某个网络请求中所产生的所有错误呢?

首先最常用的两种处理网络请求的形式即Promiseasync(事实上很多请求库都是基于这两者的封装),使用Promise那必然要与thencatch挂钩,也就是说每个请求都对应一个Promise实例,然后通过该实例上对应的方法来完成对应的操作,这应该算是比较常用的一种形式了

但如果涉及嵌套请求,那可能还要不断的增加thencatch来完成需求,好了,现在可以使用看起来真的像同步编程的async来着手优化了,即await promise,那这种情况下就根本不需要手动then了,但如果await promise抛出了错误呢?那恐怕不得不让try catch来帮忙了,而如果也是嵌套请求,那与Promise写法类似的问题又来了,有多少次请求难道我就要多少次try catch吗?那这样看来的话,Promiseasync在面对这种屎山请求的时候确实有点心有余而力不足了

前言

之所以写作本篇文章是因为前几天在优化数据库操作时,发现要不停try catch,且操作数据库的代码越多,则try catch就越多,于是突发奇想,能不能封装一个工具类来实现智能化捕获错误呢?在这种思维的推动下,我觉得这个工具类不仅仅是以一种创意的形式出现,更多的是实用性!(先不考虑这个创意能否实现)

一个令人头疼的需求

家在吉林的小明想去海南看望他的老奶奶,但小明觉得旅途如此之长,不如先去山东学习学习马保国老师的“接化发”,然后再去云南拍一个“我是云南的 云南怒江的...”的视频发一下朋友圈,最后再去海南看望老奶奶

请你运用所学知识帮帮小明,查询吉林--山东--云南--海南的车票还有吗?

  • 如果有的话,老奶奶希望小明不要在车票上花费太多的钱,所以当小明出发时,需要告诉老奶奶本次所有车票的开销是多少
  • 如果没有的话,请你务必告诉小明是哪里的车票没有了,因为小明可能会换个路线去找老奶奶

注意,当确定吉林-山东的车票未售空时才去查询山东-云南的车票是否已售空,并以此类推;因为这样的话,小明可以知道是哪个地方的车票没有了,并及时换乘

虽然吉林--山东--云南--海南的车票可以一次性查询完毕,但为了体现嵌套请求的复杂度,我们此处不讨论并发请求的情况,关于并发,你可以使用Promise.all

flow_chart.png

先来细化题目,可以看到路线依次为:吉林-山东山东-云南云南-海南,也就分别对应三个请求,且这三个请求又是嵌套发出的。而每次发出的请求,最终都会有两种情况:请求成功/失败,请求成功则代表本轮次车票未售空请求失败则代表本轮次车票已售空

之所以请求失败对应车票已售空,是为了模拟请求失败的情况,而不是通过返回一个标识来代表本轮次车票是否已售空

这个令人头疼的需求,我建议你再认真读一遍

准备工作

为了简单起见,这里就不额外开启一台服务器了,转而使用定时器模拟异步任务

以下是用于查询车票的接口,我们称之为请求函数

在下文中所指的请求函数就是requestJS、requestSY、requestYH

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, true, true]

// 查询 吉林-山东 的车票是否已售空的接口
const requestJS = () => new Promise((res, rej) => {
    setTimeout(() => {
        // 请求成功(resolve)则代表车票未售空
        if (interface[0]) return res({ ticket: true, price: 530, destination: '吉林-山东' })
        // 请求成功(rejected)则代表车票已售空
        rej({ ticket: false, destination: '吉林-山东' })
    }, 1000)
})
// 查询 山东-云南 的车票是否已售空的接口
const requestSY = () => new Promise((res, rej) => {
    setTimeout(() => {
        if (interface[1]) return res({ ticket: true, price: 820, destination: '山东-云南' })
        rej({ ticket: false, destination: '山东-云南' })
    }, 1500)
})
// 查询 云南-海南 的车票是否已售空的接口
const requestYH = () => new Promise((res, rej) => {
    setTimeout(() => {
        if (interface[2]) return res({ ticket: true, price: 1500, destination: '云南-海南' })
        rej({ ticket: false, destination: '云南-海南' })
    }, 2000)
})

Promise

一定要避免重复造轮子,所以先用Promise实现一下,看看效果如何,然后再决定应该怎么操作

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, true, true]

// 先查询吉林到山东
requestJS()
    .then(({ price: p1 }) => {
        console.log(`吉林-山东的车票未售空,价格是 ${p1} RMB`)
        // 如果吉林-山东的车票未售空,则继续查询山东-云南的车票
        requestSY()
            .then(({ price: p2 }) => {
                console.log(`山东-云南的车票未售空,价格是 ${p2} RMB`)
                // 如果山东-云南的车票未售空,则继续查询云南-海南的车票
                requestYH()
                    .then(({ price: p3 }) => {
                        console.log(`云南-海南的车票未售空,价格是 ${p3} RMB`)
                        console.log(`本次旅途共计车费 ${p1 + p2 + p3} RMB`)
                    })
                    .catch(({ destination }) => {
                        console.log(`来晚了,${destination}的车票已售空`)
                    })
            })
            .catch(({ destination }) => {
                console.log(`来晚了,${destination}的车票已售空`)
            })
    })
    .catch(({ destination }) => {
        console.log(`来晚了,${destination}的车票已售空`)
    })

测试结果如下

promise1.gif

不错,符合预期效果,现在来将第二次请求变为失败(即山东-云南请求失败)

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, false, true]

现在再来看结果

promise2.gif

依然符合预期效果,但这种方式嵌套的层级太多,一不小心就会成为屎山的必备条件,必须优化一下

由于then会在请求成功时触发,catch会在请求失败时触发,而无论是thencatch都会返回一个Promise实例(return this),我们也正是借助这个特性来实现then的链式调用

如果then方法没有返回值,则默认返回一个成功的Promise实例,而下面代码则手动为then指定了其需要返回的Promise实例。无论其中哪个Promise的状态更改为失败,都会被最后一个catch所捕获

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, true, false]

let acc = 0
// 先查询吉林到山东
requestJS()
    .then(({ price: p1 }) => {
        acc += p1
        console.log(`吉林-山东的车票未售空,价格是 ${p1} RMB`)
        // 如果吉林-山东的车票未售空,则继续查询山东-云南的车票
        return requestSY()
    })
    .then(({ price: p2 }) => {
        acc += p2
        console.log(`山东-云南的车票未售空,价格是 ${p2} RMB`)
        // 如果山东-云南的车票未售空,则继续查询云南-海南的车票
        return requestYH()
    })
    .then(({ price: p3 }) => {
    	// 能执行到这里,就说明前面所有请求都成功了
        acc += p3
        console.log(`云南-海南的车票未售空,价格是 ${p3} RMB`)
        console.log(`本次旅途共计车费 ${acc} RMB`)
    })
    .catch(({ destination }) => console.log(`来晚了,${destination}的车票已售空`))

promise3.gif

可以看到经过优化后的Promise已经把屎山磨平了一点,美中不足的就是如果想要计算总共花费的车费,那么需要在外部额外声明一个acc用来统计数据,其实这种情况可以对请求车票数据的函数requestJS等来和每次then的返回值进行简单包装,但在此处,我不想改动请求车票数据的函数体,至于为什么,我们继续往下看

async

既然Promise都说了,也是时候把async这位老大哥请出来帮帮场子了,不多赘述,我们来看async会怎么处理这种嵌套请求

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, true, true]

const f = async () => {
    try {
        const js = await requestJS()
        console.log(`吉林-山东的车票未售空,价格是 ${js.price} RMB`)
        const sy = await requestSY()
        console.log(`山东-云南的车票未售空,价格是 ${sy.price} RMB`)
        const yh = await requestYH()
        console.log(`云南-海南的车票未售空,价格是 ${yh.price} RMB`)
        console.log(`本次旅途共计车费 ${js.price + sy.price + yh.price} RMB`)
    } catch ({ destination }) {
        console.log(`来晚了,${destination}的车票已售空`)
    }
}

f()

async1.gif

要么怎么称它为老大哥呢,不得不说,果然老练啊,基本不用怎么优化就已经磨平了一点屎山

其实async与上面Promise的第二种写法有异曲同工之妙,可以看做都是将所有成功的逻辑放在了一起,仅仅使用了一个catch便可以捕获所有错误,不得不说,真是妙蛙种子吃着妙脆角进了米奇妙妙屋,妙到家了

但,你以为今天的文章就到这了吗?大错特错,正是因为这种重复性catch,所以才会萌生出自己封装一个智能化捕获函数来处理这种情况。上面所讲到的Promiseasync其实已经是很常见的一种写法了,但如果项目中存在第二种嵌套请求(比如先请求所在省份的天气,再请求所在县的天气)。如果放在async面前,我想它一定会使用两个f函数,一个为查询小明车票,一个为查询天气,那这就避免不了要写两个try catch了,文章开头我所说到的对数据库的操作大概就是这种困惑

现在来解开谜底,分享一下我是如何在有想法--确定目标--开始实现--遇到问题--解决问题--达到目标这种模式的推动下来一步一步完成的函数封装

如果你对上述Promise和async有更好的优化方式,请分享在评论区 期待你的最优解

combine-async-error心路历程

要解决一个问题,首先要明白解决它的意义何在。在小明看望老奶奶这个问题中,我们正是被这种不停地catch所困惑,所以才要想出更好的办法去优化它。于是我就想着能不能封装一个函数来替我完成所有的catch操作呢?既然这种念头已经有了,那就开始动手实现

捡捡之前的知识

在封装之前,你必须要知道以下知识点

try catch 不能捕获异步错误

// 可以捕获
try{
    throw ReferenceError('对象 is not defined')
}catch(e) {
    console.log(e)
}
// 不可以捕获
try{
    setTimeout(() => {
	throw ReferenceError('对象 is defined')
    })
}catch(e) {
    console.log(e)
}

Generator

你可以把Generator函数称作生成器,调用生成器函数会返回一个迭代器来控制这个生成器执行其代码,在生成器中你可以使用yield关键字,理论上yield可以出现在任何能求值的地方,我们通过迭代器的next方法来确保生成器始终是可控的

const f = function* () {
    console.log(1)
    // 注意yield只能出现在Gerenator函数中
    // 如果你将yield写在了回调里,请一定要确认这个回调是一个生成器函数
    yield
    console.log(2)
}
f().next()

// 1

async

async函数在执行时,遇到await会交出“线程”,转而去执行其它任务,且await总是会异步求值

const f = async () => {
    console.log(1)
    await '鲨鱼辣椒'
    console.log(3)
}
f()
console.log(2)

// 1 2 3

如果你对上面几个题目还存在疑问,请在《JavaScript每日一题》专栏中找到对应的题目进行练习

好了,现在开始由浅入深逐步分析

让await永远不要抛出错误

await永远不要抛出错误,这也是最重要的前提

// getInfo为获取车票信息的功能函数
const getInfo = async () => {
    try{
        const result = await requestJS()
        return result
    }catch(e){
        return e
    }
}

await右边是获取吉林-山东车票信息的函数requestJS,该函数会返回一个promise对象,当这个promise对象的状态为成功时,await会把成功的值赋给result,而当失败时,会直接抛出错误,一般我们会在await外包裹一层try catch来捕获可能出现的错误,那能不能不让await抛出错误呢?

很明确的告诉你,可以,只需要封装一下await关键字即可

保证不抛出错误

// noErrorAwait负责拿到成功或失败的值,并保证永远不会抛出错误!
const noErrorAwait = async f => {
    try{
        const r = await f()
        return {flag: true, data: r}
    }catch(e) {
        return {flag: false, data: e}
    }
}

const getInfo = () => {
    const result = noErrorAwait(requestJS)
    return result
}

在noErrorAwait的catch里请不要进行一些副作用操作,除非你真的需要那些东西

有了noErrorAwait的加持,getInfo可以不再是一个async函数了,但此时的getInfo仍会返回一个promise对象,这是因为noErrorAwaitasync函数的缘故。封装到这里,noErrorAwait已经实现了它的第一个特点——保证不抛出错误,现在来把getInfo补全

const noErrorAwait = async f => {
    try{
        const r = await f() // (A)
        return {flag: true, data: r}
    }catch(e) {
        return {flag: false, data: e}
    }
}

const getInfo = () => {
    const js = noErrorAwait(requestJS) // (B)
    console.log(`吉林-山东的车票未售空,价格是 ${js.data.price} RMB`)
    const sy = noErrorAwait(requestSY) // (C)
    console.log(`山东-云南的车票未售空,价格是 ${sy.data.price} RMB`)
    const yh = noErrorAwait(requestYH) // (D)
    console.log(`云南-海南的车票未售空,价格是 ${yh.data.price} RMB`)
    console.log(`本次旅途共计车费 ${js.price + sy.price + yh.price}`)
}

我们分别为(B)、(C)、(D)所对应的请求函数都套上了一层noErrorAwait,正是由于这种缘故,我们可以在getInfo中始终确保(B)、(C)、(D)下的请求函数不会报错,但致命的问题也随之到来,getInfo会确保请求函数是顺序执行的吗?

仔细看一遍就会发现getInfo是不负责顺序执行的,甚至可能会报错。这是因为noErrorAwaitawait关键字的缘故,现在手动执行一下分析原因

  • 调用getInfo
  • 调用noErrorAwait并传递参数requestJS
  • 来到noErrorAwait中,由于noErrorAwaitasync函数,所以会返回一个promise对象
  • 执行await f(),这个f就是requestJS,由于requestJS是一个异步任务,所以交出本次“线程”,也就是从(A)跳到(B)的下方,打印js.data.price,结果发现抛出了TypeError
  • 抛出TypeError的原因是因为(B)的变量js是一个初始化状态的promise对象,所以说访问初始化中的数据怎么可能不报错!

那问题来了,noErrorAwait只负责让所有的请求函数都不抛出错误,但它并不能确保所有请求函数是按顺序执行的,如何才能让它们按照顺序执行呢?

难不成又要把getInfo变回async函数,然后再通过await noErrorAwait(...)的形式来确保所有请求函数是按照顺序执行的,果然鱼与熊掌不可得兼,如果真的使用这种方式,那await noErrorAwait(...)如果抛出了错误,谁来捕获呢?总不能在它外面再套一层noErrorAwait

保证顺序执行

这个想法实现到这里,其实已经出现了很大的问题了——“保证不抛出错误”和“顺序执行”不能同时成立,但也不能遇到bug就关机睡觉呀。这个问题当时我认真思考过,期间不泛breakProxy等其它骚操作,在束手无策的时候,我突然想到了它的表哥——Generator,由于生成器是可控的,我只需要在上一次请求完成时,调用next发起下一次请求,这不就可以解决了吗,确实是不错的想法,现在来试试

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, true, true]

const noErrorAwait = async f => {
    try{
        const r = await f()
        generator.next({flag: true, data: r})
    }catch(e) {
        return {flag: false, data: e}
    }
}

const getInfo = function*() {
    const js = yield noErrorAwait(requestJS)
    console.log(`吉林-山东的车票未售空,价格是 ${js.data.price} RMB`)
    const sy = yield noErrorAwait(requestSY)
    console.log(`山东-云南的车票未售空,价格是 ${sy.data.price} RMB`)
    const yh = yield noErrorAwait(requestYH)
    console.log(`云南-海南的车票未售空,价格是 ${yh.data.price} RMB`)
    console.log(`本次旅途共计车费 ${js.data.price + sy.data.price + yh.data.price}`)
}

const generator = getInfo()
generator.next()

先来看测试结果

generator1.gif

当请求全部成功时,所有数据都拿到了,不得不说,这一切都要归功于yield关键字

noErrorAwait感知到请求函数成功时,会调用next,从而推动嵌套请求的发起,而且也不用担心生成器在什么时候执行完,因为一个noErrorAwait总会对应着一次next,这样一来getInfo就差不多已经在掌控之中了,但有个致命的问题就是:noErrorAwait感知到错误时,应该如何处理?如果继续调用next,那就与不用生成器没有区别了,因为始终都会顺序执行,解决办法就是传递一个函数,在noErrorAwait感知到错误时调用该函数,并且把出错的请求函数之前的所有请求结果全部传递进去,这样当这个回调执行时,便代表某一个请求函数抛出了错误

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, false, true]

// 存储每次的请求结果
const result = []
// 失败的回调(不要关心callback定义在哪里,以及如何传递)
const callback = (...args) => console.log('某个请求出错了,前面收到的结果是', ...args) // (A)
const noErrorAwait = async f => {
    try{
        const r = await f()
        const args = {flag: true, data: r}
        result.push(args)
        generator.next(args)
    }catch(e) {
        const args = {flag: false, data: e}
        result.push(args)
        callback(result)
        return args
    }
}

const getInfo = function*() { // (B)
    const js = yield noErrorAwait(requestJS)
    console.log(`吉林-山东的车票未售空,价格是 ${js.data.price} RMB`)
    const sy = yield noErrorAwait(requestSY)
    console.log(`山东-云南的车票未售空,价格是 ${sy.data.price} RMB`)
    const yh = yield noErrorAwait(requestYH)
    console.log(`云南-海南的车票未售空,价格是 ${yh.data.price} RMB`)
    console.log(`本次旅途共计车费 ${js.data.price + sy.data.price + yh.data.price}`)
}

const generator = getInfo() // (C)
generator.next() // (D)

通过测试可以发现当第二个请求函数抛出了错误时,noErrorAwait可以完全捕获,并及时通过callback向用户返回了数据

generator2.gif

这样就实现了一个功能较为齐全的处理嵌套请求的函数了,但仔细看看就会发现,代码中的(A)、(B)、(C)、(D)(包括(B)中的所有yield)都是由用户自定义的,也就是说,每次用户在使用这段处理嵌套请求的逻辑之前,都必须要自定义上面四处代码,那这样一来这个功能就变的极其鸡肋了,不仅对用户来说很头疼,就连开发者也落不到一个好的口碑

既然没有达到理想层面,那就说明还需要努力优化

是时候解决掉所有问题了

开始封装

通过上面的种种问题,就能得出自己的经验和教训,要么优化好了,但不能顾及其它情况;要么完成了功能,但使用起来的体验极其差劲。现在就来封装一个combineAsyncError函数,这个函数会完成所有的逻辑处理及调度,而用户则只需要传递请求函数即可

combineAsyncError即字面意思,捕获异步错误,当然它也可以捕获同步错误

使用形式

const combineAsyncError = tasks => {}
const getInfo = [requestJS, requestSY, requestYH]

combineAsyncError(getInfo)
    .then(data => {
        console.log('请求结果为:', data)
    })

combineAsyncError接收一个由请求函数所构成的数组,该函数会返回一个Promise对象,其then方法被执行时,就代表嵌套请求结束了(有可能因为成功而结束,亦有可能因为失败而结束),不过不要担心,因为data的值始终为{ result, error },如果error存在则代表请求失败,反之成功

完成combineAsyncError的返回值

const combineAsyncError = tasks => {
    return new Promise(res => handler(res))
}

当调用res时,会通知当前的Promise实例去执行它的then方法,而res也正是杀手锏,只需在请求失败或全部请求成功时调用res,这样then就会知道嵌套请求的逻辑执行完毕

combineAsyncError的初始化工作

handler中完成处理请求函数的逻辑。也就是操作Generator函数,既然这里要使用生成器,那就很有必要做一下初始化工作

const combineAsyncError = tasks => {
const doGlide = {
    node: null, // 生成器节点
    out: null, // 结束请求函数的执行
    times: 0, // 表示执行的次数
    data: { // data为返回的最终数据
        result: [],
        error: null,
    }
}
const handler = res => {}
    return new Promise(res => handler(res))
}

doGlide相当于一个公共区域(你也可以理解为原型对象),把一些值和数据存放在这个公共区域中,其它人可以通过这个公共区域来访问这里面的值和数据

在handler中使用Generator

初始化完毕,现在所有的值和数据都找到”家“(存放的地方)了,接下来在handler中使用生成器

const combineAsyncError = tasks => {
    const doGlide = {}
    const handler = res => {
        doGlide.out = res
        // 预先定义好生成器
        doGlide.node = (function*(){
        const { out, data } = doGlide
        const len = tasks.length
        // yield把循环带回了JavaScript编程的世界
        while(doGlide.times < len)
            yield noErrorAwait(tasks[doGlide.times++])
        // 全部请求成功(生成器执行完毕)时,返回数据
        out(data)
        })()
    doGlide.node.next()
    }
    return new Promise(res => handler(res))
}

res赋值给doGlide.out,调用out就是调用res,而调用res就代表本次处理完成(可以理解成out对应了一个then方法)。把Generator生成的迭代器交给doGlide.node,并先在本地启动一下生成器doGlide.node.next(),这个时候会进入while,然后执行noErrorAwait(tasks[doGlide.times++]),发出执行noErrorAwait(...)的命令后,noErrorAwait会被调用,且while会在此时变为可控的循环,因为noErrorAwait是一个异步函数,只有当yield得到具体的值时才会执行下一次循环(换句话说,yield得到了具体的值,那就代表本轮循环完成),而yield有没有值其实无所谓,我们只是利用它的特性来把循环变为可控的而已

扩展noErrorAwait

至此,所有的准备工作其实都已完备,就差noErrorAwait来完成整体的调度了,话不多说,接下来开始实现

const combineAsyncError = tasks => {
    const doGlide = {}
    const noErrorAwait = async f => {
        try{
            // 执行请求函数
            const r = await f()
            // 追加数据
            doGlide.data.result.push({flag: true, data: r})
            // 请求成功时继续执行生成器
            doGlide.node.next()
        }catch(e) {
            doGlide.data.error = e
            // 当某个请求函数失败时,立即终止函数执行并返回数据
            doGlide.out(doGlide.data)
        }
    }
    const handler = res => {}
    return new Promise(res => handler(res))
}

noErrorAwait这个async函数中,使用try catch来保证每一次请求函数执行时都不会抛出错误,当请求成功时,追加请求成功的数据,并且继续执行生成器,而生成器执行完毕,也就代表while执行完毕,所以out(data)实则是结束了整个combineAsyncError函数;而当请求失败时,则赋予error实际的值,并且执行doGlide.out来向用户返回所有值

至此,一个简单的combine-async-error函数便封装完毕了,现在通过两种情况进行测试

  • 请求函数全部成功
// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, true, true]

const getInfo = [requestJS, requestSY, requestYH]

combineAsyncError(getInfo)
    .then(data => {
        console.log('请求结果为:', data)
    })

c_a_e1.gif

  • 某一个请求函数抛出错误
// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, false, true]

const getInfo = [requestJS, requestSY, requestYH]

combineAsyncError(getInfo)
    .then(data => {
        console.log('请求结果为:', data)
    })

c_a_e2.gif

码上掘金

上面所编写的示例及封装的combine-async-error已存放至码上掘金

code.juejin.cn/pen/7121685…

比较三种形式(Promise、async、combine-async-error)

现在来比较一下三种形式,三种形式统一使用下面的请求结果

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, false, true]

Promise

let acc = 0
// 先查询吉林到山东
requestJS()
    .then(({ price: p1 }) => {
        acc += p1
        console.log(`吉林-山东的车票未售空,价格是 ${p1} RMB`)
        // 如果吉林-山东的车票未售空,则继续查询山东-云南的车票
        return requestSY()
    })
    .then(({ price: p2 }) => {
        acc += p2
        console.log(`山东-云南的车票未售空,价格是 ${p2} RMB`)
        // 如果山东-云南的车票未售空,则继续查询云南-海南的车票
        return requestYH()
    })
    .then(({ price: p3 }) => {
    	// 能执行到这里,就说明前面所有请求都成功了
        acc += p3
        console.log(`云南-海南的车票未售空,价格是 ${p3} RMB`)
        console.log(`本次旅途共计车费 ${acc} RMB`)
    })
    .catch(({ destination }) => console.log(`来晚了,${destination}的车票已售空`))

async

const f = async () => {
    try {
        const js = await requestJS()
        console.log(`吉林-山东的车票未售空,价格是 ${js.price} RMB`)
        const sy = await requestSY()
        console.log(`山东-云南的车票未售空,价格是 ${sy.price} RMB`)
        const yh = await requestYH()
        console.log(`云南-海南的车票未售空,价格是 ${yh.price} RMB`)
        console.log(`本次旅途共计车费 ${js.price + sy.price + yh.price} RMB`)
    } catch ({ destination }) {
        console.log(`来晚了,${destination}的车票已售空`)
    }
}

f()

combine-async-error

const getInfo = [requestJS, requestSY, requestYH]
combineAsyncError(getInfo)
    .then(({ result, error }) => {
        result.forEach(({data}) => console.log(`${data.destination}的车票未售空,价格是 ${data.price} RMB`))
        if (error) console.log(`来晚了,${error.destination}的车票已售空`)
    })

可以看到combine-async-error这种智能捕获错误的方式确实优雅,无论多少次嵌套请求,始终只需要一个then便可以轻松胜任所有工作,并且使用combine-async-error的形式也很简洁,根本不需要编写复杂的嵌套层级,在使用之前也不需要进行其它令人头疼的操作

扩展功能

虽然combineAsyncError函数实现到这里已经取得了不小的成就,但经过多次测试,我发现combineAsyncError始终还差点东西

现在来对combineAsyncError增加可选的配置项,提高其扩展性、灵活性

由于 combineAsyncError 配置项众多,所以仅以 forever 举例,如果你想了解更加强大的 combineAsyncError ,我在文末有详细介绍

forever取它的字面意思,即永远;不断地,在combineAsyncError里我们使用配置项forever来决定当请求函数遇到错误时,是否继续执行,默认为false

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, false, true]

const combineAsyncError = (tasks, config) => {
const doGlide = {}
const noErrorAwait = async f => {
        try {
            const r = await f()
            doGlide.data.result.push({ flag: true, data: r })
            doGlide.node.next()
        } catch (e) {
            doGlide.data.result.push({ flag: false, data: e })
            // 当forever为true时,不必理会错误,而是继续执行生成器
            if (config.forever) return doGlide.node.next()
            doGlide.out(doGlide.data)
        }
    }
    const handler = res => {}
    return new Promise(res => handler(res))
}

const getInfo = [requestJS, requestSY, requestYH]

combineAsyncError(getInfo, { forever: true })
.then(data => {
console.log('请求结果为:', data)
})

c_a_e3.gif

通过测试结果可以看到即使第二次请求失败了,但第三次请求依旧会正常发出,且combineAsyncError不会抛出任何错误。而无论是请求成功的结果,还是请求失败的结果,都可以在result中拿到

其它配置项

重新定义了combineAsyncError的入参数组,使其扩展性变的更高,另外增加了以下配置项

isCheckTypes

在设计combine-async-error时,关于传入的参数是否进行校验,其实是存在一些负面影响的,为此,combine-async-error主动添加了isCheckTypes配置项,如果该配置项的值为false,则不对入参进行检查,反之进行严格的类型检查。如果可以确保传入的类型始终是正确的,那么强烈建议你将该配置项更改为false;默认为true

由于JavaScript中存在隐式类型转换,所以即使你指定了isCheckTypes为true,combine-async-error也不会对传入的第二个参数(config)进行检查

isCheckTypes: true

acc

acc: () => {}

如果指定了该值为函数,则所有请求完成后会执行该函数,此回调函数会收到最终的请求结果

如果未指定该值,则combine-async-error返回一个Promise对象,你可以在它的then方法中得到最终的请求结果

forever

遇到错误时,是否继续执行(发出请求)。无论是嵌套还是并发请求模式,该配置项始终生效

forever: false

pipes

single

后一个请求函数是否接收前一个请求函数的结果

whole

后一个请求函数是否接收前面所有请求函数的结果

wholetrue时,single无效,反之有效

pipes: {
    single: false,
    whole: true
}

all

combine-async-error应该得到原有的扩展,为此它支持新的配置项all,如果为all指定了order值,则传入combine-async-error的请求数组会并发执行,而不是继续以嵌套的形式执行

下面的写法相当于使用了all的默认配置,因为all的值默认为false

all: false // 嵌套请求

下面的写法则是开启了并发之旅all为一个对象,其order属性决定并发的请求结果是否按照顺序来存放到最终数组中

all: {
    order: true
}

关于order的使用,举例如下

// 假设requestAuthor始终会在3-6秒钟之内返回请求结果
const requestAuthor = () => {}
// 假设requestPrice始终会在1秒钟之内返回请求结果
const requestPrice = () => {}

const getInfo = [requestAuthor, requestPrice]
combine-async-error(getInfo, {
    all: {
        order: true
    }
})

由于你指定了ordertrue,那么在最终的请求结果数组result中,requestAuthor的请求结果会作为result的第一个成员出现,而requestPrice的请求结果则会作为该数组的第二个成员出现,这是因为order始终会保证resultgetInfo的顺序一一对应,即使requestPrice是最先执行完的请求函数

如果指定了orderfalse,则最先执行完的请求函数所对应的结果就会在result中越靠前;在上例中requestPrice的请求结果会出现在result的第一个位置

requestCallback

requestCallback: {
    always: false,
    success: false,
    failed: false
}

always表示无论请求函数是成功还是失败,都会在拿到请求结果后执行为该请求函数提前指定好的callback,此callback会收到当前请求函数的结果

success表示只有当请求函数成功时,才会去执行提前执行好的callback,并且callback会收到当前请求函数执行成功的结果;failed则表示失败,与success同理

例如,当传入请求函数的形式为

combineAsyncError([
        {
            func: requestAuthor,
            callback: () => {} // 提前为requestAuthor指定好的回调函数
        }
    ], {
        requestCallback: {
        failed: true // 指定了failed
    }
})

上述示例中,只有当requestAuthor请求函数出错时,才会执行该请求函数所指定好的callback回调,并且此回调函数会收到requestAuthor失败的原因

设计初衷

combine-async-error设计的初衷是为了解决复杂的嵌套请求,现在通过丰富的配置项它也可以支持并发请求模式(不仅如此,还可以把请求函数玩出新的高度),但面对单个请求函数的情况,其效果并不理想,举例

创建一个新的请求函数requestTest,请求结果为失败

const requestTest = name => new Promise((res, rej) => {
    setTimeout(() => {
        rej({ name, destination: '今日所有车票已售空' })
    }, 1000)
})

现在分别使用Promisecombine-async-error来完成requestTest的调用

// Promise

requestTest('小明')
    .catch(({ name, destination }) => console.log(`${name}你好,${destination}`))
// combine-async-error

const getInfo = [{ func: requestTest, args: ['小明'] }]
combineAsyncError(getInfo)
    .then(({ error: { msg: { name, destination } } }) => console.log(`${name}你好,${destination}`))

vs.gif

在面对单个请求函数的情况下,使用Promsie可以便捷的发出请求,并且使用形式也较为简单;而combine-async-error则显得有些冗余了(指定请求函数、指定其收到的参数...);但随着请求函数的增多,我想combine-async-error的优势一定会体现出来

立即体验

此仓库中包含了该工具类详细的使用教程及各配置项的讲解;如果你对它有更好的建议,欢迎反馈

如果你觉得本篇文章不错,可以留个赞

现在你可以通过

npm install combine-async-error

来感受一下如何梭哈嵌套请求

或者查看它的

Git地址 combine-async-error

combine-async-error的心路历程到此为止...