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 操作符對重試操作進行自定義,這部分內容很重要,需要進行掌握。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章