-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Callback Promise Generator Async-Await 和异常处理的演进 #14
Comments
如果每个类中的方法都用try/catch包裹,是否有性能问题? |
@acthtml 异常可以在最外层统一处理,可以不用每个可能出现异常的地方都写上try/catch |
@codezyc 我的意思就是使用Decorator之后,对性能影响如何? 不过这个异常捕获方案的确不错。 |
装饰器仅在初始化时工作,此时函数已被替换,性能问题基本上可以忽略。 try catch 造成的性能影响不必担心,try 中直接调用函数,性能几乎不受影响,而带来的可维护性价值很大,见下图: 几乎所有程序都需要错误上报机制,就算自己不写 try catch,库不写 try catch(库中含有大量的 try catch),业务代码最上层也会有 try catch 捕获和收集异常,所以不用纠结,现在就开始用吧~ |
写的太赞了,我也要把这个方案应用到我的项目中 |
用 decorator 来做切面异常处理的思路很赞。 对写 try catch 这一点做下探讨
在浏览器端可能是这样,但在后端思路可能是 fail fast,不要兜住任何开发者编程时预知不到的错误类型,开发者不应该到处 try catch,由多进程模型来保证高可用,比如 egg 的 cluster。 |
@paranoidjk 滥 try 确实非常影响代码质量,在前端建议只在调用第三方接口时使用(比如发 http 请求),这已经是业务层行为,还是比较合理的。 至于框架层,兜住异常并吞掉是自取毁灭的行为。 |
异常真正的应用场景是异常场景 |
根据笔者的项目经验,本文讲解了从函数回调,到
es7
规范的异常处理方式。异常处理的优雅性随着规范的进步越来越高,不要害怕使用try catch
,不能回避异常处理。我们需要一个健全的架构捕获所有同步、异步的异常。业务方不处理异常时,中断函数执行并启用默认处理,业务方也可以随时捕获异常自己处理。
优雅的异常处理方式就像冒泡事件,任何元素可以自由拦截,也可以放任不管交给顶层处理。
1. 回调
如果在回调函数中直接处理了异常,是最不明智的选择,因为业务方完全失去了对异常的控制能力。
下方的函数
请求处理
不但永远不会执行,还无法在异常时做额外的处理,也无法阻止异常产生时笨拙的console.log('请求失败')
行为。2. 回调,无法捕获的异常
回调函数有同步和异步之分,区别在于对方执行回调函数的时机,异常一般出现在请求、数据库连接等操作中,这些操作大多是异步的。
异步回调中,回调函数的执行栈与原函数分离开,导致外部无法抓住异常。
3. 回调,不可控的异常
我们变得谨慎,不敢再随意抛出异常,这已经违背了异常处理的基本原则。
虽然使用了
error-first
约定,使异常看起来变得可处理,但业务方依然没有对异常的控制权,是否调用错误处理取决于回调函数是否执行,我们无法知道调用的函数是否可靠。更糟糕的问题是,业务方必须处理异常,否则程序挂掉就会什么都不做,这对大部分不用特殊处理异常的场景造成了很大的精神负担。
番外 Promise 基础
Promise
是一个承诺,只可能是成功、失败、无响应三种情况之一,一旦决策,无法修改结果。Promise
不属于流程控制,但流程控制可以用多个Promise
组合实现,因此它的职责很单一,就是对一个决议的承诺。resolve
表明通过的决议,reject
表明拒绝的决议,如果决议通过,then
函数的第一个回调会立即插入microtask
队列,异步立即执行。如果决议结果是决绝,那么
then
函数的第二个回调会立即插入microtask
队列。如果一直不决议,此
promise
将处于pending
状态。未捕获的
reject
会传到末尾,通过catch
接住resolve
决议会被自动展开(reject
不会)链式流,
then
会返回一个新的Promise
,其状态取决于then
的返回值。4 Promise 异常处理
不仅是
reject
,抛出的异常也会被作为拒绝状态被Promise
捕获。5 Promise 无法捕获的异常
但是,永远不要在
macrotask
队列中抛出异常,因为macrotask
队列脱离了运行上下文环境,异常无法被当前作用域捕获。不过
microtask
中抛出的异常可以被捕获,说明microtask
队列并没有离开当前作用域,我们通过以下例子来证明:至此,
Promise
的异常处理有了比较清晰的答案,只要注意在macrotask
级别回调中使用reject
,就没有抓不住的异常。6 Promise 异常追问
如果第三方函数在
macrotask
回调中以throw Error
的方式抛出异常怎么办?值得欣慰的是,由于不在同一个调用栈,虽然这个异常无法被捕获,但也不会影响当前调用栈的执行。
我们必须正视这个问题,唯一的解决办法,是第三方函数不要做这种傻事,一定要在
macrotask
抛出异常的话,请改为reject
的方式。请注意,如果
return thirdFunction()
这行缺少了return
的话,依然无法抓住这个错误,这是因为没有将对方返回的Promise
传递下去,错误也不会继续传递。我们发现,这样还不是完美的办法,不但容易忘记
return
,而且当同时含有多个第三方函数时,处理方式不太优雅:是的,我们还有更好的处理方式。
番外 Generator 基础
generator
是更为优雅的流程控制方式,可以让函数可中断执行:yield
关键字后面可以包含表达式,表达式会传给next().value
。next()
可以传递参数,参数作为yield
的返回值。这些特性足以孕育出伟大的生成器,我们稍后介绍。下面是这个特性的例子:
第一个 next 是没有参数的,因为在执行
generator
函数时,初始值已经传入,第一个next
的参数没有任何意义,传入也会被丢弃。这一句,返回值不是想当然的
5
。其的作用是将5
传递给genB.next()
,其值,由下一个 nextgenB.next(7)
传给了它,所以语句等于const result = 7
。最后一个
genBValue
,是最后一个next
的返回值,这个值,就是函数的return
值,显然为undefined
。我们回到这个语句:
如果返回值是 5,是不是就清晰了许多?是的,这种语法就是
await
。所以Async Await
与generator
有着莫大的关联,桥梁就是 生成器,我们稍后介绍 生成器。番外 Async Await
如果认为
Generator
不太好理解,那Async Await
绝对是救命稻草,我们看看它们的特征:所见即所得,
await
后面的表达式被执行,表达式的返回值被返回给了await
执行处。但是程序是怎么暂停的呢?只有
generator
可以暂停程序。那么等等,回顾一下generator
的特性,我们发现它也可以达到这种效果。番外 async await 是 generator 的语法糖
终于可以介绍 生成器 了!它可以魔法般将下面的
generator
执行成为await
的效果。下面的代码就是生成器了,生成器并不神秘,它只有一个目的,就是:
达到这个目标不难,达到了就完成了
await
的功能,就是这么神奇。利用生成器,模拟出
await
的执行效果:可以看出,
await
的执行次数由程序自动控制,而回退到generator
模拟,需要根据条件判断是否已经将函数执行完毕。7 Async Await 异常
不论是同步、异步的异常,
await
都不会自动捕获,但好处是可以自动中断函数,我们大可放心编写业务逻辑,而不用担心异步异常后会被执行引发雪崩:8 Async Await 捕获异常
我们使用
try catch
捕获异常。认真阅读
Generator
番外篇的话,就会理解为什么此时异步的异常可以通过try catch
来捕获。因为此时的异步其实在一个作用域中,通过
generator
控制执行顺序,所以可以将异步看做同步的代码去编写,包括使用try catch
捕获异常。9 Async Await 无法捕获的异常
和第五章 Promise 无法捕获的异常 一样,这也是
await
的软肋,不过任然可以通过第六章的方案解决:现在解答第六章尾部的问题,为什么
await
是更加优雅的方案:10 业务场景
在如今
action
概念成为标配的时代,我们大可以将所有异常处理收敛到action
中。我们以如下业务代码为例,默认不捕获错误的话,错误会一直冒泡到顶层,最后抛出异常。
为了防止程序崩溃,需要业务线在所有 async 函数中包裹
try catch
。我们需要一种机制捕获
action
最顶层的错误进行统一处理。为了补充前置知识,我们再次进入番外话题。
番外 Decorator
Decorator
中文名是装饰器,核心功能是可以通过外部包装的方式,直接修改类的内部属性。装饰器按照装饰的位置,分为
class decorator
method decorator
以及property decorator
(目前标准尚未支持,通过get
set
模拟实现)。Class Decorator
类级别装饰器,修饰整个类,可以读取、修改类中任何属性和方法。
Method Decorator
方法级别装饰器,修饰某个方法,和类装饰器功能相同,但是能额外获取当前修饰的方法名。
为了发挥这一特点,我们篡改一下修饰的函数。
Property Decorator
属性级别装饰器,修饰某个属性,和类装饰器功能相同,但是能额外获取当前修饰的属性名。
为了发挥这一特点,我们篡改一下修饰的属性值。
11 业务场景 统一异常捕获
我们来编写类级别装饰器,专门捕获
async
函数抛出的异常:将类所有方法都用
try catch
包裹住,将异常交给业务方统一的errorHandler
处理:我们也可以编写方法级别的异常处理:
业务方用法类似,只是装饰器需要放在函数上:
12 业务场景 没有后顾之忧的主动权
我想描述的意思是,在第 11 章这种场景下,业务方是不用担心异常导致的
crash
,因为所有异常都会在顶层统一捕获,可能表现为弹出一个提示框,告诉用户请求发送失败。业务方也不需要判断程序中是否存在异常,而战战兢兢的到处
try catch
,因为程序中任何异常都会立刻终止函数的后续执行,不会再引发更恶劣的结果。而 js 异常冒泡的方式,在前端可以用提示框兜底,nodejs端可以返回 500 错误兜底,并立刻中断后续请求代码,等于在所有危险代码身后加了一层隐藏的
return
。同时业务方也握有绝对的主动权,比如登录失败后,如果账户不存在,那么直接跳转到注册页,而不是傻瓜的提示用户帐号不存在,可以这样做:
补充
在
nodejs
端,记得监听全局错误,兜住落网之鱼:在浏览器端,记得监听
window
全局错误,兜住漏网之鱼:如有错误,欢迎斧正,本人 github 主页:https://github.com/ascoders 希望结交有识之士!
The text was updated successfully, but these errors were encountered: