RxJS中高階操作符的全面講解:switchMap,mergeMap,concatMap,exhaustMap


RxJS中高階映射操作符的全面講解:switchMap, mergeMap, concatMap (and exhaustMap)

原文鏈接:https://blog.angular-university.io/rxjs-higher-order-mapping/

有一些在日常開發中常用的RxJS的操作符是高階操作符:switchMap,mergeMap,concatMap,以及exhaustMap。

舉個例子,程序中大多數的網絡請求都是通過以上某個操作符來完成的,所以為了能夠寫出幾乎所有反應式編程,必須熟悉這些操作符的運用。

在給定的場景中,知道用哪個操作符以及為什么要用那個操作符,有時候會讓我們覺得有些迷惑。我們經常很想搞清楚這些操作符是如何運作的,還有為什么它們要叫那個名字。

這些操作符可能看起來互不相干,不過我們確實需要把它們放在一起學習。因為如何選擇了錯誤的操作符,很可能給我們的程序帶來致命的問題。

為什么mapping操作符會讓我們感到有些迷惑?

有個原因:為了理解這些操作符,我們首先要理解每個操作符的內部是如何使用Observable的組合策略的。

暫且先不討論switchMap本身,我們先搞懂什么是Observable switching;暫且不深究concatMap,我們先學習一下Observable concatenation,等。

這就是我們將在本文中要做的事情。我們將按照邏輯順序來學習concat,merge,switch還有exhaust的策略以及各自對應的mapping操作符:concatMap,mergeMap,switchMap以及exhaustMap。

我們將使用彈珠圖(marble diagram)結合實例代碼來解釋基本概念。

最后,你會很清楚地知道每個操作符是如何運作的、什么時候使用哪個操作符、為什么要使用那個,以及各個操作符名稱的緣由。

內容一覽

本文內容,我們將包含如下主題:

  • RxJS Map 操作符
  • 什么是高階(higher-order)Observable Mapping
  • Observable concatenation(連接)
  • RxJS concatMap 操作符
  • Observable merging(合並)
  • RxJS mergeMap 操作符
  • Observable switching(切換)
  • RxJS switchMap 操作符
  • Exhasut strategy 耗盡策略
  • RxJs exhaustMap 操作符
  • 如何選對mapping操作符?
  • 運行Github repo(附代碼范例)
  • 結語

此段原文未翻譯:

Note that this post is part of our ongoing RxJs Series. So without further ado, let's get started with our RxJs mapping operators deep dive!

RxJS Map操作符

一開始,我們先了解一下這些操作符通常在做些什么。

正如操作符的名字所述,它們在做某種映射。不過,到底是什么被映射了呢?我們先看下RxJS映射操作符的彈珠圖:

map操作符

基礎的map操作符是如何工作的

利用map操作符,我們可以傳入一個輸入流(值1,2,3),然后從輸入流,再創建一個派生出來的映射輸出流(值10,20,30)。

底部輸出流的值是通過拿到輸入流的值並應用了一個函數而獲得的。該函數就是簡單地把數值乘以10。

如此,map操作符都是用來映射輸入Observable的值的。這里有個例子,關於我們如何使用map操作符來處理一個HTTP請求:

const http$ : Observable<Course[]> = this.http.get('/api/courses');

http$
    .pipe(
        tap(() => console.log('HTTP request executed')),
        map(res => Object.values(res['payload']))
    )
    .subscribe(
        courses => console.log("courses", courses)
    );

01.ts

此例中,我們創建一個HTTP observable,發出一個向后端的請求,並且訂閱該observable。這個Observable會發出來自后端的HTTP響應,該響應是一個JSON對象。

在此例中,這個HTTP響應在屬性payload中包裹着數據,所以為了獲得數據,我們運用RxJS的map操作符。映射函數將會映射JSON響應的payload,並且提取出屬性payload對應的值。

現在我們已經回顧了基本的映射是如何工作的,讓我們來談談更高階映射(high-order mapping)。

什么是更高階Observable映射?

在更高階映射中,我們不會簡單地映射一個值比如說1到另外一個值比如說10,我們將把值映射進另外一個Observable中!

結果是一個高階Observable。它和其他可觀察對象一樣,只是它的值本身也是可觀察對象,我們可以單獨訂閱。

這可能聽起來有些牽強,但事實上,這種類型的mapping一直都在發生。我們來給一個關於這種類型的mapping的實例。我們舉例來說,我們有一個Angular反應式表單,它會通過一個Observable一直發送合法的表單值:

@Component({
    selector: 'course-dialog',
    templateUrl: './course-dialog.component.html'
})
export class CourseDialogComponent implements AfterViewInit {

    form: FormGroup;
    course:Course;

    @ViewChild('saveButton') saveButton: ElementRef;

    constructor(
        private fb: FormBuilder,
        private dialogRef: MatDialogRef<CourseDialogComponent>,
        @Inject(MAT_DIALOG_DATA) course:Course) {

        this.course = course;

        this.form = fb.group({
            description: [course.description, 
                          Validators.required],
            category: [course.category, Validators.required],
            releasedAt: [moment(), Validators.required],
            longDescription: [course.longDescription,
                              Validators.required]
        });
    }
}

12.ts

這個反應式表單提供一個可觀察對象this.form.valueChanges,它在用戶跟表單進行交互的時候發送最新的表單值。這個會作為我們的可觀察對象源(source Observable)。

我們要做的是保存至少一些一直被發出的值,以來實現一個預存表單草稿的特性。這樣一來,隨着用戶填寫表單的時候,數據就被漸進式的保存起來。如此可以避免因為意外的頁面重載導致整個表單數據的丟失。

為什么要用更高階可觀察對象?

為了實現保存表單草稿的功能,我們需要拿到表單值,然后創建第二個HTTP observable,該可觀察對象會做后端保存,接着訂閱它。

我們本可以手動實現這些,不過那樣我們就會掉進嵌套訂閱的反面模式中:

this.form.valueChanges
    .subscribe(
       formValue => {
      
           const httpPost$ = 
                 this.http.put(`/api/course/${courseId}`, formValue);

           httpPost$.subscribe(
               res => ... handle successful save ...
               err => ... handle save error ...
           );

       }        
    );

02.ts

如我們所見,這很快會造成我們的代碼行成多級嵌套,這個就是我們在第一個地方使用RxJS來避免的問題。

讓我們稱呼新的httpPost$可觀察對象為內部可觀察對象,因為它是由內部嵌套代碼塊生成的。

避免嵌套訂閱

我們願意用一種更簡便的方法去做所有的過程:拿到表單值,然后映射到用於保存的Observable。這會有效地創建一個高階可觀察對象,它的每個值都對應一個保存請求。

接着我們顯然會訂閱每個網絡請求Observable,然后能直接一步到位獲取所有網絡請求的響應,這樣會避免任何嵌套。

如果我們有某種更高階的RxJS映射操作符,我們就可以做到這一切!我們為什么需要四種不同的操作符呢?

為了理解這個,試想如果有多個表單值一連串快速地從valueChanges observable發送出來,並且保存操作需要耗費一些時間來完成:

  • 我們應該等待一個保存請求完成后再進行另外一個保存操作嗎?
  • 我們應該以並行方式做多個保存操作嗎?
  • 我們應該取消某個正在進行的保存操作然后開始一個新的嗎?
  • 我們應該在某個操作還在進行中時取消新的嘗試保存的操作嗎?

在探討以上這些用例之前,讓我們回顧一下上面的嵌套訂閱代碼。

在嵌套訂閱范例中,我們實際上以並行方式觸發着保存操作,這並不是我們所想要的,因為無法強力保證后端會按照順序處理保存請求,而且最后一個有效的表單值確實是存儲在后端的。

讓我們看下如何確保只在上一次保存完成后才會執行保存請求。

理解可觀察對象連接(Observable Concatenation)

為了實現按順序保存,我們將引入一個新的概念:可觀察對象連接。在這個代碼范例中,我們使用RxJS函數concat()連接兩個可觀察對象范例:

const series1$ = of('a', 'b');

const series2$ = of('x', 'y');

const result$ = concat(series1$, series2$);

result$.subscribe(console.log);

03.ts

在使用of函數創建了兩個可觀察對象series1$series2$后,我們又創建了第三個可觀察對象result$,它是連接series1$series2$的結果。

這里是該程序的控制台輸出,表示結果可觀察對象發出的值:

a
b
x
y

我們能看到,這些值是series1$series2$的值一起連接后的結果。但問題就在這里:只有當這些可觀察對象正在完成時才會生效。

函數of()將創建一些可觀察對象,這些可觀察對象會把傳入到of()的值發送出去,當所有的值都被發出后將完成這些可觀察對象。

可觀察對象連接彈珠圖

為了真的理解發生了什么,我們需要看一下可觀察對象的彈珠圖:

可觀察對象連接彈珠圖

你有沒有注意到第一個可觀察對象的值b右邊的豎線標記?它標記着帶有值a和b的第一個可觀察對象(series1$)完成的時間點。

沿着時間線,讓我們一步一步地把過程分解,看看發生了什么:

  • 兩個可觀察對象series1$series2$被傳給了函數concat()
  • concat()會訂閱第一個可觀察對象series1$,但不會訂閱第二個可觀察對象series2$(這對理解連接是至關重要的)
  • source1$發送值a,它會立即反應到可觀察對象result$的輸出中
  • 注意source2$還未發送值,因為它還沒有被訂閱
  • source1$接着發送值b,它被反應到輸出中
  • source1$然后就完成了,只有當它完成了,concat()才會訂閱source2$
  • source2$的值將開始反應到輸出,直至完成
  • source2$完成后,result$也將完成
  • 注意只要想要,我們可以傳入很多可觀察對象到concat(),不僅僅像此例中的兩個

可觀察對象連接的關鍵點

正如我們所見,可觀察對象連接就是關於可觀察對象的完成!我們取第一個可觀察對象並使用它的值,等待它完成,然后我們使用下一次可觀察對象···,直到所有可觀察對象完成。

返回到我們的更高階可觀察對象映射的范例,讓我們看看連接的概念是如何幫助我們的。

使用可觀察對象連接來實現順序保存

正如我們已經看到的,為了確保我們的表單值被按順序保存,我們需要取得每個表單值,然后映射到一個httpPost$可觀察對象。

接着我們需要訂閱它,不過我們想要在訂閱下一個httpPost$可觀察對象之前完成保存。

為了確保順序,我們需要將多個httpPost$可觀察對象連接到一起!

我們將訂閱每個httpPost$,然后按順序處理每個請求的結果。在最后,我們需要的是一個操作符,它將混合以下內容:

  • 一個更高階映射操作(取得表單值然后把它變成一個httpPost$可觀察對象)
  • 使用concat()操作符,連接多個httpPost$可觀察對象,以確保在前一個進行中的保存操作完成之前,不會創建新的HTTP保存請求

我們需要的就是恰當命名的RxJS操作符:concatMap,它通過可觀察對象連接來做更高階映射的混合操作。

RxJS concatMap 操作符

下面是如果我們使用concatMap操作符時的代碼:

this.form.valueChanges
    .pipe(
        concatMap(formValue => this.http.put(`/api/course/${courseId}`, 
                                             formValue))
    )
    .subscribe(
       saveResult =>  ... handle successful save ...,
        err => ... handle save error ...      
    );

04.ts

我們能看到,使用更高階映射操作符的第一個好處就是我們不會再有嵌套訂閱了。

通過使用concatMap,現在所有的表單值都按順序被發送到后端,在Chrome DevTools Network中顯示如下:

concatMap()

拆分concatMap的網絡日志圖

能看到,一個HTTP保存請求僅在前一個保存完成后才開始。下面是concatMap操作符如何確保請求總是按順序發生:

  • concatMap取得每個表單值,轉換到一個保存HTTP可觀察對象,這個可觀察對象成為內部可觀察對象
  • concatMap訂閱這個內部可觀察對象,發送它的輸出到結果可觀察對象
  • 第二個表單值可能比后端保存前一個表單值要來的快
  • 如果發生那個情況,新的表單值將不會立即映射到HTTP請求中
  • 相反,concatMap在映射新的值到HTTP可觀察對象之前會等待前一個HTTP可觀察對象完成,然后訂閱它,從而觸發下一個保存

注意這里的代碼僅僅是保存表單草稿的基本實現。你可以跟其他的操作符結合使用,比如僅保存合法的表單值,節流保存,以確保它們不會發生得太頻繁。

可觀察對象合並(Observable Merging)

應用可觀察對象連接到一系列HTTP保存操作,看起來似乎是一個不錯的方法,可以確保保存操作按照想要的順序發生。

但是有其他場景,相反我們想要並行執行操作,不等待前一個內部可觀察對象完成。

要完成這個,我們有合並可觀察對象結合策略!merge,不像concat,在訂閱下一個可觀察對象之前不會等待任何可觀察對象。

相反,merge同時訂閱每個被合並的可觀察對象,當多個值到達時,它將把每個源可觀察對象的值輸出到結果可觀察對象中。

實用的Merge范例

為了搞清楚合並(merging)不依賴完成,讓我們合並兩個永不完成的可觀察對象,因為它們是間隔可觀察對象(interval Observable):

const series1$ = interval(1000).pipe(map(val => val*10));

const series2$ = interval(1000).pipe(map(val => val*100));

const result$ = merge(series1$, series2$);

result$.subscribe(console.log);

05.ts

interval()創建的可觀察對象將會在一秒鍾間隔內發送值0,1,2···,它永遠不會完成。

注意我們應用了幾個map操作符到這些interval Observable中,僅僅為了在控制台更方便地區分它們。

下面是在控制台中能看到的最開始的一些值:

0
0
10
100
20
200
30
300

合並與可觀察對象完成

我們可以看到,合並后的源可觀察對象的值只要被發出,就會立即顯示在結果可觀察對象中。如果其中一個合並后的可觀察對象完成了,merge會繼續持續發出其他可觀察對象的值。

注意如果源可觀察對象完成了,merge仍然會以相同方式工作。

Merge彈珠圖

merge marble disgram

我們可以看到,合並后的源可觀察對象的值會立即顯示在輸出中。結果可觀察對象在所有的合並后的可觀察對象完成之后才會完成。

現在我們理解了合並策略,讓我們看看它在更高階可觀察對象的映射上下文中將被如何使用。

RxJS mergeMap 操作符

如果我們把合並策略與更高階可觀察對象映射的概念相結合,我們就擁有了RxJS mergeMap 操作符。讓我們來看看這個操作符的彈珠圖:

mergeMap marble diagram

下面是mergeMap操作符是如何工作的:

  • 每個源可觀察對象的值仍然會被映射到一個內部可觀察對象中,跟concatMap的情況一樣
  • 像concatMap一樣,內部可觀察對象也會通過mergeMap被訂閱
  • 內部Observable發出新的值,它們立即反應到輸出Observable中
  • 但不像concatMap,在mergeMap的情況下,我們不必等待前一個內部Observable完成再去觸發下一個內部Observable。
  • 這意味着有了mergeMap(不像concatMap),我們可以有多個內部Observable隨着時間重疊,像我們在上圖紅色高亮處看到的那樣,並行地發出值。

查看mergeMap網絡日志

返回到我們的前一個表單草稿保存范例,很明顯之所以我們需要concatMap而不是mergeMap,是因為我們不希望保存是並行發生的。

讓我們看看如何意外地選擇使用mergeMap的話,會發生什么:

this.form.valueChanges
    .pipe(
        mergeMap(formValue => 
                 this.http.put(`/api/course/${courseId}`, 
                               formValue))
    )
    .subscribe(
       saveResult =>  ... handle successful save ...,
        err => ... handle save error ...      
    );

06.ts

現在,假設用戶在跟表單交互,並開始非常快地輸入數據。在這種情況下,在網路日志中,我們會看到多個保存請求在並行運行着。

我們可以看到,請求在並行地發生着,在這種情況下是一種錯誤!在重載情況下,有可能這些請求會亂序進行。

可觀察對象切換(Observable Switching)

讓我們來談談另外一個Observable合並機制:switching。從我們不等待任何Observable完成的方面來說,相對於連接,切換的概念更接近於合並。

但是switching跟merging不同,如果一個新的Observable開始發出值的話,在訂閱新的Observable之前,我們會取消訂閱前一個Observable。

Observable switching是為了確保未使用的Observable的取消訂閱邏輯被觸發,這樣資源可以得到釋放!

Switch彈珠圖

讓我們看看switch的彈珠圖:

switch marble diagram

注意那些斜線,它們並非意外!在switch策略的情況下,重要的是要在圖表中表示更高階的Observable,也就是圖片中最上面一行。

更高階的Observable發送本身就是Observable的值。

當一條對角線從高階可觀察對象的頂部那條線分叉的時候,就是一個值被switch發出並訂閱的時候。

拆分switch彈珠圖

下面是在彈珠圖中發生了什么:

  • 高階Observable發出它的第一個內部Observable(a-b-c-d),內部Observable被訂閱(通過switch策略的實現)
  • 第一個內部Observable(a-b-c-d)發出值a和b,立即被反應到輸出中
  • 但是緊接着第二個內部Observable(e-f-g)被發出。它會觸發第一個內部Observable(a-b-c-d)的取消訂閱,這是switching的關鍵部分
  • 第二個內部Observable(e-f-g)開始發出新的值,新的值被反應到輸出中
  • 不過要注意,第一個內部Observable(a-b-c-d)同時仍然在發出新的值c和d
  • 這些后來的值,然而並沒有反應在輸出中。這是因為同時我們取消訂閱了第一個內部Observable(a-b-c-d)

現在我們能明白,為什么圖表不得不用這樣一種不尋常的方式,以對角線來畫了:這是因為我們需要直觀地表示出每個內部Observable何時被訂閱(或被取消訂閱),這發生在對角線Observable從源Observable分叉的地方。

RxJS switchMap 操作符

讓我們接着把switch策略應用到高階映射中。假設我們有一個普通的輸入流,它會發出值1,3和5。

我們將把每個值映射到一個Observable,就像我們在concatMap和mergeMap的例子中做的那樣,並獲得一個更高階的可觀察對象。

如果我們現在在被發出的內部Observable中切換,而不是連接或合並它們,我們最終會使用switchMap操作符:

拆分switchMap彈珠圖

下面是這個操作符是如何工作的:

  • 源Observable發出值1,3和5
  • 通過一個映射函數,這些值被轉換為內部Observable
  • 通過switchMap這些內部Observable被訂閱
  • 當內部Observable發出一個值,這個值立即被反應到輸出中
  • 不過如果一個新值比如5,在前一個Observable有機會完成之前就被發出,前一個內部Observable(30-30-30)將被取消訂閱,它的值將不再反應到輸出中
  • 注意上面圖中紅色的30-30-30內部Observable:最后一個值30沒有被發出,因為30-30-30內部Observable被取消訂閱了

所以我們可以看到,Observable切換就是確保我們觸發未使用Observable的取消訂閱邏輯。讓我們來看switchMap的實戰!

Search TypeAhead - switchMap操作符范例

關於switchMap有一個很常見的例子:search TypeAhead(待翻譯)。首先讓我們定義源Observable,它的值本身會觸發檢索請求。

這個源Observable會發出值,這些值是用戶在輸入框中鍵入的文本:

const searchText$: Observable<string> = 
      fromEvent<any>(this.input.nativeElement, 'keyup')
    .pipe(
        map(event => event.target.value),
        startWith('')
    )
    .subscribe(console.log);

07.ts

這個源Observable被關聯到一個輸入文本字段,用戶在它上面鍵入搜索內容。當用戶鍵入單詞“Hello World”作為檢索,下面是被searchText$發出的值:

H
H
He
Hel
Hell
Hello
Hello 
Hello W
Hello W
Hello Wo
Hello Wor
Hello Worl
Hello World

防抖並從Typeahead中刪除重復的內容

注意那些重復的值,不管是由用戶在兩個單詞之間使用空格引起的,還是因為使用Shift鍵讓字母H和W大寫。

為了防止向后端發送這些所有的值作為檢索請求,讓我們使用debounceTime操作符來等待用戶輸入直至穩定。

const searchText$: Observable<string> = 
      fromEvent<any>(this.input.nativeElement, 'keyup')
    .pipe(
        map(event => event.target.value),
        startWith(''),
        debounceTime(400)
    )
    .subscribe(console.log);

08.ts

通過使用該操作符,如果用戶以正常速度鍵入,我們只有searchText$的一個值在輸出中。

Hello World

跟之前相比,現在已經好很多了。現在只有穩定了至少400ms的值才會被發出!

但是,如果用戶一邊在思考檢索內容,一邊很慢地進行輸入的。就是說在兩個值之間會超過400ms,那么檢索流會看起來像下面這樣:

He
Hell
Hello World

而且,用戶可能輸入一個值,按退格鍵(backspace),然后又輸入一遍,這會造成重復的檢索值。我們可以通過添加distinctUntilChanged操作符來阻止重復檢索的情形發生。

取消Typeahead中過期的檢索

但不止如此,我們需要一種方法,當新的檢索開始后,取消之前的檢索。

在這里我們需要做的是轉換每個檢索字符串到后端檢索請求並訂閱它。在兩個連續的檢索請求中應用switch策略,當新的檢索被觸發后,前面的檢索將被取消。

這正是switchMap操作符將要做的事情!下面是我們的Typeahead邏輯用到的最終實現:

const searchText$: Observable<string> = 
      fromEvent<any>(this.input.nativeElement, 'keyup')
    .pipe(
        map(event => event.target.value),
        startWith(''),
        debounceTime(400),
        distinctUntilChanged()
    ); 

const lessons$: Observable<Lesson[]> = searchText$
    .pipe(
        switchMap(search => this.loadLessons(search))        
    )
    .subscribe();

function loadLessons(search:string): Observable<Lesson[]> {
    
    const params = new HttpParams().set('search', search);
   
    return this.http.get(`/api/lessons/${coursesId}`, {params});
}

09.ts

Typeahead的switchMap樣例

讓我們來看下switchMap操作符的實踐!當用戶在檢索欄中鍵入內容,然后稍稍猶豫了下,接着鍵入其他內容,下面是在網絡日志中大體上可以看到的內容:

switchMap Demo with a Typeahead

我們可以看到,有幾個前面的檢索在它們進行的地方已經被取消了。這太棒了!這可以節省服務器資源用於做其他的事情。

Exhaust策略

在typeahead的場景中,switchMap操作符是最合適的。不過也有一些其他的情況:我們要做的是在前面的值沒有完成處理之前,忽略源Observable中所有新的值。

舉個例子,假如我們在點擊保存按鈕的時候,觸發向后端的保存請求。為了確保保存操作按順序發生,我們可能首先使用concatMap操作符來實現它。

fromEvent(this.saveButton.nativeElement, 'click')
    .pipe(
        concatMap(() => this.saveCourse(this.form.value))
    )
    .subscribe();

10.ts

這個確保了保存操作按順序處理。但如果用戶多次點擊了保存按鈕,會發生什么呢?下面是我們在網絡日志中會看到的內容:

我們能看到,每個點擊觸發了各自的保存:假如我們點擊20次,我們會有20次保存操作!在這個例子中,我們需要多做一些事情,而不僅僅讓保存操作按順序發生。

我們也想要能夠取消點擊,但僅在某個保存操作已經在進行的時候。exhaust可觀察對象結合策略將允許我們實現這個。

Exhaust彈珠圖

為了理解exhaust是如何工作的,讓我們看一下這張彈珠圖:

exhaust marble diagram

就像之前一樣,這里在首行中我們有一個高階Observable,它的值本身是Observable,從那個首行中分叉開來。下面是在這張彈珠圖中發生的內容:

  • 就像switch例子一樣,exhaust訂閱了第一個內部Observable(a-b-c)
  • 正常地,值a,b和c立即被反應到輸出中
  • 然后第二個內部Observable(d-e-f)被發出了,第一個Observable(a-b-c)還在進行中
  • 第二個Observable通過exhaust策略被清除了,它將不會被訂閱(這是exhaust的關鍵部分)
  • 僅當第一個Observable(a-b-c)完成后,exhaust策略才會訂閱新的Observable
  • 當第三個Observable(g-h-i)被發出的時候,第一個Observable(a-b-c)已經完成了,所以第三個Observable不會被清除,並且會被訂閱
  • 第三個Observable的值g-h-i會出現在結果Observable的輸出中,不像值d-e-f那樣沒有出現在輸出中

就像concat,merge還有switch,現在我們可以應用exhaust策略到高階映射的上下文中。

RxJS exhaustMap 操作符

現在讓我們來看下exhaustMap的彈珠圖。記住,跟上一個彈珠圖中的首行不一樣,源Observable1-3-5發出的值不是Observable。

相反,這些值可能是比如鼠標點擊:

那么下面是exhaustMap彈珠圖中發生的內容:

  • 值1被發出,內部Observable 10-10-10被創建
  • 源Observable中的值3被發出之前,Observable 10-10-10發出了所有的值並完成了,所以全部10-10-10值被發出到輸出中
  • 在輸入中新的值3被發出了,觸發了新的內部Observable 30-30-30
  • 但現在,當30-30-30還在運行的時候,我們有一個值5從源Observable中發出了
  • 這個值5通過exhaust策略被清除了,意味着50-50-50 Observable將永遠不會被創建,所以值50-50-50永遠不會顯示在輸出中

exhastMap的實例

現在讓我們把exhaustMap操作符應用到我們的保存按鈕場景中:

fromEvent(this.saveButton.nativeElement, 'click')
    .pipe(
        exhaustMap(() => this.saveCourse(this.form.value))
    )
    .subscribe();

11.ts

如果現在我們一次性點擊5次保存按鈕,我們將得到以下網絡日志:

exhaustMap

可以看到,正如所料,某個保存請求還在進行中時產生的點擊都被忽略了!

注意如果我們一次性持續點擊比如20次,最終進行中的保存請求會結束,然后第二個請求將開始。

如何選擇正確的映射操作符?

從高階映射操作符的角度來說,concatMap,mergeMap,switchMap和exhaustMap的行為是相似的。

但它們在許多微妙的地方也是如此不同,以至於沒有一種操作符可以安全地作為默認選擇。

相反,我們可以基於使用場景簡單地選擇恰當的操作符:

  • 如果我們需要在等待完成的時候按順序做事,那么concatMap是正確的選擇
  • 為了並行處理事情,mergeMap是最佳選擇
  • 如果需要取消的邏輯,就選switchMap
  • 當前操作還在進行時,要忽略新的Observable,exhaustMap就是干這個的

運行Github repo(附代碼示例)

如果你想嘗試運行本文中的例子,這是包含了本文中運行代碼的演練場

這個代碼庫包含了一個小的HTTP后端請求,它會幫助我們在更實際的場景中嘗試RxJS映射操作符,它還包含了運行代碼,像預保存草稿表單,a typeahead(未翻譯),用Reactive風格寫的關於組建的主題和樣例:

RxJs In Practice Course

結語

我們已經看到,在反應式編程比如網絡請求中做一些很常見的操作,使用RxJS高階映射操作符是必不可少的。

為了真正理解這些操作符還有它們的名稱,我們首先需要集中理解Observable的底層結合策略concat,merge,switch和exhaust。

我們也要認識到,有一個更高階映射操作在發生,它們的值被轉換成獨立的Observable,這些Observable被映射操作符本身以一種隱藏的方式訂閱。

選擇正確的操作符就是關於選擇正確的內部Observable組合策略。選擇錯誤的操作符通常不會馬上造成程序崩潰,但隨着時間的推移,它可能導致一些難以排除的問題。

我希望你喜歡這篇文章!如果關於RxJS,你願意學習更多,我們推薦看下RxJS實戰課程,這些課程更詳細地介紹了很多有用的模式和操作符。

同樣,如果你們有任何問題或評論,請在下方留言,我會回復你們的。

如果你正開始學習Angular,看一下Angular入門教程

-- The End --


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM