根據筆者的項目經驗,本文講解了從函數回調,到 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 主頁:https://github.com/ascoders 但願結交有識之士!