根據(jù)筆者的項目經(jīng)驗,本文講解了從函數(shù)回調(diào),到 es7
規(guī)范的異常處理方式。異常處理的優(yōu)雅性隨著規(guī)范的進步越來越高,不要害怕使用 try catch
,不能回避異常處理。
我們需要一個健全的架構捕獲所有同步、異步的異常。業(yè)務方不處理異常時,中斷函數(shù)執(zhí)行并啟用默認處理,業(yè)務方也可以隨時捕獲異常自己處理。
優(yōu)雅的異常處理方式就像冒泡事件,任何元素可以自由攔截,也可以放任不管交給頂層處理。
文字講解僅是背景知識介紹,不包含對代碼塊的完整解讀,不要忽略代碼塊的閱讀。
1. 回調(diào)
如果在回調(diào)函數(shù)中直接處理了異常,是最不明智的選擇,因為業(yè)務方完全失去了對異常的控制能力。
下方的函數(shù) 請求處理
不但永遠不會執(zhí)行,還無法在異常時做額外的處理,也無法阻止異常產(chǎn)生時笨拙的 console.log('請求失敗')
行為。
function fetch(callback) { setTimeout(() => { console.log('請求失敗') })}fetch(() => { console.log('請求處理') // 永遠不會執(zhí)行})
2. 回調(diào),無法捕獲的異常
回調(diào)函數(shù)有同步和異步之分,區(qū)別在于對方執(zhí)行回調(diào)函數(shù)的時機,異常一般出現(xiàn)在請求、數(shù)據(jù)庫連接等操作中,這些操作大多是異步的。
異步回調(diào)中,回調(diào)函數(shù)的執(zhí)行棧與原函數(shù)分離開,導致外部無法抓住異常。
從下文開始,我們約定用
setTimeout
模擬異步操作
function fetch(callback) { setTimeout(() => { throw Error('請求失敗') })}try { fetch(() => { console.log('請求處理') // 永遠不會執(zhí)行 })} catch (error) { console.log('觸發(fā)異常', error) // 永遠不會執(zhí)行}// 程序崩潰// Uncaught Error: 請求失敗
3. 回調(diào),不可控的異常
我們變得謹慎,不敢再隨意拋出異常,這已經(jīng)違背了異常處理的基本原則。
雖然使用了 error-first
約定,使異??雌饋碜兊每商幚恚珮I(yè)務方依然沒有對異常的控制權,是否調(diào)用錯誤處理取決于回調(diào)函數(shù)是否執(zhí)行,我們無法知道調(diào)用的函數(shù)是否可靠。
更糟糕的問題是,業(yè)務方必須處理異常,否則程序掛掉就會什么都不做,這對大部分不用特殊處理異常的場景造成了很大的精神負擔。
function fetch(handleError, callback) { setTimeout(() => { handleError('請求失敗') })}fetch(() => { console.log('失敗處理') // 失敗處理}, error => { console.log('請求處理') // 永遠不會執(zhí)行})
番外 Promise 基礎
Promise
是一個承諾,只可能是成功、失敗、無響應三種情況之一,一旦決策,無法修改結果。
Promise
不屬于流程控制,但流程控制可以用多個 Promise
組合實現(xiàn),因此它的職責很單一,就是對一個決議的承諾。
resolve
表明通過的決議,reject
表明拒絕的決議,如果決議通過,then
函數(shù)的第一個回調(diào)會立即插入 microtask
隊列,異步立即執(zhí)行。
簡單補充下事件循環(huán)的知識,js 事件循環(huán)分為 macrotask 和 microtask。
microtask 會被插入到每一個 macrotask 的尾部,所以 microtask 總會優(yōu)先執(zhí)行,哪怕 macrotask 因為 js 進程繁忙被 hung 住。
比如setTimeout
setInterval
會插入到 macrotask 中。
const promiseA = new Promise((resolve, reject) => { resolve('ok')})promiseA.then(result => { console.log(result) // ok})
如果決議結果是決絕,那么 then
函數(shù)的第二個回調(diào)會立即插入 microtask
隊列。
const promiseB = new Promise((resolve, reject) => { reject('no')})promiseB.then(result => { console.log(result) // 永遠不會執(zhí)行}, error => { console.log(error) // no})
如果一直不決議,此 promise
將處于 pending
狀態(tài)。
const promiseC = new Promise((resolve, reject) => { // nothing})promiseC.then(result => { console.log(result) // 永遠不會執(zhí)行}, error => { console.log(error) // 永遠不會執(zhí)行})
未捕獲的 reject
會傳到末尾,通過 catch
接住
const promiseD = new Promise((resolve, reject) => { reject('no')})promiseD.then(result => { console.log(result) // 永遠不會執(zhí)行}).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
,其狀態(tài)取決于 then
的返回值。
const promiseF = new Promise((resolve, reject) => { resolve('ok')})promiseF.then(result => { return Promise.reject('error1')}).then(result => { console.log(result) // 永遠不會執(zhí)行 return Promise.resolve('ok1') // 永遠不會執(zhí)行}).then(result => { console.log(result) // 永遠不會執(zhí)行}).catch(error => { console.log(error) // error1})
4 Promise 異常處理
不僅是 reject
,拋出的異常也會被作為拒絕狀態(tài)被 Promise
捕獲。
function fetch(callback) { return new Promise((resolve, reject) => { throw Error('用戶不存在') })}fetch().then(result => { console.log('請求處理', result) // 永遠不會執(zhí)行}).catch(error => { console.log('請求處理異常', error) // 請求處理異常 用戶不存在})
5 Promise 無法捕獲的異常
但是,永遠不要在 macrotask
隊列中拋出異常,因為 macrotask
隊列脫離了運行上下文環(huán)境,異常無法被當前作用域捕獲。
function fetch(callback) { return new Promise((resolve, reject) => { setTimeout(() => { throw Error('用戶不存在') }) })}fetch().then(result => { console.log('請求處理', result) // 永遠不會執(zhí)行}).catch(error => { console.log('請求處理異常', error) // 永遠不會執(zhí)行})// 程序崩潰// Uncaught Error: 用戶不存在
不過 microtask
中拋出的異??梢员徊东@,說明 microtask
隊列并沒有離開當前作用域,我們通過以下例子來證明:
Promise.resolve(true).then((resolve, reject)=> { throw Error('microtask 中的異常')}).catch(error => { console.log('捕獲異常', error) // 捕獲異常 Error: microtask 中的異常})
至此,Promise
的異常處理有了比較清晰的答案,只要注意在 macrotask
級別回調(diào)中使用 reject
,就沒有抓不住的異常。
6 Promise 異常追問
如果第三方函數(shù)在 macrotask
回調(diào)中以 throw Error
的方式拋出異常怎么辦?
function thirdFunction() { setTimeout(() => { throw Error('就是任性') })}Promise.resolve(true).then((resolve, reject) => { thirdFunction()}).catch(error => { console.log('捕獲異常', error)})// 程序崩潰// Uncaught Error: 就是任性
值得欣慰的是,由于不在同一個調(diào)用棧,雖然這個異常無法被捕獲,但也不會影響當前調(diào)用棧的執(zhí)行。
我們必須正視這個問題,唯一的解決辦法,是第三方函數(shù)不要做這種傻事,一定要在 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
傳遞下去,錯誤也不會繼續(xù)傳遞。
我們發(fā)現(xiàn),這樣還不是完美的辦法,不但容易忘記 return
,而且當同時含有多個第三方函數(shù)時,處理方式不太優(yōu)雅:
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 基礎
generator
是更為優(yōu)雅的流程控制方式,可以讓函數(shù)可中斷執(zhí)行:
function* generatorA() { console.log('a') yield console.log('b')}const genA = generatorA()genA.next() // agenA.next() // b
yield
關鍵字后面可以包含表達式,表達式會傳給 next().value
。
next()
可以傳遞參數(shù),參數(shù)作為 yield
的返回值。
這些特性足以孕育出偉大的生成器,我們稍后介紹。下面是這個特性的例子:
function* generatorB(count) { console.log(count) const result = yield 5 console.log(result * count)}const genB = generatorB(2)genB.next() // 2const genBValue = genB.next(7).value // 14// genBValue undefined
第一個 next 是沒有參數(shù)的,因為在執(zhí)行 generator
函數(shù)時,初始值已經(jīng)傳入,第一個 next
的參數(shù)沒有任何意義,傳入也會被丟棄。
const result = yield 5
這一句,返回值不是想當然的 5
。其的作用是將 5
傳遞給 genB.next()
,其值,由下一個 next genB.next(7)
傳給了它,所以語句等于 const result = 7
。
最后一個 genBValue
,是最后一個 next
的返回值,這個值,就是函數(shù)的 return
值,顯然為 undefined
。
我們回到這個語句:
const result = yield 5
如果返回值是 5,是不是就清晰了許多?是的,這種語法就是 await
。所以 Async Await
與 generator
有著莫大的關聯(lián),橋梁就是 生成器,我們稍后介紹 生成器。
番外 Async Await
如果認為 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
后面的表達式被執(zhí)行,表達式的返回值被返回給了 await
執(zhí)行處。
但是程序是怎么暫停的呢?只有 generator
可以暫停程序。那么等等,回顧一下 generator
的特性,我們發(fā)現(xiàn)它也可以達到這種效果。
番外 async await 是 generator 的語法糖
終于可以介紹 生成器 了!它可以魔法般將下面的 generator
執(zhí)行成為 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
后面的表達式被執(zhí)行,表達式的返回值被返回給了yield
執(zhí)行處。
達到這個目標不難,達到了就完成了 await
的功能,就是這么神奇。
function step(generator) { const gen = generator() // 由于其傳值,返回步驟交錯的特性,記錄上一次 yield 傳過來的值,在下一個 next 返回過去 let lastValue // 包裹為 Promise,并執(zhí)行表達式 return () => Promise.resolve(gen.next(lastValue).value).then(value => { lastValue = value return lastValue })}
利用生成器,模擬出 await
的執(zhí)行效果:
const run = step(main)function recursive(promise) { promise().then(result => { if (result) { recursive(promise) } })}recursive(run)// 400// 600// 800
可以看出,await
的執(zhí)行次數(shù)由程序自動控制,而回退到 generator
模擬,需要根據(jù)條件判斷是否已經(jīng)將函數(shù)執(zhí)行完畢。
7 Async Await 異常
不論是同步、異步的異常,await
都不會自動捕獲,但好處是可以自動中斷函數(shù),我們大可放心編寫業(yè)務邏輯,而不用擔心異步異常后會被執(zhí)行引發(fā)雪崩:
function fetch(callback) { return new Promise((resolve, reject) => { setTimeout(() => { reject() }) })}async function main() { const result = await fetch() console.log('請求處理', result) // 永遠不會執(zhí)行}main()
8 Async Await 捕獲異常
我們使用 try catch
捕獲異常。
認真閱讀 Generator
番外篇的話,就會理解為什么此時異步的異??梢酝ㄟ^ try catch
來捕獲。
因為此時的異步其實在一個作用域中,通過 generator
控制執(zhí)行順序,所以可以將異步看做同步的代碼去編寫,包括使用 try catch
捕獲異常。
function fetch(callback) { return new Promise((resolve, reject) => { setTimeout(() => { reject('no') }) })}async function main() { try { const result = await fetch() console.log('請求處理', result) // 永遠不會執(zhí)行 } catch (error) { console.log('異常', error) // 異常 no }}main()
9 Async Await 無法捕獲的異常
和第五章 Promise 無法捕獲的異常 一樣,這也是 await
的軟肋,不過任然可以通過第六章的方案解決:
function thirdFunction() { return new Promise((resolve, reject) => { setTimeout(() => { reject('收斂一些') }) })}async function main() { try { const result = await thirdFunction() console.log('請求處理', result) // 永遠不會執(zhí)行 } catch (error) { console.log('異常', error) // 異常 收斂一些 }}main()
現(xiàn)在解答第六章尾部的問題,為什么 await
是更加優(yōu)雅的方案:
async function main() { try { const result1 = await secondFunction() // 如果不拋出異常,后續(xù)繼續(xù)執(zhí)行 const result2 = await thirdFunction() // 拋出異常 const result3 = await thirdFunction() // 永遠不會執(zhí)行 console.log('請求處理', result) // 永遠不會執(zhí)行 } catch (error) { console.log('異常', error) // 異常 收斂一些 }}main()
10 業(yè)務場景
在如今 action
概念成為標配的時代,我們大可以將所有異常處理收斂到 action
中。
我們以如下業(yè)務代碼為例,默認不捕獲錯誤的話,錯誤會一直冒泡到頂層,最后拋出異常。
const successRequest = () => Promise.resolve('a')const failRequest = () => Promise.reject('b')class Action { async successReuqest() { const result = await successRequest() console.log('successReuqest', '處理返回值', result) // successReuqest 處理返回值 a }
如有錯誤,歡迎斧正,本人 github 主頁:https://github.com/ascoders 希望結交有識之士!
http://www.cnblogs.com/ascoders/p/6358838.html