RXJS 的错误处理和重试机制

错误处理机制是每个编程语言中必不可少的机制,通常使用 try...catch 来进行异常的捕获和处理。在 RXJS 中,有一套独有的方式进行错误处理,本文就对 RXJS 的错误处理和重试机制进行介绍。

错误处理

当数据流中的某个 Observable 发生异常时,需要进行异常捕获和处理,在 RXJS 中,有两种方式进行处理:

  • 使用 subscribe 函数的第二个参数
  • 使用 catch 操作符

使用 subscribe 函数的第二个参数

在使用 subscribe 函数连接 Observable 和 Observer 时,可以接收三个回调函数作为参数:

  • 当上游的 Observable 吐出数据时的回调函数
  • 当上游的 Observable 发生异常时的回调函数
  • 当上游的 Observable 终结(complete)时的回调函数

这里,需要用到 subscribe 的第二个参数。下面是一个简单的例子:

import { of } from "rxjs/observable/of";
import { map } from "rxjs/operators";
const source$ = of(1,2,3);
source$.pipe(
    map(v => {
        if(v % 2 === 0){
            throw new Error("Bad Number")
        }
        return v;
    })
).subscribe(console.log,(err) => {
    console.log(err.message)
})

运行结果:

1
Bad Number

上例中,通过 subscribe 捕获了数据流中异常,如果不想捕获异常,可以将该参数设置为 null

使用 catch 操作符

另一种捕获异常方式,可以使用 RXJS 提供的 catch 操作符:

import { of } from "rxjs/observable/of";
import { map, catchError } from "rxjs/operators";
const source$ = of(1,2,3);
source$.pipe(
    map(v => {
        if(v % 2 === 0){
            throw new Error("Bad Number")
        }
        return v;
    }),
    catchError((err) => {
        console.log(err.message)
        return of(-1)
    })
).subscribe(console.log)

运行结果:

1
Bad Number
-1

本例中,我们使用 catchError 操作符而不是 catch 操作符在管道中进行错误捕获,这是由我们的导包方式决定的。由于 catch 和 JavaScript 中的关键字 catch 冲突,于是 RXJS 提供了一个 catchError 别名来进行错误处理。使用 catchError 操作符时,其接收一个函数作为参数,该函数必须返回一个 Observable 对象,该 Observable 对象中的数据将会在发生异常时传递给下游,上面的例子中,我们只向下游传递了一个数字 -1,实际上如果你需要的话,还可以向下游传递任意数量的数据,具体的代码看下面的第三个例子。
同时,catchError 的参数函数,还可以使用第二个参数,其代表上游的 Observable 对象,当直接返回这个对象时,会启动 catchError 的重试机制。这里我们对代码再进行一些修改:

import { of } from "rxjs/observable/of";
import { map, catchError } from "rxjs/operators";
let flag:number = 0;
const source$ = of(1,2,3);
source$.pipe(
    map(v => {
        if(v % 2 === 0 && flag < 3){
            throw new Error("Bad Number")
        }
        return v;
    }),
    catchError((err,caught$) => {
        flag++;
        console.log(`第${flag}次重试:${err.message}`)
        return caught$;
    })
).subscribe(console.log)

运行结果:

1
第1次重试:Bad Number
1
第2次重试:Bad Number
1
第3次重试:Bad Number
1
2
3

传递给 catchError 的参数函数,如果将第二个参数 caught$ 直接返回,将会启动重试机制。本例中,为了防止 catchError 进行无限次重试,我设置了一个标志变量 flag,当 flag 累加为 3 时,map 操作符中就不再抛出错误,数据流状态变为正常。
注:采用下面的方式导包,就可以直接使用 catch 操作符,而无需使用 catchError

import { of } from "rxjs/observable/of";
import "rxjs/add/operator/map"
import "rxjs/add/operator/catch"
const source$ = of(1,2,3);
source$.map(v => {
    if(v % 2 === 0){
        throw new Error("Bad Number")
    }
    return v;
}).catch((err) => {
    console.log(err.message)
    return of(-1)
}).subscribe(console.log)

下面是一个捕获异常时,向下游传递任意数量数据的例子:

import { of } from "rxjs/observable/of";
import { interval } from "rxjs/observable/interval";
import "rxjs/add/operator/map"
import "rxjs/add/operator/catch"
import "rxjs/add/operator/take"
const source$ = of(1,2,3);
source$.map(v => {
    if(v % 2 === 0){
        throw new Error("Bad Number")
    }
    return v;
}).catch((err) => {
    console.log(err.message)
    return interval(500).take(3)
}).subscribe(console.log)

运行结果:

1
Bad Number
0
1
2

重试机制

上面的例子介绍了 RXJS 的错误处理机制,同时在使用 catch 操作符的时候,还可以启用重试机制,这是个非常优秀的特性。事实上,在 RXJS 中,针对重试操作还提供了两个专门的操作符 retryretryWhen

retry 操作符

retry 操作符接受一个 number 类型的参数,表示重试的次数,当上游的 Observable 发生异常后,使用 retry 操作符会立即进行重试,在有限的重试次数内如果异常仍未被处理,将会向下游抛出异常。
下面是一个例子:

import { of } from "rxjs/observable/of";
import "rxjs/add/operator/map"
import "rxjs/add/operator/catch"
import "rxjs/add/operator/retry"

const source$ = of(1,2,3)
const error$ = source$.map(v => {
    if(v === 2){
        throw new Error("BAD NUMBER")
    }
    return v;
})
error$.retry(3).catch((e) => {
    console.log(e.message)
    return of(-1)
}).subscribe(console.log)

运行结果:

1
1
1
1
BAD NUMBER
-1

如上,当上游的 Observable 发生异常时,使用 retry 后会立即进行重试,直到超出重试次数,再向下游抛出异常。

retry 操作符的缺陷

使用 retry 操作符来进行异常后的重试非常方便,但也有一些缺点。最明显的缺点莫过于使用 retry 操作符会在发生异常后立马重试。我们在请求后端接口的时候,当服务器发生错误时,如果能进行几次重试操作,用户体验将会大大增强,但是服务器发生错误后不大可能立马恢复工作。在使用 retry 操作符时,会在上游发生异常后立马进行重试,这时服务器可能还没有恢复过来呢,因此最好在某个时间段(比如 200 毫秒)之后再进行重试操作。
要在某个时间段之后进行重试操作,retry 操作符就无能为力了,此时需要使用 RXJS 提供的另一个重试操作符 retryWhen

retryWhen 操作符

retryWhen 操作符接收一个函数作为参数(也叫做 notifer),该函数返回一个 Observable 对象,下次重试,将在该 Observable 对象吐出值后进行。此外,该参数函数还有一个参数,这个参数是一个包含了错误信息的 Observable,在需要限制重试次数时,该对象十分有用。
下面是一个例子:

import { of } from "rxjs/observable/of";
import { interval } from "rxjs/observable/interval";
import "rxjs/add/operator/map"
import "rxjs/add/operator/catch"
import "rxjs/add/operator/retryWhen"

const source$ = of(1,2,3)
const error$ = source$.map(v => {
    if(v === 2){
        throw new Error("BAD NUMBER")
    }
    return v;
})
error$.retryWhen(() => {
    return interval(1000)
}).subscribe(console.log)

运行结果:

1
1
1
1
1
...

在上游发生异常后,在 retryWhen 操作符中,每过一秒钟都会进行重试,控制台会持续的输出 1。
当上游的异常恢复后,retryWhen 将不会重新订阅:

import { of } from "rxjs/observable/of";
import { interval } from "rxjs/observable/interval";
import "rxjs/add/operator/map"
import "rxjs/add/operator/catch"
import "rxjs/add/operator/retryWhen"

let flag:number = 0;
const source$ = of(1,2,3)
const error$ = source$.map(v => {
    if(v === 2 && flag < 3){
        flag++;
        throw new Error("BAD NUMBER")
    }
    return v;
})
error$.retryWhen(() => {
    return interval(1000)
}).subscribe(console.log)

运行结果:

1
1
1
1
2
3

上例中,在重试了 3 次后,标志变量 flag 变为 3,此时上游的异常恢复,停止重试。

使用 retryWhen 实现 retry

前面说到,retryWhen 的参数函数可以接受一个参数,该参数是一个 Observable 对象,其中保存了上游错误信息,每次上游发生异常后,这个 Observable 对象就会吐出一条数据,因此我们可以直接使用这个 Observable 对象来实现重试:

import { of } from "rxjs/observable/of";
import "rxjs/add/operator/map"
import "rxjs/add/operator/retryWhen"

let flag:number = 0;
const source$ = of(1,2,3)
const error$ = source$.map(v => {
    if(v === 2 && flag < 3){
        flag++;
        throw new Error("BAD NUMBER")
    }
    return v;
})
error$.retryWhen((err$) => {
    return err$
}).subscribe(console.log)

运行结果:

1
1
1
1
2
3

上面的代码实现中,当上游发生异常后就会立即重试,直到上游异常恢复,这一点很像前面介绍的 retry 操作符。但相比于前面的 retry 操作符,还有一点缺陷:无法像 retry 操作符一样,指定重试的次数,具体重试多少次依赖于上游的异常什么时候恢复,如果上游的异常一直不恢复,就会一直重试。从这一点来看,上面的代码,更像最开始提到的 catch 操作符的功能。
要使用 retryWhen 实现 retry,需要满足两个条件:

  • 重试指定的次数
  • 当超过重试次数后,向下游抛出一个异常

下面就来实现一个 myRetry 操作符,用来模拟 retry

import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/retryWhen";

Observable.prototype.myRetry = function (count){
    return this.retryWhen((err$) => {
        return err$.scan((errCount,err) => {
            if(errCount >= count){
                throw new Error(err)
            }
            return errCount + 1
        },0)
    })
}

上面的代码中,我们定义了一个 myRetry 操作符,并将其扩展到 Observable.prototype 上。下面对代码进行一些说明:
该操作符返回一个新的 Observable,该 Observable 对象使用上游的 Observable 对象调用 retryWhen 操作符的返回值。在这种情况下,下面两段代码是等价的:

source$.retryWhen(err$ => err$)
source$.myRetry()

retryWhen 操作符的内部,直接返回了 retryWhen 参数函数的 err$ 参数对象,前面说到,如果在 retryWhen 直接返回直接该对象,将会在上游发生异常后立马进行重试,这是我们向 retry 靠近的第一步。
同时,对 err$ 对象使用 scan 操作符进行规约,统计了上游发生异常的次数,该次数也就是重试操作的次数,因为上游每发生一次异常,就会进行一次重试。当重试次数大于传入的最大重试次数 count 时,就手动向下游抛出一个异常,异常的内容也就是 scan 操作符的第二个参数:异常对象。
注:关于 scan 操作符这里不进行过多的介绍,更多的内容请查看文档或相关的书籍。
现在来验证下我们自定义的 myRetry 操作符,看是否工作正常:

import { of } from "rxjs/observable/of";
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/map"
import "rxjs/add/operator/retryWhen"
import "rxjs/add/operator/scan"
import "rxjs/add/operator/catch"

Observable.prototype.myRetry = function (count){
    return this.retryWhen((err$) => {
        return err$.scan((errCount,err) => {
            if(errCount >= count){
                throw new Error(err)
            }
            return errCount + 1
        },0)
    })
}

const source$ = of(1,2,3)
const error$ = source$.map(v => {
    if(v === 2){
        throw new Error("BAD NUMBER")
    }
    return v;
})

error$.myRetry(3).catch((err) => {
    console.log("ERROR:",err.message)
    return of(-1)
}).subscribe(console.log)

运行结果:

1
1
1
1
ERROR: Error: BAD NUMBER
-1

如上,通过自定义操作符,结合 retryWhen 操作符,模拟实现了 retry

自定义实现 retry 的意义

使用 retryWhen 自定义实现 retry 操作符的意义在于,通过这个过程,我们理解了使用 retryWhen 控制重试次数的方式:

  • 在有限的重复次数内,进行重试
  • 超出重试次数,向下游抛出异常

再结合 retryWhen 提供了延迟重试功能,我们可以定义这样一个操作符,进行有限次的延迟重试。
这就需要对 myRetry 操作符的定义进行一些修改:

import { of } from "rxjs/observable/of";
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/map"
import "rxjs/add/operator/retryWhen"
import "rxjs/add/operator/scan"
import "rxjs/add/operator/catch"
import "rxjs/add/operator/delay"

Observable.prototype.myRetry = function (count,delayTime){
    return this.retryWhen((err$) => {
        return err$.scan((errCount,err) => {
            if(errCount >= count){
                throw new Error(err)
            }
            return errCount + 1
        },0).delay(delayTime)
    })
}

const source$ = of(1,2,3)
const error$ = source$.map(v => {
    if(v === 2){
        throw new Error("BAD NUMBER")
    }
    return v;
})

error$.myRetry(3,1000).catch((err) => {
    console.log("ERROR:",err.message)
    return of(-1)
}).subscribe(console.log)

运行结果:

1
1
1
1
ERROR: Error: BAD NUMBER
-1

上面的示例代码,在上游 Observable 发生异常后,会每隔 1000ms 重试一次,三次后若上游的 Observable 异常仍未恢复,就向下游抛出错误。
通过对 myRetry 操作符进行修改:接收一个延迟时间参数,在 retryWhen 的参数函数中返回带有错误信息的 Observable 对象时,使用了 delay 操作符,在该操作符的作用下,会在某个时间段之后再向下游吐出数据,这样就实现了一个有限次并且具备延时重试功能的操作符。如果想让重试立即进行,不需要延迟,只需将 myRetry 操作符的第二个参数传为 0 即可。

递增延时重试

在上面的代码中,每次重试间隔的时间段都是一样的。如果我们想要这样的功能:第一次重试在 200ms 后进行,第二次重试在 400ms 后进行,第三次重试在 600ms 后进行...这样的功能,在某种程度上具有更好的用户体验,倘若服务器出现了错误,我们每次重试最好在前一次重试的基础上增加一些时间,以减轻对服务器的压力(毕竟服务器已经挂了,鸭梨山大呀,还是悠着点吧)。
要实现递增延时重试,使用 delay 操作符就不行了,就好比实现延时重试,就不能再使用 retry 操作符而是使用 retryWhen 操作符,在使用递增延时重试时,就需要使用 delayWhen 操作符。
我们重新定义一个用来实现递增延时重试的方法 myRetryAutoIncrease,下面是代码实现:

Observable.prototype.myRetryAutoIncrease = function (count,initialDelayTime){
    return this.retryWhen((err$) => {
        return err$.scan((errCount,err) => {
            if(errCount >= count){
                throw new Error(err)
            }
            return errCount + 1
        },0).delayWhen(errCount => {
            return timer(initialDelayTime * errCount)
        })
    })
}

myRetryAutoIncrease 操作符接受两个参数:

  • 重试的最大次数
  • 初始延迟时间

下面是这个操作符的使用示例:

import { of } from "rxjs/observable/of";
import { timer } from "rxjs/observable/timer";
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/map"
import "rxjs/add/operator/retryWhen"
import "rxjs/add/operator/scan"
import "rxjs/add/operator/catch"
import "rxjs/add/operator/delayWhen"

Observable.prototype.myRetryAutoIncrease = function (count,initialDelayTime){
    return this.retryWhen((err$) => {
        return err$.scan((errCount,err) => {
            if(errCount >= count){
                throw new Error(err)
            }
            return errCount + 1
        },0).delayWhen(errCount => {
            return timer(initialDelayTime * errCount)
        })
    })
}


const source$ = of(1,2,3)
const error$ = source$.map(v => {
    if(v === 2){
        throw new Error("BAD NUMBER")
    }
    return v;
})

error$.myRetryAutoIncrease(3,1000).catch((err) => {
    console.log("ERROR:",err.message)
    return of(-1)
}).subscribe(console.log)

运行结果:

1
1
1
1
ERROR: Error: BAD NUMBER
-1

重试机制的本质

重试机制的本质,就是在上游的 Observable 发生异常后,对上游的 Observable 对象进行取消订阅和重新订阅操作,直到上游的 Observable 异常恢复为止。
下面是一个实例验证:

import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/map"
import "rxjs/add/operator/retryWhen"
import "rxjs/add/operator/catch"
import "rxjs/add/operator/delay"

let index:number = 0;
const source$ = Observable.create((observer) => {
    console.log("开始订阅拉~")
    const timer = setInterval(() => {
        observer.next(index++)
    },500)
    return {
        unsubscribe(){
            clearInterval(timer)
            console.log("取消订阅拉~")
        }
    }
})

const error$ = source$.map(v => {
    if(v % 2 === 0){
        throw new Error("BAD NUMBER")
    }
    return v;
})

error$.retryWhen((err$) => err$.delay(1000)).subscribe(console.log)

运行结果:

开始订阅拉~
取消订阅拉~
开始订阅拉~
1
取消订阅拉~
开始订阅拉~
3
取消订阅拉~
开始订阅拉~
5
取消订阅拉~
...

当上游的 Observable 发生异常后,会立马对上游的 Observable 进行退订,在一段时间后进行重新订阅,直到上游的 Observable 不再产生异常为止。

总结

本文主要总结了 RXJS 中的错误处理机制,重试机制以及重试机制的本质。在重试机制中,主要介绍了 retryretryWhen 两个操作符,以及基于 retryWhen 操作符对重试操作进行自定义,这部分内容很重要,需要进行掌握。

完。