爲什麼我推薦使用智能化 async?

摘要

開發中無論怎樣都會產生網絡請求,這樣一來自然也就避免不了大量使用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 每日一題》[1] 專欄中找到對應的題目進行練習

好了,現在開始由淺入深逐步分析

讓 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關鍵字的緣故,現在手動執行一下分析原因

那問題來了,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已存放至碼上掘金

https://code.juejin.cn/pen/7121685764311613447

比較三種形式 (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 地址 github.com/FuncJin/combine-async-error

combine-async-error的心路歷程到此爲止...

關於本文

作者:FuncJin

https://juejin.cn/post/7121853787794325512

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/KaNyWM_VJ9ScRk_g0nB_tQ