RxJs的switchMap,mergeMap,concatMap, and exhaustMap

大家还是看原文吧:RxJs Mapping: switchMap vs mergeMap vs concatMap vs exhaustMap (angular-university.io)

以下的中文部分也只是机器翻译而已^_^

 

文章太长,先说结论:

concatMap:依次订阅每个Observable,依次发出第一个Observable,第二个Observable等等等的每个值,使用场景:必须等上一次保存成功后才允许执行下一次保存;

mergeMap:同时订阅所有Observable,任何Observable发出值了就推给最后的订阅者执行操作,使用场景:对保存顺序没有要求;

switchMap:先订阅第一个Observable,只要后面的Observable开始发出值了,就取消订阅上一个Observable,同时订阅后面这个Observable,并发出的值给后面的订阅者执行操作,使用场景:typeahead;

exhaustMap:先订阅第一个Observable,如果后面的Observable在它还没有完成之前开始发出值,那么后面这个Observable整个会被忽略,不会被订阅;如果后面的Observable开始发出值时,前一个Observable已经完成,那么后面这个Observable就会被订阅并且得到执行。

注意switchMap和exhaustMap的区别,一个是忽略前面的Observable,一个是忽略后面的Observable。

 

我们日常发现的一些最常用的 RxJs 操作符是 RxJs 高阶映射操作符:switchMap、mergeMap、concatMap 和exhaustMap。

例如,我们程序中的大部分网络调用都将使用这些运算符之一完成,因此熟悉它们对于编写几乎所有反应式程序至关重要。

知道在给定情况下使用哪个运算符(以及为什么)可能有点令人困惑,我们经常想知道这些运算符是如何真正工作的,以及为什么它们会这样命名。

这些运算符可能看起来不相关,但我们真的很想一口气学习它们,因为选择错误的运算符可能会意外地导致我们程序中的微妙问题。

为什么映射运算符有点混乱?

这样做是有原因的:为了理解这些操作符,我们首先需要了解每个内部使用的 Observable 组合策略。

与其试图自己理解switchMap,不如先了解什么是Observable切换; 我们需要先学习 Observable 连接等,而不是直接深入 concatMap。

这就是我们在这篇文章中要做的事情,我们将按逻辑顺序学习 concat、merge、switch 和exhaust 策略及其对应的映射运算符:concatMap、mergeMap、switchMap 和exhaustMap。

我们将结合使用弹珠图和一些实际示例(包括运行代码)来解释这些概念。

最后,您将确切地知道这些映射运算符中的每一个是如何工作的,何时使用,为什么使用,以及它们名称的原因。

 

 

 

 

Table of Contents

In this post, we will cover the following topics:

  • The RxJs Map Operator
  • What is higher-order Observable Mapping
  • Observable Concatenation
  • The RxJs concatMap Operator
  • Observable Merging
  • The RxJs mergeMap Operator
  • Observable Switching
  • The RxJs switchMap Operator
  • The Exhaust strategy
  • The RxJs exhaustMap Operator
  • How to choose the right mapping Operator?
  • Running GitHub repo (with code samples)
  • Conclusions

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!

基本 Map Operator 的工作原理

让我们从头开始,介绍这些映射运算符的一般作用。

正如运算符的名称所暗示的那样,他们正在做某种映射:但究竟是什么被映射了? 我们先来看看 RxJs Map 操作符的弹珠图:

RxJs Map Operator marble diagram

基本 Map Operator 的工作原理

使用 map 运算符,我们可以获取输入流(值为 1、2、3),并从中创建派生的映射输出流(值为 10、20、30)。

底部输出流的值是通过获取输入流的值并将它们应用到一个函数来获得的:这个函数只是将这些值乘以 10。

所以 map 操作符就是映射输入 observable 的值。 以下是我们如何使用它来处理 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)
    );

 

 

在这个例子中,我们正在创建一个 HTTP observable 来进行后端调用,我们正在订阅它。 observable 将发出后端 HTTP 响应的值,这是一个 JSON 对象。

在这种情况下,HTTP 响应将数据包装在有效负载属性中,因此为了获取数据,我们应用了 RxJs 映射运算符。 然后映射函数将映射 JSON 响应负载并提取负载属性的值。

既然我们已经回顾了基本映射的工作原理,现在让我们来谈谈高阶映射。

什么是高阶可观察映射?

在高阶映射中,我们不是将像 1 这样的普通值映射到另一个像 10 这样的值,而是将一个值映射到一个 Observable 中!

结果是一个高阶的 Observable。 它和其他任何一个 Observable 一样只是一个 Observable,但它的值本身也是 Observables,我们可以单独订阅。

这听起来可能有些牵强,但实际上,这种类型的映射一直在发生。 让我们举一个这种类型映射的实际例子。 比方说,例如,我们有一个 Angular Reactive Form,它通过 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]
        });
    }
}

 

 

Reactive Form 提供了一个 Observable this.form.valueChanges,它在用户与表单交互时发出最新的表单值。这将是我们的源 Observable。

我们想要做的是在这些值随着时间的推移发出时至少保存其中一些值,以实现表单草稿预保存功能。这样,随着用户填写表单,数据会逐渐保存,从而避免由于意外重新加载而丢失整个表单数据。为什么使用Higher-Order Observables?

为了实现表单草稿保存的功能,我们需要获取表单值,然后创建第二个HTTP observable,来进行后台保存,然后订阅这个observerable。

我们当然可以完全手动完成这些,但是我们后掉进nested subscribes anti-pattern(嵌套订阅反模式,也就是订阅里嵌套订阅,代码会难以阅读和理解):

   
  this.form.valueChanges
  .subscribe(
  formValue => {
   
  const httpPost$ =
  this.http.put(`/api/course/${courseId}`, formValue);
   
  httpPost$.subscribe(
  res => ... handle successful save ...
  err => ... handle save error ...
  );
   
  }
  );
view raw02.ts hosted with ❤ by GitHub

如你所见,这样会让我们的代码迅速的形成了多层级的嵌套,然鹅这其实正是我们一开始使用RxJs时就想避免的一个问题。

Let's call this new httpPost$ Observable the inner Observable, as it was created in an inner nested code block.

避免嵌套订阅

我们希望以一种更方便的方式完成所有这些过程:我们想拿到form表单的值,然后map到一个save Observable. 而这将高效的创建一个高阶Obserbale,其中每一个值对应一个保存请求。

然后我们想透明地订阅这些网络Observables中的每一个,并直接接收所有的网络响应,避免出现任何嵌套。

 

And we could do all this if we would have available some sort of a higher order RxJs mapping operator! Why do we need four different operators then?

我们可以使用某些更高阶的RxJs映射运算符来做这些!那我们为啥需要4个不同的操作符呢?

 

To understand that, imagine what happens if multiple form values are emitted by the valueChanges observable in quick succession and the save operation takes some time to complete:

为了理解,想象一下这种情况下会发生什么:valueChanges observable快速连续的发出多组表单值,并且保存操作需要一定的时间才能完成。

  • should we wait for one save request to complete before doing another save?
  • 我们应该等待一个保存请求完成后再进行下一次保存吗?
  • should we do multiple saves in parallel?
  • 我们应该同时进行多次保存吗?
  • should we cancel an ongoing save and start a new one?
  • 我们应该取消正在进行的保存并开始一次新的保存吗?
  • should we ignore new save attempts while one is already ongoing?
  • 当一次保存正在进行的时候我们应该忽略新的保存尝试吗?

Before exploring each one of these use cases, let's go back to the nested subscribes code above.

在探索上述几种用例之前,先回顾一下上面的嵌套订阅代码。

In the nested subscribes example, we are actually triggering the save operations in parallel, which is not what we want because there is no strong guarantee that the backend will handle the saves sequentially and that the last valid form value is indeed the one stored on the backend.

在嵌套订阅例子里,我们已经并行地触发了保存操作,这并不是我们想要的,因为不能确保后端会按顺序依次处理这些保存(请求),最后一个有效的表单值确实是存储在后端的那个。

Let's see what it would take to ensure that a save request is done only after the previous save is completed.

我们看看如何确保 一个保存请求 只有在 前一个保存请求 完成后 才会被执行。

Understanding Observable Concatenation

In order to implement sequential saves, we are going to introduce the new notion of Observable concatenation. In this code example, we are concatenating two example observables using the concat() RxJs function:

为了实现依次保存,我们将介绍Observable连接的概念。在这份样例代码里,我们把两个样例observables使用 concat() RxJs方法连接起来:

   
  const series1$ = of('a', 'b');
   
  const series2$ = of('x', 'y');
   
  const result$ = concat(series1$, series2$);
   
  result$.subscribe(console.log);
view raw03.ts hosted with ❤ by GitHub

After creating two Observables series1$ and series2$ using the of creation function, we have then created a third result$ Observable, which is the result of concatenating series1$ and series2$.

使用 of 创建方法创建了两个Observables series1$serries2$ 后,我们创建第三个 result$ Observable,也就是 series1$和 serries2$ 连接后的结果。

Here is the console output of this program, showing the values emitted by the result Observable:

这是这个程序的控制台输出,展示了result Observable发出的值:

a
b
x
y

As we can see, the values are the result of concatenating the values of series1$ with series2$ together. But here is the catch: this only works because these Observables are completing!!

如我们所见,这些值就是把series1$和 serries2$ 连接在一起的结果。但这里有一个问题:只有在这些Observables(指series1$和 serries2$)都完成了才正常工作!

The of() function will create Observables that emit values passed to of() and then it will complete the Observables after all values are emitted.

of方法创建的Observable会发出传递给of方法的全部值然后在全部值都发出后就会完成这个Observable。

Observable Concatenation Marble Diagram

To really understand what is going on, we need to look at the Observable concatenation marble diagram:

为了真正理解发生了啥,我们需要看下Observable连接弹珠图:

RxJs Map Operator marble diagram

Do you notice the vertical bar after the value b on the first Observable? That marks the point in time when the first Observable with values a and b (series1$) is completed.

有没有注意到第一个Observable的值b后面的那条竖线?那标记了具有值a和b的第一个Observable完成的时间点。

Let's break down what is going on here by following step-by-step the timeline:

我们按照时间线来分解一下这里发生了什么:

  • the two Observables series1$ and series2$ are passed to the concat() function
  • 两个Observables series1$ and series2$ 传给了concat()方法
  • concat() will then subscribe to the first Observable series1$, but not to the second Observable series2$ (this is critical to understand concatenation)
  • concat()将会订阅第一个Observable series1$,但是没有订阅第二个Observable series2$(这对理解连接很重要)
  • source1$ emits the value a, which gets immediately reflected in the output result$ Observable
  • series1$发出值a,马上就反映在输出result$ Observable上(原文写着source1$,个人认为应该是前面说的series1$哈,下同)
  • note that the source2$ Observable is not yet emitting values, because it has not yet been subscribed to
  • 注意 series2$ 还没有发出值,因为它还根本没有被订阅
  • source1$ will then emit the b value, which gets reflected in the output
  • series1$ 接下来将发出值 b,也被反映在输出上
  • source1$ will then complete, and only after that will concat() now subscribe to source2$
  • series1$ 接下来将完成,只有在此后concat() 才将订阅 series2$
  • the source2$ values will then start getting reflected in the output, until source2$ completes
  • series2$ 的值接下来将开始反映在输出上,直到 seires2$ 完成
  • when source2$ completes, then the result$ Observable will also complete
  • series2$完成时,result$ 也将完成
  • note that we can pass to concat() as many Observables as we want, and not only two like in this example
  • 注意,我们传多少个Observable给concat()方法都可以,而不仅仅是向本例里只传两个

The key point about Observable Concatenation

As we can see, Observable concatenation is all about Observable completion! We take the first Observable and use its values, wait for it to complete and then we use the next Observable, etc. until all Observables complete.

如我们所见,Observable连接全是和Observable完成有关!我们拿着第一个observable使用它的值,等待它完成,然后使用下一个observable,以此类推,直到所有observable都完成。

Going back to our higher-order Observable mapping example, let's see how the notion of concatenation can help us.

回到我们的高阶observable 映射样例,我们看看连接这个概念如何帮助我们。

Using Observable Concatenation to implement sequential saves

As we have seen, in order to make sure that our form values are saved sequentially, we need to take each form value and map it to an httpPost$ Observable.

如我们所见,为了确保我们的表单值是依次保存的,我们需要拿到每个表单数据然后映射给一个httppost$ observable。

We then need to subscribe to it, but we want the save to complete before subscribing to the next httpPost$ Observable.

我们需要订阅他,但是我们希望在订阅下一个 httpPost$ observable前完成 保存 。

In order to ensure sequentiality, we need to concatenate the multiple httpPost$ Observables together!

We will then subscribe to each httpPost$ and handle the result of each request sequentially. In the end, what we need is an operator that does a mixture of:

我们接下来将订阅每个httpPost$并依次处理每个请求的结果。最后,我们需要的是一个做以下混合的操作符:

  • a higher-order mapping operation (taking the form value and turning it into an httpPost$ Observable)
  • 一个高阶mapping操作(获取表单值转换为一个httpPost$ observable)
  • with a concat() operation, concatenating the multiple httpPost$ Observables together to ensure that an HTTP save is not made before the previous ongoing save completes first
  • 有一个concat()操作,把多个httpPost$ observable 连接到一起来确保上一个正在进行的保存完成之前不会发出下一个HTTP保存

What we need is the aptly named RxJs concatMap Operator, which does this mixture of higher order mapping with Observable concatenation.

我们需要恰当命名的RxJs concatMap操作符,它将高阶映射和observable 连接混合在一起

The RxJs concatMap Operator

Here is what our code looks like if we now use the concatMap Operator:

   
  this.form.valueChanges
  .pipe(
  concatMap(formValue => this.http.put(`/api/course/${courseId}`,
  formValue))
  )
  .subscribe(
  saveResult => ... handle successful save ...,
  err => ... handle save error ...
  );
view raw04.ts hosted with ❤ by GitHub

As we can see, the first benefit of using a higher-order mapping operator like concatMap is that now we no longer have nested subscribes.

如我们所见,使用高接映射操作符如concatMap的第一个好处是现在我们不再有嵌套订阅。

By using concatMap, now all form values are going to be sent to the backend sequentially, as shown here in the Chrome DevTools Network tab:

通过使用concatMap,现在所有表单值都将会被依次发送到后端,正如这里chrome devtools network tab所展示的:

RxJs concatMap Example

Breaking down the concatMap network log diagram

As we can see, one save HTTP request starts only after the previous save has completed. Here is how the concatMap operator is ensuring that the requests always happen in sequence:

如我们所见,一个HTTP保存请求只有在上一个保存请求完成后才会发起。以下是concatMap操作符确保请求总是依次发起的方法:

  • concatMap is taking each form value and transforming it into a save HTTP Observable, called an inner Observable
  • concatMap获取每个表单值,转换成一个http保存observable,我们叫它内部observable
  • concatMap then subscribes to the inner Observable and sends its output to the result Observable
  • concatMap然后订阅这个内部Observable并且把它的输出发送给结果Observable
  • a second form value might come faster than what it takes to save the previous form value in the backend
  • 第二个表单值可能比后端保存前一个表单值所花的时间来得快
  • If that happens, that new form value will not be immediately mapped to an HTTP request
  • 如发生这种情况,新的表单值不会马上映射给一个HTTP请求
  • instead, concatMap will wait for previous HTTP Observable to complete before mapping the new value to an HTTP Observable, subscribing to it and therefore triggering the next save
  • 而是,concatMap会等待前一个HTTP observable完成,然后在把新的值映射给一个HTTP Observable,订阅它并因此触发下一次保存

Notice that the code here is just the basis of an implementation to save draft form values. You can combine this with other operators to for example save only valid form values, and throttle the saves to make sure that they don't occur too frequently.

注意这里的代码只是保存草稿表单值的基本实现。你可以结合其他操作符来实现比如说只保存有效表单值,比如说节流保存来确保保存不会太频繁发生。

Observable Merging

Applying Observable concatenation to a series of HTTP save operations seems like a good way to ensure that the saves happen in the intended order.

将 Observable 连接应用于一系列 HTTP 保存操作似乎是确保保存按预期顺序发生的好方法。

But there are other situations where we would like to instead run things in parallel, without waiting for the previous inner Observable to complete.

但在其他情况下,我们希望并行运行,而不是等待前面的内部 Observable 完成。

And for that, we have the merge Observable combination strategy! Merge, unlike concat, will not wait for an Observable to complete before subscribing to the next Observable.

为此,我们有 merge Observable 组合策略! 与 concat 不同,Merge 在订阅下一个 Observable 之前不会等待 Observable 完成。

Instead, merge subscribes to every merged Observable at the same time, and then it outputs the values of each source Observable to the combined result Observable as the multiple values arrive over time.

Practical Merge Example

To make it clear that merging does not rely on completion, let's merge two Observables that never complete, as these are interval Observables:

   
  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);
view raw05.ts hosted with ❤ by GitHub

The Observables created with interval() will emit the values 0, 1, 2, etc. at a one second interval and will never complete.

Notice that we are applying a couple of map operator to these interval Observables, just to make it easier to distinguish them in the console output.

Here are the first few values visible in the console:

0
0
10
100
20
200
30
300

Merging and Observable Completion

As we can see, the values of the merged source Observables show up in the result Observable immediately as they are emitted. If one of the merged Observables completes, merge will continue to emit the values of the other Observables as they arrive over time.

Notice that if the source Observables do complete, merge will still work in the same way.

The Merge Marble Diagram

Let's take another merge example, depicted in the following marble diagram:

RxJs merge Example

As we can see, the values of the merged source Observables show up immediately in the output. The result Observable will not be completed until all the merged Observables are completed.

Now that we understand the merge strategy, let's see how it how it can be used in the context of higher-order Observable mapping.

The RxJs mergeMap Operator

If we combine the merge strategy with the notion of higher-order Observable mapping, we get the RxJs mergeMap Operator. Let's have a look at the marble diagram for this operator:

RxJs mergeMap Example

Here is how the mergeMap operator works:

  • each value of the source Observable is still being mapped into an inner Observable, just like the case of concatMap
  • Like concatMap, that inner Observable is also subscribed to by mergeMap
  • as the inner Observables emit new values, they are immediately reflected in the output Observable
  • but unlike concatMap, in the case of mergeMap we don't have to wait for the previous inner Observable to complete before triggering the next innner Observable
  • this means that with mergeMap (unlike concatMap) we can have multiple inner Observables overlapping over time, emitting values in parallel like we see highlighted in red in the picture

Checking the mergeMap Network Log

Going back to our previous form draft save example, its clear that what we need concatMap in that case and not mergeMap, because we don't want the saves to happen in parallel.

Let's see what happens if we would accidentally choose mergeMap instead:

   
  this.form.valueChanges
  .pipe(
  mergeMap(formValue =>
  this.http.put(`/api/course/${courseId}`,
  formValue))
  )
  .subscribe(
  saveResult => ... handle successful save ...,
  err => ... handle save error ...
  );
view raw06.ts hosted with ❤ by GitHub

Let's now say that the user interacts with the form and starts inputting data rather quickly. In that case, we would now see multiple save requests running in parallel in the network log:

RxJs mergeMap Example

As we can see, the requests are happening in parallel, which in this case is an error! Under heavy load, it's possible that these requests would be processed out of order.

Observable Switching

Let's now talk about another Observable combination strategy: switching. The notion of switching is closer to merging than to concatenation, in the sense that we don't wait for any Observable to terminate.

But in switching, unlike merging, if a new Observable starts emitting values we are then going to unsubscribe from the previous Observable, before subscribing to the new Observable.

Observable switching is all about ensuring that the unsubscription logic of unused Observables gets triggered, so that resources can be released!

Switch Marble Diagram

Let's have a look at the marble diagram for switching:

RxJs switch Example

Notice the diagonal lines, these are not accidental! In the case of the switch strategy, it was important to represent the higher-order Observable in the diagram, which is the top line of the image.

This higher-order Observable emits values which are themselves Observables.

The moment that a diagonal line forks from the higher-order Observable top line, is the moment when a value Observable was emitted and subscribed to by switch.

Breaking down the switch Marble Diagram

Here is what is going on in this diagram:

  • the higher-order Observable emits its first inner Observable (a-b-c-d), that gets subscribed to (by the switch strategy implementation)
  • the first inner Observable (a-b-c-d) emits values a and b, that get immediately reflected in the output
  • but then the second inner Observable (e-f-g) gets emitted, which triggers the unsubscription from the first inner Observable (a-b-c-d), and this is the key part of switching
  • the second inner Observable (e-f-g) then starts emitting new values, that get reflected in the output
  • but notice that the first inner Observable (a-b-c-d) is meanwhile still emitting the new values c and d
  • these later values, however, are not reflected in the output, and that is because we had meanwhile unsubscribed from the first inner Observable (a-b-c-d)

We can now understand why the diagram had to be drawn in this unusual way, with diagonal lines: its because we need to represent visually when each inner Observable gets subscribed (or unsubscribed) from, which happens at the points the diagonal lines fork from the source higher-order Observable.

The RxJs switchMap Operator

Let's then take the switch strategy and apply it to higher order mapping. Let's say that we have a plain input stream that is emitting the values 1, 3 and 5.

We are then going to map each value to an Observable, just like we did in the cases of concatMap and mergeMap and obtain a higher-order Observable.

If we now switch between the emitted inner Observables, instead of concatenating them or merging them, we end up with the switchMap Operator:

RxJs switchMap Example

Breaking down the switchMap Marble Diagram

Here is how this operator works:

  • the source observable emits values 1, 3 and 5
  • these values are then turned into Observables by applying a mapping function
  • the mapped inner Observables get subscribed to by switchMap
  • when the inner Observables emit a value, the value gets immediately reflected in the output
  • but if a new value like 5 gets emitted before the previous Observable got a chance to complete, the previous inner Observable (30-30-30) will be unsubscribed from, and its values will no longer be reflected in the output
  • notice the 30-30-30 inner Observable in red in the diagram above: the last 30 value was not emitted because the 30-30-30 inner Observable got unsubscribed from

So as we can see, Observable switching is all about making sure that we trigger that unsubscription logic from unused Observables. Let's now see switchMap in action!

Search TypeAhead - switchMap Operator Example

A very common use case for switchMap is a search Typeahead. First let's define our source Observable, whose values are themselves going to trigger search requests.

This source Observable is going to emit values which are the search text that the user types in an input:

   
  const searchText$: Observable<string> =
  fromEvent<any>(this.input.nativeElement, 'keyup')
  .pipe(
  map(event => event.target.value),
  startWith('')
  )
  .subscribe(console.log);
   
   
view raw07.ts hosted with ❤ by GitHub

This source Observable is linked to an input text field where the user types its search. As the user types the words "Hello World" as a search, these are the values emitted by searchText$:

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

Debouncing and removing duplicates from a Typeahead

Notice the duplicate values, either caused by the use of the space between the two words, or the use of the Shift key for capitalizing the letters H and W.

In order to avoid sending all these values as separate search requests to the backend, let's wait for the user input to stabilize by using the debounceTime operator:

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

With the use of this operator, if the user types at a normal speed, we now have only one value in the output of searchText$:

Hello World

This is already much better than what we had before, now a value will only be emitted if its stable for at least 400ms!

But if the user types slowly as he is thinking about the search, to the point that it takes more than 400 ms between two values, then the search stream could look like this:

He
Hell
Hello World

Also, the user could type a value, hit backspace and type it again, which might lead to duplicate search values. We can prevent the occurrence of duplicate searches by adding the distinctUntilChanged operator.

Cancelling obsolete searches in a Typeahead

But more than that, we would need a way to cancel previous searches, as a new search get's started.

What we want to do here is to transform each search string into a backend search request and subscribe to it, and apply the switch strategy between two consecutive search requests, causing the previous search to be canceled if a new search gets triggered.

And that is exactly what the switchMap operator will do! Here is the final implementation of our Typeahead logic that uses it:

   
  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});
  }
view raw09.ts hosted with ❤ by GitHub

switchMap Demo with a Typeahead

Let's now see the switchMap operator in action! If the user types on the search bar, and then hesitates and types something else, here is what we can typically see in the network log:

RxJs switchMap Example

As we can see, several of the previous searches have been canceled as they where ongoing, which is awesome because that will release server resources that can then be used for other things.

The Exhaust Strategy

The switchMap operator is ideal for the typeahead scenario, but there are other situations where what we want to do is to ignore new values in the source Observable until the previous value is completely processed.

For example, let's say that we are triggering a backend save request in response to a click in a save button. We might try first to implement this using the concatMap operator, in order to ensure that the save operations happen in sequence:

   
  fromEvent(this.saveButton.nativeElement, 'click')
  .pipe(
  concatMap(() => this.saveCourse(this.form.value))
  )
  .subscribe();
view raw10.ts hosted with ❤ by GitHub

This ensures the saves are done in sequence, but what happens now if the user clicks the save button multiple times? Here is what we will see in the network log:

RxJs Exhaust Strategy

As we can see, each click triggers its own save: if we click 20 times, we get 20 saves! In this case, we would like something more than just ensuring that the saves happen in sequence.

We want also to be able to ignore a click, but only if a save is already ongoing. The exhaust Observable combination strategy will allow us to do just that.

Exhaust Marble Diagram

To understand how exhaust works, let's have a look at this marble diagram:

RxJs Exhaust Strategy

Just like before, we have here a higher-order Observable on the first line, whose values are themselves Observables, forking from that top line. Here is what is going on in this diagram:

  • Just like in the case of switch, exhaust is subscribing to the first inner Observable (a-b-c)
  • The value a, b and c get immediately reflected in the output, as usual
  • then a second inner Observable (d-e-f) is emitted, while the first Observable (a-b-c) is still ongoing
  • This second Observable gets discarded by the exhaust strategy, and it will not be subscribed to (this is the key part of exhaust)
  • only after the first Observable (a-b-c) completes, will the exhaust strategy subscribe to new Observables
  • when the third Observable (g-h-i) is emitted, the first Observable (a-b-c) has already completed, and so this third Observable will not be discarded and will be subscribed to
  • the values g-h-i of the third Observable will then show up in the output of the result Observable, unlike to values d-e-f that are not present in the output

Just like the case of concat, merge and switch, we can now apply the exhaust strategy in the context of higher-order mapping.

The RxJs exhaustMap Operator

Let's now have a look at the marble diagram of the exhaustMap operator. Let's remember, unlike the top line of the previous diagram, the source Observable 1-3-5 is emitting values that are not Observables.

Instead, these values could for example be mouse clicks:

RxJs exhaustMap Example

So here is what is going on in the case of the exhaustMap diagram:

  • the value 1 gets emitted, and a inner Observable 10-10-10 is created
  • the Observable 10-10-10 emits all values and completes before the value 3 gets emitted in the source Observable, so all 10-10-10 values where emitted in the output
  • a new value 3 gets emitted in the input, that triggers a new 30-30-30 inner Observable
  • but now, while 30-30-30 is still running, we get a new value 5 emitted in the source Observable
  • this value 5 is discarded by the exhaust strategy, meaning that a 50-50-50 Observable was never created, and so the 50-50-50 values never showed up in the output

A Practical Example for exhaustMap

Let's now apply this new exhaustMap Operator to our save button scenario:

   
  fromEvent(this.saveButton.nativeElement, 'click')
  .pipe(
  exhaustMap(() => this.saveCourse(this.form.value))
  )
  .subscribe();
view raw11.ts hosted with ❤ by GitHub

If we now click save let's say 5 times in a row, we are going to get the following network log:

RxJs Exhaust Strategy

As we can see, the clicks that we made while a save request was still ongoing where ignored, as expected!

Notice that if we would keep clicking for example 20 times in a row, eventually the ongoing save request would finish and a second save request would then start.

How to choose the right mapping Operator?

The behavior of concatMap, mergeMap, switchMap and exhaustMap is similar in the sense they are all higher order mapping operators.

But its also so different in so many subtle ways, that there isn't really one operator that can be safely pointed to as a default.

Instead, we can simply choose the appropriate operator based on the use case:

  • if we need to do things in sequence while waiting for completion, then concatMap is the right choice

  • for doing things in parallel, mergeMap is the best option

  • in case we need cancellation logic, switchMap is the way to go

  • for ignoring new Observables while the current one is still ongoing, exhaustMap does just that

posted @ 2021-08-11 15:40  透明飞起来了  阅读(827)  评论(0编辑  收藏  举报