Callback Promise Generator Async-Await 和异常处理的演进
2017-02-04 16:38
801 查看
根据笔者的项目经验,本文讲解了从函数回调,到
我们需要一个健全的架构捕获所有同步、异步的异常。业务方不处理异常时,中断函数执行并启用默认处理,业务方也可以随时捕获异常自己处理。
优雅的异常处理方式就像冒泡事件,任何元素可以自由拦截,也可以放任不管交给顶层处理。
文字讲解仅是背景知识介绍,不包含对代码块的完整解读,不要忽略代码块的阅读。
下方的函数
异步回调中,回调函数的执行栈与原函数分离开,导致外部无法抓住异常。
从下文开始,我们约定用
虽然使用了
更糟糕的问题是,业务方必须处理异常,否则程序挂掉就会什么都不做,这对大部分不用特殊处理异常的场景造成了很大的精神负担。
简单补充下事件循环的知识,js 事件循环分为 macrotask 和 microtask。
microtask 会被插入到每一个 macrotask 的尾部,所以 microtask 总会优先执行,哪怕 macrotask 因为 js 进程繁忙被 hung 住。
比如
如果决议结果是决绝,那么
如果一直不决议,此
未捕获的
链式流,
不过
至此,
值得欣慰的是,由于不在同一个调用栈,虽然这个异常无法被捕获,但也不会影响当前调用栈的执行。
我们必须正视这个问题,唯一的解决办法,是第三方函数不要做这种傻事,一定要在
请注意,如果
我们发现,这样还不是完美的办法,不但容易忘记
是的,我们还有更好的处理方式。
这些特性足以孕育出伟大的生成器,我们稍后介绍。下面是这个特性的例子:
第一个 next 是没有参数的,因为在执行
这一句,返回值不是想当然的
最后一个
我们回到这个语句:
如果返回值是 5,是不是就清晰了许多?是的,这种语法就是
所见即所得,
但是程序是怎么暂停的呢?只有
下面的代码就是生成器了,生成器并不神秘,它只有一个目的,就是:
所见即所得,
达到这个目标不难,达到了就完成了
利用生成器,模拟出
可以看出,
认真阅读
因为此时的异步其实在一个作用域中,通过
现在解答第六章尾部的问题,为什么
我们以如下业务代码为例,默认不捕获错误的话,错误会一直冒泡到顶层,最后抛出异常。
为了防止程序崩溃,需要业务线在所有 async 函数中包裹
我们需要一种机制捕获
为了补充前置知识,我们再次进入番外话题。
装饰器按照装饰的位置,分为
为了发挥这一特点,我们篡改一下修饰的函数。
为了发挥这一特点,我们篡改一下修饰的属性值。
将类所有方法都用
我们也可以编写方法级别的异常处理:
业务方用法类似,只是装饰器需要放在函数上:
业务方也不需要判断程序中是否存在异常,而战战兢兢的到处
像 golang 中异常处理方式,就存在这个问题
通过 err, result := func() 的方式,虽然固定了第一个参数是错误信息,但下一行代码免不了要以
而 js 异常冒泡的方式,在前端可以用提示框兜底,nodejs端可以返回 500 错误兜底,并立刻中断后续请求代码,等于在所有危险代码身后加了一层隐藏的
同时业务方也握有绝对的主动权,比如登录失败后,如果账户不存在,那么直接跳转到注册页,而不是傻瓜的提示用户帐号不存在,可以这样做:
在浏览器端,记得监听
如有错误,欢迎斧正,本人 github 主页:https://github.com/ascoders 希望结交有识之士!
es7规范的异常处理方式。异常处理的优雅性随着规范的进步越来越高,不要害怕使用
try catch,不能回避异常处理。
我们需要一个健全的架构捕获所有同步、异步的异常。业务方不处理异常时,中断函数执行并启用默认处理,业务方也可以随时捕获异常自己处理。
优雅的异常处理方式就像冒泡事件,任何元素可以自由拦截,也可以放任不管交给顶层处理。
文字讲解仅是背景知识介绍,不包含对代码块的完整解读,不要忽略代码块的阅读。
1. 回调
如果在回调函数中直接处理了异常,是最不明智的选择,因为业务方完全失去了对异常的控制能力。下方的函数
请求处理不但永远不会执行,还无法在异常时做额外的处理,也无法阻止异常产生时笨拙的
console.log('请求失败')行为。
function fetch(callback) { setTimeout(() => { console.log('请求失败') }) } fetch(() => { console.log('请求处理') // 永远不会执行 })
2. 回调,无法捕获的异常
回调函数有同步和异步之分,区别在于对方执行回调函数的时机,异常一般出现在请求、数据库连接等操作中,这些操作大多是异步的。异步回调中,回调函数的执行栈与原函数分离开,导致外部无法抓住异常。
从下文开始,我们约定用
setTimeout模拟异步操作
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 Await与
generator有着莫大的关联,桥梁就是 生成器,我们稍后介绍 生成器。
番外 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 主页:https://github.com/ascoders 希望结交有识之士!
相关文章推荐
- Callback Promise Generator Async-Await 和异常处理的演进_1
- Callback Promise Generator Async-Await 和异常处理的演进
- Callback Promise Generator Async-Await 和异常处理的演进_0
- Callback Promise Generator Async-Await 和异常处理的演进
- Callback Promise Generator Async-Await 和异常处理的演进_2
- 关于Promise,Generator,async / await 对异步的处理
- 前端面试送命题(二)-callback,promise,generator,async-await
- Callback, Promise和Async/Await的对比
- .NET异步操作学习之一:Async/Await中异常的处理
- node-使用promise, generator, async/await 读取文件的方法
- callback, promise, co/yield, async/await 大混战
- ES6总结--Promise 、Generator 、Async/Await
- 关于async & await(TAP)异步模型的异常捕获
- 协程(Coroutine)-ES中关于Generator/async/await的学习思考
- ECMAScript 6 入门笔记(五)异步promise,Generator,async
- Promise 、Async/Await的使用
- Async下处理多个异常
- asp.net ashx 一般处理程序 使用async await异步直接 copy可用哦
- [TypeScript] Simplify asynchronous callback functions using async/await
- 异常处理:nested exception is java.lang.NoClassDefFoundError: net/sf/cglib/proxy/CallbackFilter