今天来介绍一些跟「错误处理」有关的 operators。在使用 RxJS 时,资料流是透过 pipe
及各式各样的 operators
在处理,且很多时候是非同步的,因此大多时候发生错误并不能单纯的使用 try...catch
方式处理,就需要透过这些错误处理相关的 operators 来帮忙罗!
catchError
可以在来源 Observable 发生错误时,进行额外的处理,一般来说发生错误时,都是在订阅时使用处理:
interval(1000)
.pipe(
map(data => {
if (data % 2 === 0) {
return data;
} else {
throw new Error('发生错误');
}
}),
)
.subscribe({
next: data => {
console.log(`catchError 示范 (1): ${data}`);
},
error: error => {
console.log(`catchError 示范 (1): 错误 - ${error}`);
}
});
// catchError 示范 (1): 0
// catchError 示范 (1): 错误 - Error: 发生错误
// (发生错误,整个资料流中断)
弹珠图:
---0---#
但订阅毕竟不是整个 Observable 资料流的一部份,而是我们订阅时自己撰写的逻辑,如果要将错误处理也视为整个 Observable 的一部份,就可以使用 catchError
,catchError
内会传入错误讯息,且需要回传另一个 Observable,当过程中错误发生时,就会改成使用 catchError
回传的 Observable,让後续的其他 operators 可以继续下去,而不会中断整个资料流:
interval(1000)
.pipe(
map(data => {
if (data % 2 === 0) {
return data;
} else {
throw new Error('发生错误');
}
}),
catchError(error => {
return interval(1000);
}),
map(data => data * 2)
)
.subscribe({
next: data => {
console.log(`catchError 示范 (2): ${data}`);
},
error: error => {
console.log(`catchError 示范 (2): 错误 - ${error}`);
}
});
// catchError 示范 (2): 0
// (这时候来源 Observable 发生错误,用另一个 Observable 取代)
// (以下是错误处理後新的 Observable)
// catchError 示范 (2): 0
// catchError 示范 (2): 2
// catchError 示范 (2): 4
弹珠图:
---0---#
catchError(---0---1---2...)
---0-------0----1----2...
^ 发生错误,换成 catchError 内的 Observable
map(data => data * 2)
---0-------0----2----4...
如果遇到不能处理的问题,也可以就让错误发生,此时只需要回传 throwError
即可:
interval(1000)
.pipe(
map(data => {
if (data % 2 === 0) {
return data;
} else {
throw new Error('发生错误');
}
}),
catchError(error => {
if(error === null) {
return interval(1000);
}
return throwError(error);
})
)
.subscribe({
next: data => {
console.log(`catchError 示范 (3): ${data}`);
},
error: error => {
console.log(`catchError 示范 (3): 错误 - ${error}`);
}
});
// catchError 示范 (3): 0
// catchError 示范 (3): 错误 - Error: 发生错误
// (发生错误,整个资料流中断)
在 Observable 中,不论是 throw new Error()
还是回传 throwError()
都会产生错误并中断资料流,所以前面程序使用 map
处理错误的逻辑也可以改成:
switchMap(data => iif(() => data % 2 === 0, of(data), throwError('发生错误')))
会更有 functional programming 的风格!
程序码:https://stackblitz.com/edit/mastering-rxjs-operator-catcherror
当 Observable 发生错误时,可以使用 retry
来重试整个 Observable,在 retry
内可以指定重试几次:
interval(1000)
.pipe(
switchMap(data =>
iif(() => data % 2 === 0, of(data), throwError('发生错误'))),
map(data => data + 1),
retry(3),
)
.subscribe({
next: data => {
console.log(`retry 示范 (1): ${data}`);
},
error: error => {
console.log(`retry 示范 (1): 错误 - ${error}`);
}
});
// retry 示范 (1): 1
// (发生错误,重试第 1 次)
// retry 示范 (1): 1
// (发生错误,重试第 2 次)
// retry 示范 (1): 1
// (发生错误,重试第 3 次)
// retry 示范 (1): 1
// (发生错误,已经重试 3 次了,不在重试,直接让错误发生)
// retry 示范 (1): 错误 - 发生错误
弹珠图:
---1---#
retry(3)
---1------1------1------1---#
^ 发生错误,重试第 1 次
^ 发生错误,重试第 2 次
^ 发生错误,重试第 3 次
^ 不在重试,直接让错误发生
若不指定次数,预设为 -1
,代表会持续重试;若不想重试,也可以指定次数为 0
,就会直接让错误发生。
程序码:https://stackblitz.com/edit/mastering-rxjs-opereator-retry
retryWhen
也可以再发生错误时进行重试,但 retryWhen
更有弹性,在 retryWhen
内需要设计一个 notifier
callback function,retryWhen
会将错误资讯传入 notifier
function,同时需要回传一个 Observable,retryWhen
会订阅这个 Observable,每当有事件发生时,就进行重试,直到这个回传的 Observable 结束,才停止重试。
以下程序在错误发生时,会每三秒重试一次,共重试三次:
interval(1000)
.pipe(
switchMap(data =>
iif(() => data % 2 === 0, of(data), throwError('发生错误'))),
map(data => data + 1),
retryWhen((error) => interval(3000).pipe(take(3)))
)
.subscribe({
next: data => {
console.log(`retryWhen 示范 (1): ${data}`);
},
error: error => {
console.log(`retryWhen 示范 (1): 错误 - ${error}`);
},
complete: () => {
console.log('retryWhen 示范 (1): 完成');
}
});
// retryWhen 示范 (1): 1
// retryWhen 示范 (1): 1
// retryWhen 示范 (1): 1
// (重试的 Observable 完成,因此整个 Observable 也完成)
// retryWhen 示范 (1): 完成
弹珠图:
-1-#
retryWhen(---0---1---2|)
-1----1----1----1|
^ 发生错误,三秒後重试
^ 重试的 Observable 完成,因此整个 Observable 也完成
由於是让重试的 Observable 完成,因此整个资料流也会当作「完成」,处理订阅的 complete()
callback。
如果希望重试几次次後发生错误,一样加入 throwError
即可:
const retryTimesThenThrowError = (every, times) => interval(every).pipe(
switchMap((value, index) =>
iif(() => index === times, throwError('重试後发生错误'), of(value)))
);
interval(1000)
.pipe(
switchMap(data =>
iif(() => data % 2 === 0, of(data), throwError('发生错误'))),
map(data => data + 1),
retryWhen((error) => retryTimesThenThrowError(3000, 3))
)
.subscribe({
next: data => {
console.log(`retryWhen 示范 (2): ${data}`);
},
error: error => {
console.log(`retryWhen 示范 (2): 错误 - ${error}`);
},
complete: () => {
console.log('retryWhen 示范 (2): 完成');
}
});
// retryWhen 示范 (2): 1
// retryWhen 示范 (2): 1
// retryWhen 示范 (2): 1
// retryWhen 示范 (2): 1
// retryWhen 示范 (2): 错误 - 重试後发生错误
另外一个小技巧,我们也可以让使用者自己决定何时要重试:
const click$ = fromEvent(document, 'click');
interval(1000)
.pipe(
switchMap(data =>
iif(() => data % 2 === 0, of(data), throwError('发生错误'))),
map(data => data + 1),
retryWhen((error) => click$)
)
.subscribe({
next: data => {
console.log(`retryWhen 示范 (3): ${data}`);
},
error: error => {
console.log(`retryWhen 示范 (3): 错误 - ${error}`);
},
complete: () => {
console.log('retryWhen 示范 (3): 结束');
}
});
以上程序码能在错误发生後,当滑鼠 click 事件发生时,才进行重试。
程序码:https://stackblitz.com/edit/mastering-rxjs-operator-retrywhen
finalize
会在整个来源 Observable 结束时,才进入处理,因此永远会在最後才呼叫到:
interval(1000)
.pipe(
take(5),
finalize(() => {
console.log('finalize 示范 (1): 在 pipe 内的 finalize 被呼叫了')
}),
map(data => data + 1),
)
.subscribe({
next: data => {
console.log(`finalize 示范 (1): ${data}`);
},
complete: () => {
console.log(`finalize 示范 (1): 完成`);
}
});
// finalize 示范 (1): 1
// finalize 示范 (1): 2
// finalize 示范 (1): 3
// finalize 示范 (1): 4
// finalize 示范 (1): 5
// finalize 示范 (1): 完成
// finalize 示范 (1): 在 pipe 内的 finalize 被呼叫了
从结果可以看到,尽管 map
放在 finalize
後面,但还是不断的处理 map
内的逻辑,直到来源 Observable 结束後,才进入 finalize
处理,同时也可以注意到 finalize
会比 subsribe
的 complete
还慢进入。
严格来说 finalize
不算是错误处理的 operator,因为 finalize
会在整个 Observable 结束时才进入处理,跟有没有发生错误无关,但经常与错误处理搭配一起使用。
interval(1000)
.pipe(
switchMap(data =>
iif(() => data % 2 === 0, of(data), throwError('发生错误'))),
// 当之前的 operator 发生错误时,资料流会中断,但会进来 finalize
finalize(() => {
console.log('finalize 示范 (2): 在 pipe 内的 finalize 被呼叫了')
}),
// 当之前的 operator 发生错误时,这里就不会呼叫了
map(data => data + 1),
)
.subscribe({
next: data => {
console.log(`finalize 示范 (2): ${data}`);
},
error: error => {
console.log(`finalize 示范 (2): 错误 - ${error}`);
}
});
// finalize 示范 (2): 1
// finalize 示范 (2): 错误 - 发生错误
// finalize 示范 (2): 在 pipe 内的 finalize 被呼叫了
从结果可以看到,finalize
也会比 subsribe
的 error
还慢被呼叫。透过 finalize
我们可以确保就算过程中发生错误导致整个资料流中断,还会有个地方可以处理些事情。
程序码:https://stackblitz.com/edit/mastering-rxjs-operator-finalize
catchError
:可以用来决定当来源 Observable 发生错误时该如何进行,回传一个 Observable 代表会使用此 Observable 继续下去,因此回传 throwError
则代表依然发生错误。retry
:当来源 Observable 发生错误时,重新尝试指定次数。retryWhen
:当来源 Observable 发生错误时,可以照自定的 Observable 来决定重试的时机。finalize
:在 Observable 结束时,无论是 error()
还是 complete()
,最後都可以进入 finalize
进行最终处理。
<<: Day 32:来呼叫星战 Profile List 下一页吧(1/2)
>>: 第 29 型 - 单元测试 (Unit Testing)
前言 今天要解的题目是top 100 liked里面的763. Partition Labels这题...
slate 将 typescript 的型别扩充相关的内容都集合在 interfaces/cust...
时间来到今年四月,Dennis 学长跑来问我有没有兴趣打 MyFirst CTF 以及 AIS3 P...
一般在使用资料库新增资料的时候,都会看到新建资料的日期跟时间,今天会再sqllite上加入日期。 我...
延续上篇还没介绍完的Arm Mali GPU系列解决方案,今天要来接着介绍Mali-G510 GPU...