爲什麼我推薦使用智能化 async?
摘要
開發中無論怎樣都會產生網絡請求,這樣一來自然也就避免不了大量使用then
、catch
或try catch
來捕獲錯誤,而捕獲錯誤的代碼量是隨着網絡請求的增多而增多,那應該如何優雅的系統性捕獲某個網絡請求中所產生的所有錯誤呢?
首先最常用的兩種處理網絡請求的形式即Promise
與async
(事實上很多請求庫都是基於這兩者的封裝),使用Promise
那必然要與then
、catch
掛鉤,也就是說每個請求都對應一個Promise
實例,然後通過該實例上對應的方法來完成對應的操作,這應該算是比較常用的一種形式了
但如果涉及嵌套請求,那可能還要不斷的增加then
、catch
來完成需求,好了,現在可以使用看起來真的像同步編程的async
來着手優化了,即await promise
,那這種情況下就根本不需要手動then
了,但如果await promise
拋出了錯誤呢?那恐怕不得不讓try catch
來幫忙了,而如果也是嵌套請求,那與Promise
寫法類似的問題又來了,有多少次請求難道我就要多少次try catch
嗎?那這樣看來的話,Promise
與async
在面對這種屎山請求的時候確實有點心有餘而力不足了
前言
之所以寫作本篇文章是因爲前幾天在優化數據庫操作時,發現要不停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
會在請求失敗時觸發,而無論是then
或catch
都會返回一個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
,所以纔會萌生出自己封裝一個智能化捕獲函數來處理這種情況。上面所講到的Promise
與async
其實已經是很常見的一種寫法了,但如果項目中存在第二種嵌套請求 (比如先請求所在省份的天氣,再請求所在縣的天氣)。如果放在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
對象,這是因爲noErrorAwait
是async
函數的緣故。封裝到這裏,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
是不負責順序執行的,甚至可能會報錯。這是因爲noErrorAwait
中await
關鍵字的緣故,現在手動執行一下分析原因
-
調用
getInfo
-
調用
noErrorAwait
並傳遞參數requestJS
-
來到
noErrorAwait
中,由於noErrorAwait
是async
函數,所以會返回一個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
就關機睡覺呀。這個問題當時我認真思考過,期間不泛break
、Proxy
等其它騷操作,在束手無策的時候,我突然想到了它的表哥——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
後一個請求函數是否接收前面所有請求函數的結果
當whole
爲true
時,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
}
})
複製代碼
由於你指定了order
爲true
,那麼在最終的請求結果數組result
中,requestAuthor
的請求結果會作爲result
的第一個成員出現,而requestPrice
的請求結果則會作爲該數組的第二個成員出現,這是因爲order
始終會保證result
與getInfo
的順序一一對應,即使requestPrice
是最先執行完的請求函數
如果指定了order
爲false
,則最先執行完的請求函數所對應的結果就會在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)
})
複製代碼
現在分別使用Promise
和combine-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