这是我参与「掘金日新计划 · 12 月更文挑战」的第2天 点击查看活动详情
前言
在异步处 理中,try/catch⽅法派不上⽤场,使⽤回调函数或者Promise来处理异常虽 然能够解决问题,但是依然存在诸多缺点。RxJS实践的是函数式编程,函 数式编程有不同于传统命令式编程的特点,这就要求我们⽤⼀种全新的思 维⽅式来进⾏异常错误处理。
-
捕获并处理上游产生的异常错误 —— catch
-
当上游产生错误时进行重试 —— retry和retryWhen
-
无论是否出错都要进行一些操作—— finally
程序之外的⼀切都不是百分之百靠得住,开发者的责任之 ⼀,就是要预料到这些异常可能发⽣,在代码中做出对应的预防措施。
程序中产⽣异常还有可能是代码⾃⾝造成的,⽐如访问⼀个可能是 null或者undefined的对象的属性,就会导致⼀个异常发⽣。
JavaScript⾃带的try/catch⽅式就很⿇烦。当错误发⽣的时候,JavaScript运⾏环境会中⽌当前指令,创建⼀个指向产⽣错误指令的栈 跟踪信息(Stack Trace),包含错误信息、代码⾏号和代码所在⽂件名, 把这些信息封在Error对象中,然后沿着执⾏栈⼀层⼀层往上找catch区块, 如果找不到,那就只能交给JavaScript运⾏环境⽤默认⽅法处理。
try/catch⽅式只适⽤于同步代码指令,对于异步操作, try/catch就完全⽆⽤武之地了。
Promise的异常处理
交给Promise⼀件事情,它⼀定会 有⼀个结果的,要么成功要么失败,⽽且绝对只会有⼀个结果,不会三⼼ ⼆意有了⼀个结果之后变卦改为另⼀个结果。 ⼀个标准的Promise对象有三种状态,预备状态(pending)、成功状态 (fulfilled)和失败状态(rejected)。
Promise的异常处理依然存在不⾜之处。最主要的缺点就是,Promise 不能够重试。实际应⽤中,遇到⼀个失败的情况,重试是⼀个不错的选 择。⽐如⼀个对服务器的API调⽤,第⼀次访问失败,可能只是⽹络临时 发⽣故障,或者服务器临时出错,如果过⼀会⼉⾃动重新调⽤⼀次,可能 就能返回正确的结果了,这⽐不做重试直接显⽰给⽤户⼀个出错提⽰更 好,要知道,⽤户看到出错提⽰之后,很可能也会选择重试,既然这样, 还不如让这个重试的⼯作由代码来做。
Promise还有⼀个缺点是,并不强制要求异常被捕获。
RxJS的异常处理
在RxJS中,错误异常和数据⼀样,会沿着数据流管道从上游向下游流 动,流过所有的过滤类或者转化类操作符,最后触发Observer的error⽅法。
错误异常只存在于⾃⼰所处的数据流管道中,不会像 try/catch那样影响全局,在⼀个数据流管道中奔流的错误,绝不会影响另⼀ 个管道,⾄于怎么处理这个错误异常,则交给Observer来处理。
对错误异常的处理可以分为两类: - 恢复(recover) - 重试(retry)
恢复,就是本来虽然产⽣了错误异常,但是依然让运算继续下去。
重试,就是当发⽣错误异常的时候,认为这个错误只是临时的,重新 尝试之前发⽣错误的操作,寄希望于重试之后能够获得正常的结果。
重试和恢复往往配合使⽤,因为重试往往是有次数限 制的,不能⽆限重试,如果尝试了次数上限之后得到的依然是错误异常, 还是要⽤“恢复”的⽅法获得默认值继续运算。
catch主要⽤于“恢复”⼯作,⽽retry和retryWhen就像它们的名字 表⽰的⼀样,⽤于“重试”⼯作。
1.catch 捕获 observable 中的错误
在RxJS数据流中产⽣的错误会沿着管道流动,因为⼤部分 操作符都会忽略错误,直接转⼿给下游,⼀直传递给Observer,但是catch 这种操作符⽐较特殊,它会在管道中捕获上游传递过来的错误。
const { of, range, interval } = rxjs;
const { scan, map,catchError } = rxjs.operators;
const throwOnUnluckyNumber = value => {
if (value === 4) {
throw new Error('unlucky number 4');
}
return value;
};
const source$ = range(1, 5);
const error$ = source$.pipe(
map(throwOnUnluckyNumber)
)
const catch$ = error$.pipe(
catchError((err, caught$) => of(8))
)
catch$.subscribe(console.log); // 1 2 3 8
在上⾯的代码中,source会 在吐出1、2、3之后就吐出⼀个Error对象。
2.retry 重试 observable 中的错误
上⾯介绍的catch主要⽤于“恢复”,但是这种恢复只是往数据流管道⾥ 塞另外的数据,让数据流得以继续,很多时候,这样还是不够的,毕竟塞 进去的数据并不是真正预期的数据,这时候,如果重来⼀次有可能获得正 确结果,就应该⽤上“重试”,retry就是⽤来重试的操作符之⼀。
要注意,只有对于再来⼀次有可能成功的操作,才有重试的必要,⽐ 如访问服务器API的操作,失败的原因是服务器崩溃,那就可以重试;对 于必定会失败的操作,⽐如上⾯代码例⼦中使⽤throwOnUnluckyNumber的 error$对象,⽆论重试多少次都是失败,就没有必要重试了。
retry这个操作符的作⽤就是让上游的Observable重新⾛⼀遍,达到重试 的⽬的。
这个操作符接受⼀个数值参数number,number⽤于指定重试的次数, 如果number为负数或者没有number参数,那么就是⽆限次retry,直到上游 不再抛出错误异常为⽌。
配合使⽤retry和catch的代码如下:
const { of, range, interval } = rxjs;
const { scan, map,catchError ,retry} = rxjs.operators;
const throwOnUnluckyNumber = value => {
if (value === 4) {
throw new Error('unlucky number 4');
}
return value;
};
const source$ = range(1, 5);
const error$ = source$.pipe(
map(throwOnUnluckyNumber)
)
const catch$ = error$.pipe(
catchError((err, caught$) => of(8))
)
const retry$ = error$.pipe(retry(2))
retry$.subscribe(console.log); // 123 123 123 8
Promise不能重试,这是Promise的⼀个劣势,但是 RxJS的Observable可以重试,所以能够更好地处理现实场景。
retry也有⼀个问题,当上游传下错误时,retry会⽴即开始重 试,⽽现实中这种处理⽅式未必合理,还是以访问服务器API为例,服务 器返回⼀个错误,⽴刻重新访问这个API,很可能还是返回⼀个错误,因 为我们都知道服务器要是因为崩溃出问题,不⼤能瞬间恢复正常,最好的 策略是稍微等待⼀段时间之后再重新尝试。
retry这个操作符还不满⾜延时重试的要求,所以,还需要另外 ⼀个操作符,那就是retryWhen。
3.retryWhen ## 当发生错误时,基于自定义的标准来重试
retryWhen接受⼀个函数作为参数,这个参数称为notifer,⽤于控制“重 试”的节奏和次数,这⽐retry单纯只能控制重试次数要前进⼀步。
notifer返回⼀个 Observable对象,当上游扔下来错误的时候,retryWhen就会调⽤notifer,然 后根据notifer返回的Observable对象来决定何时重试,这个返回的 Observable就是⼀个“节奏控制器”,“节奏控制器”每吐出⼀个数据,就会进 ⾏⼀次重试。
当“节奏控制器”完结的时候,retryWhen返回的Observable对象也会⽴ 刻完结。
const { timer, range, interval } = rxjs;
const { tap, map, retryWhen, delayWhen } = rxjs.operators;
// 每1秒发出值
const source = interval(1000);
const example = source.pipe(
map(val => {
if (val > 5) {
// 错误将由 retryWhen 接收
throw val;
}
return val;
}),
retryWhen(errors =>
errors.pipe(
// 输出错误信息
tap(val => console.log(`Value ${val} was too high!`)),
// 5秒后重启
delayWhen(val => timer(val * 1000))
)
)
);
/*
输出:
0
1
2
3
4
5
"Value 6 was too high!"
--等待5秒后然后重复此过程
*/
const subscribe = example.subscribe(val => console.log(val));
还有⼀个finally,⽤于执⾏⽆论出错还是不出错都要做的事情。比较容易理解就不过多赘述了。
因为⽆论是retryWhen还是retry,所谓的“重试”,其实就是重新订阅 (subscribe)⼀遍上游Observable对象的过程,在订阅上游的同时,会退订 上⼀次的订阅。
小结
Promise可以解决回调函数⽅式的问题,但是Promise⾃⾝不能够重试。
RxJS⾃带的操作符⽐较完美地解决了所有问题,使⽤catch、retry和 retryWhen,可以⽅便地⽀持“恢复”和“重试”两类异常处理⽅式。
retryWhen和scan、delay等操作符结合,可以⾮常⽅便定制出任意重试 功能,可见RxJS功能的强⼤。
参考
参考《深入浅出RxJs》——程墨