(七)温故知新系列之RXJS——RXJS操作符基础(异常处理)

359 阅读5分钟

这是我参与「掘金日新计划 · 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产⽣⼀系列递增正整数,这些正整数经过throwOnUnluckyNumber之后,其中的正整数4会引发错误,所以error产⽣⼀系列递增正整数,这些正整数经过 throwOn-UnluckyNumber之后,其中的正整数4会引发错误,所以error会 在吐出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》——程墨