Callback Promise Generator Async-Await 和异常处理的演进

根据笔者的项目经验,本文讲解了从函数回调,到 es7 规范的异常处理方式。异常处理的优雅性随着规范的进步愈来愈高,不要惧怕使用 try catch,不能回避异常处理。javascript

咱们须要一个健全的架构捕获全部同步、异步的异常。业务方不处理异常时,中断函数执行并启用默认处理,业务方也能够随时捕获异常本身处理。前端

优雅的异常处理方式就像冒泡事件,任何元素能够自由拦截,也能够听任无论交给顶层处理。java

文字讲解仅是背景知识介绍,不包含对代码块的完整解读,不要忽略代码块的阅读。node

1. 回调

若是在回调函数中直接处理了异常,是最不明智的选择,由于业务方彻底失去了对异常的控制能力。git

下方的函数 请求处理 不但永远不会执行,还没法在异常时作额外的处理,也没法阻止异常产生时笨拙的 console.log('请求失败') 行为。github

function fetch(callback) {
    setTimeout(() => {
        console.log('请求失败')
    })
}

fetch(() => {
    console.log('请求处理') // 永远不会执行
})复制代码

2. 回调,没法捕获的异常

回调函数有同步和异步之分,区别在于对方执行回调函数的时机,异常通常出如今请求、数据库链接等操做中,这些操做大可能是异步的。golang

异步回调中,回调函数的执行栈与原函数分离开,致使外部没法抓住异常。数据库

从下文开始,咱们约定用 setTimeout 模拟异步操做promise

function fetch(callback) {
    setTimeout(() => {
        throw Error('请求失败')
    })
}

try {
    fetch(() => {
        console.log('请求处理') // 永远不会执行
    })
} catch (error) {
    console.log('触发异常', error) // 永远不会执行
}

// 程序崩溃
// Uncaught Error: 请求失败复制代码

3. 回调,不可控的异常

咱们变得谨慎,不敢再随意抛出异常,这已经违背了异常处理的基本原则。浏览器

虽然使用了 error-first 约定,使异常看起来变得可处理,但业务方依然没有对异常的控制权,是否调用错误处理取决于回调函数是否执行,咱们没法知道调用的函数是否可靠。

更糟糕的问题是,业务方必须处理异常,不然程序挂掉就会什么都不作,这对大部分不用特殊处理异常的场景形成了很大的精神负担。

function fetch(handleError, callback) {
    setTimeout(() => {
        handleError('请求失败')
    })
}

fetch(() => {
    console.log('失败处理') // 失败处理
}, error => {
    console.log('请求处理') // 永远不会执行
})复制代码

番外 Promise 基础

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
})复制代码

4 Promise 异常处理

不只是 reject,抛出的异常也会被做为拒绝状态被 Promise 捕获。

function fetch(callback) {
    return new Promise((resolve, reject) => {
        throw Error('用户不存在')
    })
}

fetch().then(result => {
    console.log('请求处理', result) // 永远不会执行
}).catch(error => {
    console.log('请求处理异常', error) // 请求处理异常 用户不存在
})复制代码

5 Promise 没法捕获的异常

可是,永远不要在 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,就没有抓不住的异常。

6 Promise 异常追问

若是第三方函数在 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 基础

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 Awaitgenerator 有着莫大的关联,桥梁就是 生成器,咱们稍后介绍 生成器

番外 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 后面的表达式被执行,表达式的返回值被返回给了 await 执行处。

可是程序是怎么暂停的呢?只有 generator 能够暂停程序。那么等等,回顾一下 generator 的特性,咱们发现它也能够达到这种效果。

番外 async await 是 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 模拟,须要根据条件判断是否已经将函数执行完毕。

7 Async Await 异常

不管是同步、异步的异常,await 都不会自动捕获,但好处是能够自动中断函数,咱们大可放心编写业务逻辑,而不用担忧异步异常后会被执行引起雪崩:

function fetch(callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject()
        })
    })
}

async function main() {
    const result = await fetch()
    console.log('请求处理', result) // 永远不会执行
}

main()复制代码

8 Async Await 捕获异常

咱们使用 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()复制代码

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) // 永远不会执行
    } 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()复制代码

10 业务场景

在现在 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

Decorator 中文名是装饰器,核心功能是能够经过外部包装的方式,直接修改类的内部属性。

装饰器按照装饰的位置,分为 class decorator method decorator 以及 property decorator(目前标准还没有支持,经过 get set 模拟实现)。

Class Decorator

类级别装饰器,修饰整个类,能够读取、修改类中任何属性和方法。

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复制代码

Method Decorator

方法级别装饰器,修饰某个方法,和类装饰器功能相同,可是能额外获取当前修饰的方法名。

为了发挥这一特色,咱们篡改一下修饰的函数。

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复制代码

Property Decorator

属性级别装饰器,修饰某个属性,和类装饰器功能相同,可是能额外获取当前修饰的属性名。

为了发挥这一特色,咱们篡改一下修饰的属性值。

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复制代码

11 业务场景 统一异常捕获

咱们来编写类级别装饰器,专门捕获 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()复制代码

12 业务场景 没有后顾之忧的主动权

我想描述的意思是,在第 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 但愿结交有识之士!

相关文章
相关标签/搜索