根據(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