根據筆者的項目經驗,本文講解了從函數回調,到 es7
規範的異常處理方式。異常處理的優雅性隨着規範的進步愈來愈高,不要懼怕使用 try catch
,不能迴避異常處理。javascript
咱們須要一個健全的架構捕獲全部同步、異步的異常。業務方不處理異常時,中斷函數執行並啓用默認處理,業務方也能夠隨時捕獲異常本身處理。前端
優雅的異常處理方式就像冒泡事件,任何元素能夠自由攔截,也能夠聽任無論交給頂層處理。java
文字講解僅是背景知識介紹,不包含對代碼塊的完整解讀,不要忽略代碼塊的閱讀。node
若是在回調函數中直接處理了異常,是最不明智的選擇,由於業務方徹底失去了對異常的控制能力。git
下方的函數 請求處理
不但永遠不會執行,還沒法在異常時作額外的處理,也沒法阻止異常產生時笨拙的 console.log('請求失敗')
行爲。github
function fetch(callback) {
setTimeout(() => {
console.log('請求失敗')
})
}
fetch(() => {
console.log('請求處理') // 永遠不會執行
})複製代碼
回調函數有同步和異步之分,區別在於對方執行回調函數的時機,異常通常出如今請求、數據庫鏈接等操做中,這些操做大可能是異步的。golang
異步回調中,回調函數的執行棧與原函數分離開,致使外部沒法抓住異常。數據庫
從下文開始,咱們約定用
setTimeout
模擬異步操做promise
function fetch(callback) {
setTimeout(() => {
throw Error('請求失敗')
})
}
try {
fetch(() => {
console.log('請求處理') // 永遠不會執行
})
} catch (error) {
console.log('觸發異常', error) // 永遠不會執行
}
// 程序崩潰
// Uncaught Error: 請求失敗複製代碼
咱們變得謹慎,不敢再隨意拋出異常,這已經違背了異常處理的基本原則。瀏覽器
雖然使用了 error-first
約定,使異常看起來變得可處理,但業務方依然沒有對異常的控制權,是否調用錯誤處理取決於回調函數是否執行,咱們沒法知道調用的函數是否可靠。
更糟糕的問題是,業務方必須處理異常,不然程序掛掉就會什麼都不作,這對大部分不用特殊處理異常的場景形成了很大的精神負擔。
function fetch(handleError, callback) {
setTimeout(() => {
handleError('請求失敗')
})
}
fetch(() => {
console.log('失敗處理') // 失敗處理
}, error => {
console.log('請求處理') // 永遠不會執行
})複製代碼
Promise
是一個承諾,只多是成功、失敗、無響應三種狀況之一,一旦決策,沒法修改結果。
Promise
不屬於流程控制,但流程控制能夠用多個 Promise
組合實現,所以它的職責很單一,就是對一個決議的承諾。
resolve
代表經過的決議,reject
代表拒絕的決議,若是決議經過,then
函數的第一個回調會當即插入 microtask
隊列,異步當即執行。
簡單補充下事件循環的知識,js 事件循環分爲 macrotask 和 microtask。
microtask 會被插入到每個 macrotask 的尾部,因此 microtask 總會優先執行,哪怕 macrotask 由於 js 進程繁忙被 hung 住。
好比setTimeout
setInterval
會插入到 macrotask 中。
const promiseA = new Promise((resolve, reject) => {
resolve('ok')
})
promiseA.then(result => {
console.log(result) // ok
})複製代碼
若是決議結果是決絕,那麼 then
函數的第二個回調會當即插入 microtask
隊列。
const promiseB = new Promise((resolve, reject) => {
reject('no')
})
promiseB.then(result => {
console.log(result) // 永遠不會執行
}, error => {
console.log(error) // no
})複製代碼
若是一直不決議,此 promise
將處於 pending
狀態。
const promiseC = new Promise((resolve, reject) => {
// nothing
})
promiseC.then(result => {
console.log(result) // 永遠不會執行
}, error => {
console.log(error) // 永遠不會執行
})複製代碼
未捕獲的 reject
會傳到末尾,經過 catch
接住
const promiseD = new Promise((resolve, reject) => {
reject('no')
})
promiseD.then(result => {
console.log(result) // 永遠不會執行
}).catch(error => {
console.log(error) // no
})複製代碼
resolve
決議會被自動展開(reject
不會)
const promiseE = new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
resolve('ok')
})
})
promiseE.then(result => {
console.log(result) // ok
})複製代碼
鏈式流,then
會返回一個新的 Promise
,其狀態取決於 then
的返回值。
const promiseF = new Promise((resolve, reject) => {
resolve('ok')
})
promiseF.then(result => {
return Promise.reject('error1')
}).then(result => {
console.log(result) // 永遠不會執行
return Promise.resolve('ok1') // 永遠不會執行
}).then(result => {
console.log(result) // 永遠不會執行
}).catch(error => {
console.log(error) // error1
})複製代碼
不只是 reject
,拋出的異常也會被做爲拒絕狀態被 Promise
捕獲。
function fetch(callback) {
return new Promise((resolve, reject) => {
throw Error('用戶不存在')
})
}
fetch().then(result => {
console.log('請求處理', result) // 永遠不會執行
}).catch(error => {
console.log('請求處理異常', error) // 請求處理異常 用戶不存在
})複製代碼
可是,永遠不要在 macrotask
隊列中拋出異常,由於 macrotask
隊列脫離了運行上下文環境,異常沒法被當前做用域捕獲。
function fetch(callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
throw Error('用戶不存在')
})
})
}
fetch().then(result => {
console.log('請求處理', result) // 永遠不會執行
}).catch(error => {
console.log('請求處理異常', error) // 永遠不會執行
})
// 程序崩潰
// Uncaught Error: 用戶不存在複製代碼
不過 microtask
中拋出的異常能夠被捕獲,說明 microtask
隊列並無離開當前做用域,咱們經過如下例子來證實:
Promise.resolve(true).then((resolve, reject)=> {
throw Error('microtask 中的異常')
}).catch(error => {
console.log('捕獲異常', error) // 捕獲異常 Error: microtask 中的異常
})複製代碼
至此,Promise
的異常處理有了比較清晰的答案,只要注意在 macrotask
級別回調中使用 reject
,就沒有抓不住的異常。
若是第三方函數在 macrotask
回調中以 throw Error
的方式拋出異常怎麼辦?
function thirdFunction() {
setTimeout(() => {
throw Error('就是任性')
})
}
Promise.resolve(true).then((resolve, reject) => {
thirdFunction()
}).catch(error => {
console.log('捕獲異常', error)
})
// 程序崩潰
// Uncaught Error: 就是任性複製代碼
值得欣慰的是,因爲不在同一個調用棧,雖然這個異常沒法被捕獲,但也不會影響當前調用棧的執行。
咱們必須正視這個問題,惟一的解決辦法,是第三方函數不要作這種傻事,必定要在 macrotask
拋出異常的話,請改成 reject
的方式。
function thirdFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('收斂一些')
})
})
}
Promise.resolve(true).then((resolve, reject) => {
return thirdFunction()
}).catch(error => {
console.log('捕獲異常', error) // 捕獲異常 收斂一些
})複製代碼
請注意,若是 return thirdFunction()
這行缺乏了 return
的話,依然沒法抓住這個錯誤,這是由於沒有將對方返回的 Promise
傳遞下去,錯誤也不會繼續傳遞。
咱們發現,這樣還不是完美的辦法,不但容易忘記 return
,並且當同時含有多個第三方函數時,處理方式不太優雅:
function thirdFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('收斂一些')
})
})
}
Promise.resolve(true).then((resolve, reject) => {
return thirdFunction().then(() => {
return thirdFunction()
}).then(() => {
return thirdFunction()
}).then(() => {
})
}).catch(error => {
console.log('捕獲異常', error)
})複製代碼
是的,咱們還有更好的處理方式。
generator
是更爲優雅的流程控制方式,可讓函數可中斷執行:
function* generatorA() {
console.log('a')
yield
console.log('b')
}
const genA = generatorA()
genA.next() // a
genA.next() // b複製代碼
yield
關鍵字後面能夠包含表達式,表達式會傳給 next().value
。
next()
能夠傳遞參數,參數做爲 yield
的返回值。
這些特性足以孕育出偉大的生成器,咱們稍後介紹。下面是這個特性的例子:
function* generatorB(count) {
console.log(count)
const result = yield 5
console.log(result * count)
}
const genB = generatorB(2)
genB.next() // 2
const genBValue = genB.next(7).value // 14
// genBValue undefined複製代碼
第一個 next 是沒有參數的,由於在執行 generator
函數時,初始值已經傳入,第一個 next
的參數沒有任何意義,傳入也會被丟棄。
const result = yield 5複製代碼
這一句,返回值不是想固然的 5
。其的做用是將 5
傳遞給 genB.next()
,其值,由下一個 next genB.next(7)
傳給了它,因此語句等於 const result = 7
。
最後一個 genBValue
,是最後一個 next
的返回值,這個值,就是函數的 return
值,顯然爲 undefined
。
咱們回到這個語句:
const result = yield 5複製代碼
若是返回值是 5,是否是就清晰了許多?是的,這種語法就是 await
。因此 Async Await
與 generator
有着莫大的關聯,橋樑就是 生成器,咱們稍後介紹 生成器。
若是認爲 Generator
不太好理解,那 Async Await
絕對是救命稻草,咱們看看它們的特徵:
const timeOut = (time = 0) => new Promise((resolve, reject) => {
setTimeout(() => {
resolve(time + 200)
}, time)
})
async function main() {
const result1 = await timeOut(200)
console.log(result1) // 400
const result2 = await timeOut(result1)
console.log(result2) // 600
const result3 = await timeOut(result2)
console.log(result3) // 800
}
main()複製代碼
所見即所得,await
後面的表達式被執行,表達式的返回值被返回給了 await
執行處。
可是程序是怎麼暫停的呢?只有 generator
能夠暫停程序。那麼等等,回顧一下 generator
的特性,咱們發現它也能夠達到這種效果。
終於能夠介紹 生成器 了!它能夠魔法般將下面的 generator
執行成爲 await
的效果。
function* main() {
const result1 = yield timeOut(200)
console.log(result1)
const result2 = yield timeOut(result1)
console.log(result2)
const result3 = yield timeOut(result2)
console.log(result3)
}複製代碼
下面的代碼就是生成器了,生成器並不神祕,它只有一個目的,就是:
所見即所得,
yield
後面的表達式被執行,表達式的返回值被返回給了yield
執行處。
達到這個目標不難,達到了就完成了 await
的功能,就是這麼神奇。
function step(generator) {
const gen = generator()
// 因爲其傳值,返回步驟交錯的特性,記錄上一次 yield 傳過來的值,在下一個 next 返回過去
let lastValue
// 包裹爲 Promise,並執行表達式
return () => Promise.resolve(gen.next(lastValue).value).then(value => {
lastValue = value
return lastValue
})
}複製代碼
利用生成器,模擬出 await
的執行效果:
const run = step(main)
function recursive(promise) {
promise().then(result => {
if (result) {
recursive(promise)
}
})
}
recursive(run)
// 400
// 600
// 800複製代碼
能夠看出,await
的執行次數由程序自動控制,而回退到 generator
模擬,須要根據條件判斷是否已經將函數執行完畢。
不管是同步、異步的異常,await
都不會自動捕獲,但好處是能夠自動中斷函數,咱們大可放心編寫業務邏輯,而不用擔憂異步異常後會被執行引起雪崩:
function fetch(callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject()
})
})
}
async function main() {
const result = await fetch()
console.log('請求處理', result) // 永遠不會執行
}
main()複製代碼
咱們使用 try catch
捕獲異常。
認真閱讀 Generator
番外篇的話,就會理解爲何此時異步的異常能夠經過 try catch
來捕獲。
由於此時的異步其實在一個做用域中,經過 generator
控制執行順序,因此能夠將異步看作同步的代碼去編寫,包括使用 try catch
捕獲異常。
function fetch(callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('no')
})
})
}
async function main() {
try {
const result = await fetch()
console.log('請求處理', result) // 永遠不會執行
} catch (error) {
console.log('異常', error) // 異常 no
}
}
main()複製代碼
和第五章 Promise 沒法捕獲的異常 同樣,這也是 await
的軟肋,不過任然能夠經過第六章的方案解決:
function thirdFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('收斂一些')
})
})
}
async function main() {
try {
const result = await thirdFunction()
console.log('請求處理', result) // 永遠不會執行
} catch (error) {
console.log('異常', error) // 異常 收斂一些
}
}
main()複製代碼
如今解答第六章尾部的問題,爲何 await
是更加優雅的方案:
async function main() {
try {
const result1 = await secondFunction() // 若是不拋出異常,後續繼續執行
const result2 = await thirdFunction() // 拋出異常
const result3 = await thirdFunction() // 永遠不會執行
console.log('請求處理', result) // 永遠不會執行
} catch (error) {
console.log('異常', error) // 異常 收斂一些
}
}
main()複製代碼
在現在 action
概念成爲標配的時代,咱們大能夠將全部異常處理收斂到 action
中。
咱們以以下業務代碼爲例,默認不捕獲錯誤的話,錯誤會一直冒泡到頂層,最後拋出異常。
const successRequest = () => Promise.resolve('a')
const failRequest = () => Promise.reject('b')
class Action {
async successReuqest() {
const result = await successRequest()
console.log('successReuqest', '處理返回值', result) // successReuqest 處理返回值 a
}
async failReuqest() {
const result = await failRequest()
console.log('failReuqest', '處理返回值', result) // 永遠不會執行
}
async allReuqest() {
const result1 = await successRequest()
console.log('allReuqest', '處理返回值 success', result1) // allReuqest 處理返回值 success a
const result2 = await failRequest()
console.log('allReuqest', '處理返回值 success', result2) // 永遠不會執行
}
}
const action = new Action()
action.successReuqest()
action.failReuqest()
action.allReuqest()
// 程序崩潰
// Uncaught (in promise) b
// Uncaught (in promise) b複製代碼
爲了防止程序崩潰,須要業務線在全部 async 函數中包裹 try catch
。
咱們須要一種機制捕獲 action
最頂層的錯誤進行統一處理。
爲了補充前置知識,咱們再次進入番外話題。
Decorator
中文名是裝飾器,核心功能是能夠經過外部包裝的方式,直接修改類的內部屬性。
裝飾器按照裝飾的位置,分爲 class decorator
method decorator
以及 property decorator
(目前標準還沒有支持,經過 get
set
模擬實現)。
類級別裝飾器,修飾整個類,能夠讀取、修改類中任何屬性和方法。
const classDecorator = (target: any) => {
const keys = Object.getOwnPropertyNames(target.prototype)
console.log('classA keys,', keys) // classA keys ["constructor", "sayName"]
}
@classDecorator
class A {
sayName() {
console.log('classA ascoders')
}
}
const a = new A()
a.sayName() // classA ascoders複製代碼
方法級別裝飾器,修飾某個方法,和類裝飾器功能相同,可是能額外獲取當前修飾的方法名。
爲了發揮這一特色,咱們篡改一下修飾的函數。
const methodDecorator = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
return {
get() {
return () => {
console.log('classC method override')
}
}
}
}
class C {
@methodDecorator
sayName() {
console.log('classC ascoders')
}
}
const c = new C()
c.sayName() // classC method override複製代碼
屬性級別裝飾器,修飾某個屬性,和類裝飾器功能相同,可是能額外獲取當前修飾的屬性名。
爲了發揮這一特色,咱們篡改一下修飾的屬性值。
const propertyDecorator = (target: any, propertyKey: string | symbol) => {
Object.defineProperty(target, propertyKey, {
get() {
return 'github'
},
set(value: any) {
return value
}
})
}
class B {
@propertyDecorator
private name = 'ascoders'
sayName() {
console.log(`classB ${this.name}`)
}
}
const b = new B()
b.sayName() // classB github複製代碼
咱們來編寫類級別裝飾器,專門捕獲 async
函數拋出的異常:
const asyncClass = (errorHandler?: (error?: Error) => void) => (target: any) => {
Object.getOwnPropertyNames(target.prototype).forEach(key => {
const func = target.prototype[key]
target.prototype[key] = async (...args: any[]) => {
try {
await func.apply(this, args)
} catch (error) {
errorHandler && errorHandler(error)
}
}
})
return target
}複製代碼
將類全部方法都用 try catch
包裹住,將異常交給業務方統一的 errorHandler
處理:
const successRequest = () => Promise.resolve('a')
const failRequest = () => Promise.reject('b')
const iAsyncClass = asyncClass(error => {
console.log('統一異常處理', error) // 統一異常處理 b
})
@iAsyncClass
class Action {
async successReuqest() {
const result = await successRequest()
console.log('successReuqest', '處理返回值', result)
}
async failReuqest() {
const result = await failRequest()
console.log('failReuqest', '處理返回值', result) // 永遠不會執行
}
async allReuqest() {
const result1 = await successRequest()
console.log('allReuqest', '處理返回值 success', result1)
const result2 = await failRequest()
console.log('allReuqest', '處理返回值 success', result2) // 永遠不會執行
}
}
const action = new Action()
action.successReuqest()
action.failReuqest()
action.allReuqest()複製代碼
咱們也能夠編寫方法級別的異常處理:
const asyncMethod = (errorHandler?: (error?: Error) => void) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const func = descriptor.value
return {
get() {
return (...args: any[]) => {
return Promise.resolve(func.apply(this, args)).catch(error => {
errorHandler && errorHandler(error)
})
}
},
set(newValue: any) {
return newValue
}
}
}複製代碼
業務方用法相似,只是裝飾器須要放在函數上:
const successRequest = () => Promise.resolve('a')
const failRequest = () => Promise.reject('b')
const asyncAction = asyncMethod(error => {
console.log('統一異常處理', error) // 統一異常處理 b
})
class Action {
@asyncAction async successReuqest() {
const result = await successRequest()
console.log('successReuqest', '處理返回值', result)
}
@asyncAction async failReuqest() {
const result = await failRequest()
console.log('failReuqest', '處理返回值', result) // 永遠不會執行
}
@asyncAction async allReuqest() {
const result1 = await successRequest()
console.log('allReuqest', '處理返回值 success', result1)
const result2 = await failRequest()
console.log('allReuqest', '處理返回值 success', result2) // 永遠不會執行
}
}
const action = new Action()
action.successReuqest()
action.failReuqest()
action.allReuqest()複製代碼
我想描述的意思是,在第 11 章這種場景下,業務方是不用擔憂異常致使的 crash
,由於全部異常都會在頂層統一捕獲,可能表現爲彈出一個提示框,告訴用戶請求發送失敗。
業務方也不須要判斷程序中是否存在異常,而戰戰兢兢的處處 try catch
,由於程序中任何異常都會馬上終止函數的後續執行,不會再引起更惡劣的結果。
像 golang 中異常處理方式,就存在這個問題
經過 err, result := func() 的方式,雖然固定了第一個參數是錯誤信息,但下一行代碼免不了要以if error {...}
開頭,整個程序的業務代碼充斥着巨量的沒必要要錯誤處理,而大部分時候,咱們還要爲如何處理這些錯誤想的焦頭爛額。
而 js 異常冒泡的方式,在前端能夠用提示框兜底,nodejs端能夠返回 500 錯誤兜底,並馬上中斷後續請求代碼,等於在全部危險代碼身後加了一層隱藏的 return
。
同時業務方也握有絕對的主動權,好比登陸失敗後,若是帳戶不存在,那麼直接跳轉到註冊頁,而不是傻瓜的提示用戶賬號不存在,能夠這樣作:
async login(nickname, password) {
try {
const user = await userService.login(nickname, password)
// 跳轉到首頁,登陸失敗後不會執行到這,因此不用擔憂用戶看到奇怪的跳轉
} catch (error) {
if (error.no === -1) {
// 跳轉到登陸頁
} else {
throw Error(error) // 其餘錯誤不想管,把球繼續踢走
}
}
}複製代碼
在 nodejs
端,記得監聽全局錯誤,兜住落網之魚:
process.on('uncaughtException', (error: any) => {
logger.error('uncaughtException', error)
})
process.on('unhandledRejection', (error: any) => {
logger.error('unhandledRejection', error)
})複製代碼
在瀏覽器端,記得監聽 window
全局錯誤,兜住漏網之魚:
window.addEventListener('unhandledrejection', (event: any) => {
logger.error('unhandledrejection', event)
})
window.addEventListener('onrejectionhandled', (event: any) => {
logger.error('onrejectionhandled', event)
})複製代碼
若有錯誤,歡迎斧正,本人 github 主頁:github.com/ascoders 但願結交有識之士!