错误处理机制是每个编程语言中必不可少的机制,通常使用 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 中,针对重试操作还提供了两个专门的操作符 retry
和 retryWhen
。
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 中的错误处理机制,重试机制以及重试机制的本质。在重试机制中,主要介绍了 retry
和 retryWhen
两个操作符,以及基于 retryWhen
操作符对重试操作进行自定义,这部分内容很重要,需要进行掌握。
完。