简体   繁体   English

rxJs 中的 flatMap 与 concatMap 的挣扎

[英]Struggling with flatMap vs concatMap in rxJs

I am struggling to understand the difference between the flatMap and concatMap in rxJs.我正在努力理解 rxJs 中flatMapconcatMap之间的区别。

The most clear answer that I could understand was that here difference-between-concatmap-and-flatmap我能理解的最明确的答案是这里的区别-concatmap-and-flatmap

So I went and tried things out by my self.所以我自己去尝试了一些东西。

import "./styles.css";
import { switchMap, flatMap, concatMap } from "rxjs/operators";
import { fromFetch } from "rxjs/fetch";
import { Observable } from "rxjs";

function createObs1() {
  return new Observable<number>((subscriber) => {
    setTimeout(() => {
      subscriber.next(1);
      subscriber.complete();
    }, 900);
  });
}

function createObs2() {
  return new Observable<number>((subscriber) => {
    setTimeout(() => {
      subscriber.next(2);
      //subscriber.next(22);
      //subscriber.next(222);
      subscriber.complete();
    }, 800);
  });
}

function createObs3() {
  return new Observable<number>((subscriber) => {
    setTimeout(() => {
      subscriber.next(3);
      //subscriber.next(33);
      //subscriber.next(333);
      subscriber.complete();
    }, 700);
  });
}

function createObs4() {
  return new Observable<number>((subscriber) => {
    setTimeout(() => {
      subscriber.next(4);
      subscriber.complete();
    }, 600);
  });
}

function createObs5() {
  return new Observable<number>((subscriber) => {
    setTimeout(() => {
      subscriber.next(5);
      subscriber.complete();
    }, 500);
  });
}

createObs1()
  .pipe(
    flatMap((resp) => {
      console.log(resp);
      return createObs2();
    }),
    flatMap((resp) => {
      console.log(resp);
      return createObs3();
    }),
    flatMap((resp) => {
      console.log(resp);
      return createObs4();
    }),
    flatMap((resp) => {
      console.log(resp);
      return createObs5();
    })
  )
  .subscribe((resp) => console.log(resp));

console.log("hellooo");

I have used that playground here playground example我在这里用过那个游乐场的例子

Questions问题

1) From my understanding the use of flatMap should mix the outputs so that the console logs are like (1,3,2,4,5). 1)据我了解,flatMap 的使用应该混合输出,以便控制台日志类似于(1、3、2、4、5)。 I have tried more than 30 times and always come on the same row (1, 2, 3, 4, 5)我已经尝试了 30 多次并且总是在同一行(1、2、3、4、5)

What am I doing wrong or have undestood wrong?我做错了什么或有什么不明白的错误?

2) If on createObs2() and createObs3() you remove the comments and include the code with multiple emitted events then things get messy. 2)如果在createObs2()createObs3()上删除注释并包含带有多个发出事件的代码,那么事情就会变得一团糟。 Even if you change to concatMap it messes things and results come mixed.即使您更改为 concatMap ,它也会使事情变得混乱,结果也很复杂。 Multiple numbers that I expect only once come multiple times.我期望只出现一次的多个数字多次出现。 The result can be (1, 2, 33, 3, 2, 22, 3, 33, 4, 5, 4, 3, 4, 5) Why this happens?结果可能是 (1, 2, 33, 3, 2, 22, 3, 33, 4, 5, 4, 3, 4, 5) 为什么会这样?

How I test the example on playground.我如何在操场上测试这个例子。 I just remove only 1 letter from the last console.log("hello").我只从最后一个 console.log("hello") 中删除了 1 个字母。 Only one change for example console.log("heloo") and is then observed and project is compiled again and output printed in console.只有一个更改,例如 console.log("heloo") 然后被观察并再次编译项目并在控制台中打印 output。

Edit: The reason I have gone to flatMap and concatMap was to find a replacement for nested subscriptions in angular using the http library.编辑:我去 flatMap 和 concatMap 的原因是使用 http 库在 angular 中找到嵌套订阅的替代品。

createObs1().subscribe( (resp1) => {
          console.log(resp1);
          createObs2().subscribe( (resp2) => {
               console.log(resp2);
               createObs3().subscribe( (resp3) => {
                   console.log(resp3);
                   createObs4().subscribe( (resp4) => {
                        console.log(resp4);
                        createObs5().subscribe( (resp5) => {
                            console.log(resp5);
                            })
                        })
                   })
               })
             })

Your test scenario is not really sufficient to see the differences between these two operators.您的测试场景不足以看到这两个运算符之间的差异。 In your test case, each observable only emits 1 time.在您的测试用例中,每个 observable 只发出 1 次。 If an observable only emits a single value, there is really no different between concatMap and flatMap ( aka mergeMap ).如果一个 observable 只发出一个值,那么concatMapflatMap ( aka mergeMap ) 之间并没有什么不同。 The differences can only be seen when there are multiple emissions.只有在多次排放时才能看到差异。

So, let's use a different scenario.所以,让我们使用不同的场景。 Let's have a source$ observable that simply emits an incrementing integer every 1 second.让我们有一个source$ observable,它每 1 秒简单地发出一个递增的 integer。 Then, within our " Higher Order Mapping Operator " ( concatMap & mergeMap ), we will return an observable that emits a variable number of times every 1 second, then completes.然后,在我们的“ 高阶映射运算符”( concatMapmergeMap )中,我们将返回一个每 1 秒发出可变次数的可观察对象,然后完成。

// emit number every second
const source$ = interval(1000).pipe(map(n => n+1)); 

// helper to return observable that emits the provided number of times
function inner$(max: number, description: string): Observable<string> {
  return interval(1000).pipe(
    map(n => `[${description}: inner source ${max}] ${n+1}/${max}`),
    take(max), 
  );
}

Then let's define two separate observables based on the source$ and the inner$ ;然后让我们根据source$inner$定义两个独立的 observables; one using concatMap and one using flatMap and observe the output.一种使用concatMap ,另一种使用flatMap并观察 output。

const flatMap$ = source$.pipe(
    flatMap(n => inner$(n, 'flatMap$'))
);

const concatMap$ = source$.pipe(
    concatMap(n => inner$(n, 'concatMap$'))
);

Before looking the differences in the output, let's talk about what these operators have in common.在查看 output 的区别之前,让我们先谈谈这些运算符的共同点。 They both:他们两个:

  • subscribe to the observable returned by the passed in function订阅传入的 function 返回的 observable
  • emit emissions from this "inner observable"从这个“内部可观察”排放出排放物
  • unsubscribe from the inner observable(s)取消订阅内部 observable(s)

What's different, is how they create and manage inner subscriptions:不同的是,他们如何创建和管理内部订阅:

concatMap - only allows a single inner subscription at a time. concatMap - 一次只允许一个内部订阅。 As it receives emissions, it will only subscribe to one inner observable at a time.当它接收到发射时,它一次只会订阅一个内部可观察对象。 So it will initially subscribe to the observable created by "emission 1", and only after it completes, will it subscribe to the observable created by "emission 2".所以它最初会订阅“emission 1”创建的 observable,只有在它完成后,才会订阅“emission 2”创建的 observable。 This is consistent with how the concat static method behaves.这与concat static 方法的行为方式一致。

flatMap ( aka mergeMap ) - allows many inner subscriptions. flatMap ( aka mergeMap ) - 允许许多内部订阅。 So, it will subscribe to the inner observables as new emissions are received.因此,它会在接收到新的发射时订阅内部的 observables。 This means that emissions will not be in any particular order as it will emit whenever any of its inner observables emit.这意味着发射不会按任何特定顺序进行,因为它会在其任何内部可观察对象发射时发射。 This is consistent with how themerge static method behaves ( which is why I personally prefer the name "mergeMap" ).这与merge static 方法的行为方式一致(这就是我个人更喜欢名称“mergeMap”的原因)。

Here's a StackBlitz that shows the output for the above observables concatMap$ and mergeMap$ :这是一个StackBlitz ,它显示了上述 observables concatMap$mergeMap$的 output : concatMap 与 mergeMap

Hopefully, the above explanation helps to clear up your questions!希望以上解释有助于解决您的问题!

#1 - " use of flatMap should mix the outputs " #1 - “使用 flatMap 应该混合输出

The reason this wasn't working as you expected was because only one emission was going through the flatMap , which means you only ever had a single "inner observable" emitting values.这没有像您预期的那样工作的原因是因为只有一个发射通过flatMap ,这意味着您只有一个“内部可观察”发射值。 As demonstrated in the above example, once flatMap receives multiple emissions, it can have multiple inner observables that emit independently.如上例所示,一旦 flatMap 接收到多个发射,它就可以有多个独立发射的内部 observable。

#2 - " ...and include the code with multiple emitted events then things get messy. " #2 - “ ...并包含带有多个发出事件的代码,然后事情变得一团糟。

The "things get messy" is due to having multiple inner subscription that emit values. “事情变得一团糟”是由于有多个发出值的内部订阅。

For the part you mention about using concatMap and still getting "mixed" output, I would not expect that.对于您提到的关于使用concatMap并且仍然“混合”output 的部分,我不希望这样。 I have seen weird behavior in StackBlitz with observable emissions when "auto save" is enabled ( seems like sometimes it doesn't completely refresh and old subscriptions seem to survive the auto refresh, which gives very messy console output ).当启用“自动保存”时,我在 StackBlitz 中看到了具有可观察排放的奇怪行为(似乎有时它没有完全刷新,旧订阅似乎在自动刷新后仍然存在,这使得控制台非常混乱 output )。 Maybe code sandbox has a similar problem.也许代码沙箱也有类似的问题。

#3 - " The reason I have gone to flatMap and concatMap was to find a replacement for nested subscriptions in angular using the http library " #3 - “我使用 flatMap 和 concatMap 的原因是使用 http 库在 angular 中找到嵌套订阅的替代品

This makes sense.这是有道理的。 You don't want to mess around with nested subscriptions, because there isn't a great way to guarantee the inner subscriptions will be cleaned up.你不想搞乱嵌套订阅,因为没有一种很好的方法可以保证内部订阅会被清理。

In most cases with http calls, I find that switchMap is the ideal choice because it will drop emissions from inner observables you no longer care about.在大多数使用 http 调用的情况下,我发现switchMap是理想的选择,因为它会丢弃您不再关心的内部可观察对象的排放。 Imagine you have a component that reads an id from a route param.想象一下,您有一个从路由参数中读取id的组件。 It uses this id to make an http call to fetch data.它使用此id进行 http 调用以获取数据。

itemId$ = this.activeRoute.params.pipe(
    map(params => params['id']),
    distinctUntilChanged()
);

item$ = this.itemId$.pipe(
    switchMap(id => http.get(`${serverUrl}/items/${id}`)),
    map(response => response.data)
);

We want item$ to emit only the "current item" ( corresponds to the id in the url ).我们希望item$只发出“当前项目”(对应于 url 中的 id )。 Say our UI has a button the user can click to navigate to the next item by id and your app finds itself with a click-happy user who keeps smashing that button, which changes the url param even faster than the http call can return the data.假设我们的 UI 有一个按钮,用户可以单击该按钮以通过id导航到下一个项目,并且您的应用程序发现自己有一个喜欢点击的用户不断粉碎该按钮,这比 http 调用可以返回数据更快地更改 url 参数.

If we chose mergeMap , we would end up with many inner observables that would emit the results of all of those http calls.如果我们选择mergeMap ,我们最终会得到许多内部可观察对象,它们会发出所有这些 http 调用的结果。 At best, the screen will flicker as all those different calls come back.充其量,屏幕会随着所有这些不同的呼叫回来而闪烁。 At worst ( if the calls came back out of order ) the UI would be left displaying data that isn't in sync with the id in the url:-(在最坏的情况下(如果呼叫无序返回),UI 将显示与 url 中的 id 不同步的数据:-(

If we chose concatMap , the user would be forced to wait for all the http calls to be completed in series, even though we only care about that most recent one.如果我们选择concatMap ,用户将被迫等待所有 http 调用连续完成,即使我们只关心最近的调用。

But, with switchMap , whenever a new emission ( itemId ) is received, it will unsubscribe from the previous inner observable and subscribe to the new one.但是,使用switchMap ,每当收到新的发射( itemId )时,它将取消订阅前一个内部可观察对象并订阅新的。 This means it will not ever emit the results from the old http calls that are no longer relevant.这意味着它永远不会发出不再相关的旧 http 调用的结果。 :-) :-)

One thing to note is that since http observables only emit once, the choice between the various operators ( switchMap , mergeMap , concatMap ) may not seem to make a difference, since they all perform the "inner observable handling" for us.需要注意的一点是,由于 http 可观察对象只发出一次,因此各种运算符( switchMapmergeMapconcatMap )之间的选择似乎没有什么区别,因为它们都为我们执行“内部可观察处理”。 However, it's best to future-proof your code and choose the one that truly gives you the behavior you would want, should you start receiving more than a single emission.但是,如果您开始接收多个发射,最好对您的代码进行未来验证并选择真正为您提供所需行为的代码。

Every time the first observable emits, a second observable is created in the flatMap and starts emitting.每次第一个 observable 发射时,都会在flatMap中创建第二个 observable 并开始发射。 However, the value from the first observable is not passed along any further.但是,第一个 observable 的值不会进一步传递。

Every time that second observable emits, the next flatMap creates a third observable, and so on.每次第二个 observable 发射时,下一个flatMap创建第三个 observable,依此类推。 Again, the original value coming into the flatMap is not passed along any further.同样,进入flatMap的原始值不会进一步传递。

createObs1()
  .pipe(
    flatMap(() => createObs2()), // Merge this stream every time prev observable emits
    flatMap(() => createObs3()), // Merge this stream every time prev observable emits
    flatMap(() => createObs4()), // Merge this stream every time prev observable emits
    flatMap(() => createObs5()), // Merge this stream every time prev observable emits
  )
  .subscribe((resp) => console.log(resp));

// OUTPUT:
// 5

So, it's only the values emitted from createObs5() that actually get emitted to the observer.因此,只有从createObs5()发出的值才会真正发送给观察者。 The values emitted from the previous observables have just been triggering the creation of new observables.先前的 observable 发出的值刚刚触发了新 observable 的创建。

If you were to use merge , then you would get what you may have been expecting:如果您要使用merge ,那么您会得到您所期望的:

createObs1()
  .pipe(
    merge(createObs2()),
    merge(createObs3()),
    merge(createObs4()),
    merge(createObs5()),
  )
  .subscribe((resp) => console.log(resp));

// OUTPUT:
// 5
// 4
// 3
// 2
// 1

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM