RxJS:可观察到的流通过管道传输到groupBy(),再通过concatMap();后续密钥丢失的数据

时间:2018-09-28 20:05:56

标签: rxjs6

我正在尝试使用RxJS groupBy运算符,然后使用concatMap来根据某些键将记录收集到单独的组中。

我注意到,当concatMap跟随groupBy运算符时,似乎丢失了第一个键之后出现的所有键的数据。

例如:

考虑以下代码块:

// DOES NOT WORK
const records = ['a:1', 'b:2', 'c:3', 'd:1', 'e:2', 'f:3', 'g:1'];
const clicks = new Subject();

const result = clicks.pipe(
  groupBy(x => x.substr(2,1)),
  concatMap(ev$ => ev$.pipe(map(x => ({key: ev$.key, value: x})))),
);
const subscription = result.subscribe(x => console.log(x));

records.forEach(x => clicks.next(x));

// Expected Output:
// { key: '1', value: 'a:1' }
// { key: '1', value: 'd:1' }
// { key: '1', value: 'g:1' }
// { key: '2', value: 'b:2' }
// { key: '2', value: 'e:2' }
// { key: '3', value: 'c:3' }
// { key: '3', value: 'f:3' }
// 
// Actual Output:
// { key: '1', value: 'a:1' }
// { key: '1', value: 'd:1' }
// { key: '1', value: 'g:1' }
// ...Nothing more -- no results for key 2 and 3

但是,当我单独使用concatMap运算符时,它的行为符合预期。

// WORKS
const records = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
const clicks = new Subject();

const result = clicks.pipe(
  concatMap(ev => ev.subject$.pipe(take(4), map(x => ev.key + x))),
);
const subscription = result.subscribe(x => console.log(x));

records.forEach(x => clicks.next({key: x, subject$: interval(1000)}));

// Expected & Actual Output:
// a0
// a1
// a2
// a3
// b0
// b1
// b2
// b3
// c0
// c1
// c2
// c3
// d0
// d1
// d2
// d3
// e0
// e1
// e2
// e3
// f0
// f1
// f2
// f3
// g0
// g1
// g2
// g3

通读RxJS groupByconcatMap的文档不会为我提供任何有关此处可能发生的情况的线索。而reactivex.io中有关RxJS concatMap的部分使我相信这应该可行。

有人可以帮助我了解这里第一种情况的情况吗?我该如何使第一个方案起作用?

2 个答案:

答案 0 :(得分:5)

我似乎终于明白了这里的问题所在。

在上述问题的方案#1中,代码首先将源流传输到groupBy运算符中,然后再传输到concatMap运算符中。而且这些运算符的组合似乎正在导致此问题。

groupBymergeMap的内部运作方式

通读the code for the groupBy operator,我意识到groupBy在内部为在源流中找到的每个键创建了一个新的Subject实例。然后,该Subject实例立即发出属于该键的所有值。

所有Subject实例都被包装到GroupedObservale中,并由groupBy运算符向下游发出。 GroupedObservable实例的流是concatMap运算符的输入。

concatMap运算符在内部调用mergeMap值为1的concurrency运算符,这意味着只能同时订阅一个可观察的源。

mergeMap运算符仅预订一个可观测值,或者订阅conccurency参数所允许的可观测值,并将所有其他可观测值保留在“缓冲区”中,直到第一个观测值完成。 >

这是怎么造成问题的?

首先,由于我已经阅读了这些运算符的代码,所以我不太确定这是否是一个“问题”。

尽管如此,我在问题中描述的行为仍然是因为groupBy运算符立即使用相应的Subject实例发出各个值,而mergeMap运算符不会订阅该特定的{ {1}}。因此,使用该Subject发出的源流中的所有值都将丢失。

我试图用一个粗糙的大理石图来说明这个问题: groupBy to concatMap issue

这不是这些运算符的工作方式的“问题”,而是我对这些运算符的理解以及文档(尤其是Subject的文档)的“问题”,这可能会使刚接触该文档的人有些困惑RxJS)。

可以通过使concatMap运算符使用groupBy而不是ReplaySubject来发出分组的值来轻松解决此问题。 Subject接受一个groupBy参数,该参数使我们可以将subjectSelector实例切换为Subject实例。

以下代码有效:

ReplaySubject

方案2为什么起作用?

在我的问题中,方案2可以正常工作,因为// THIS VERSION WORKS const records = ['a:1', 'b:2', 'c:3', 'd:1', 'e:2', 'f:3', 'g:1']; const clicks = new Subject(); const result = clicks.pipe( groupBy(x => x.substr(2,1), null, null, () => new ReplaySubject()), concatMap(ev$ => ev$.pipe(map(x => ({key: ev$.key, value: x})))), ); const subscription = result.subscribe(x => console.log(x)); records.forEach(x => clicks.next(x)); // We also need to explicity complete() the source // stream to ensure that the observable stream for // the first GroupedObservable completes allowing // the concatMap operator to move to the second // GroupedObservable. clicks.complete(); // Expected and Actual output // { key: '1', value: 'a:1' } // { key: '1', value: 'd:1' } // { key: '1', value: 'g:1' } // { key: '2', value: 'b:2' } // { key: '2', value: 'e:2' } // { key: '3', value: 'c:3' } // { key: '3', value: 'f:3' } 仅创建了一个Observable,但没有开始发出值。因此,当interval最终订阅该Observable时,该Observable的所有值均可用。

答案 1 :(得分:0)

我的答案是补充Kiran的问题,并注意,如果您使用异步mergeMap,则会遇到与问题中所述完全相同的问题。

如Kiren所述,当您使用groupBy时,它会在内部创建一个Subject并立即订阅源。以下作品...

source.pipe(
  groupBy(item => item.id),
  mergeMap(byId => {
    return byId.pipe(map(x=>service.put(x)));
  }),

...因为(根据我的收集)订阅是同步的-mergeMap立即订阅每个新分组(假设没有并发限制),因此它捕获数据。

如果要按组异步执行某些操作,则可以尝试...

source.pipe(
  groupBy(item => item.id),
  mergeMap(async byId => {
    let service = await this.getSomething(byId.key); 
    return byId.pipe(map(x=>service.put(x)));
  }),
  mergeAll()

...,此时对Observable分组的订阅被推迟到mergeAll,它将缺少初始数据。

解决方案与Kiran所说的完全一样:您必须使用缓冲主题,以便在最终订阅该组时也可以重播这些值: groupBy(item => item.id, null, null,()=>new ReplaySubject())可以正常工作。

我的个人解决方案诞生于在初始订阅后不希望进行任何缓冲,因此,我创建了一个自定义BufferSubject,该缓冲仅缓冲到第一次订阅后才通过{{1 }}。

next

并用于代替重放:

/** buffers items until the first subscription, then replays them and stops buffering */
export class BufferSubject<T> extends Subject<T>{
  private _events: T[] = [];

  constructor(private scheduler?: SchedulerLike) {
    super();
  }

  next(value: T) {
    this._events.push(value);    
    super.next(value);
  }

  _subscribe(subscriber: Subscriber<T>): Subscription {
    const _events =  this._events;

    //stop buffering
    this.next = super.next;
    this._events = null;    

    const scheduler = this.scheduler;
    const len = _events.length;
    let subscription: Subscription;

    if (this.closed) {
      throw new ObjectUnsubscribedError();
    } else if (this.isStopped || this.hasError) {
      subscription = Subscription.EMPTY;
    } else {
      this.observers.push(subscriber);
      subscription = new SubjectSubscription(this, subscriber);
    }

    if (scheduler) {
      subscriber.add(subscriber = new ObserveOnSubscriber<T>(subscriber, scheduler));
    }

    for (let i = 0; i < len && !subscriber.closed; i++) {
      subscriber.next(_events[i]);
    }

    if (this.hasError) {
      subscriber.error(this.thrownError);
    } else if (this.isStopped) {
      subscriber.complete();
    }

    return subscription;
  }
}

/** from rxjs internals */
export class SubjectSubscription<T> extends Subscription {
  closed: boolean = false;

  constructor(public subject: Subject<T>, public subscriber: Observer<T>) {
    super();
  }

  unsubscribe() {
    if (this.closed) {
      return;
    }

    this.closed = true;

    const subject = this.subject;
    const observers = subject.observers;

    this.subject = null;

    if (!observers || observers.length === 0 || subject.isStopped || subject.closed) {
      return;
    }

    const subscriberIndex = observers.indexOf(this.subscriber);

    if (subscriberIndex !== -1) {
      observers.splice(subscriberIndex, 1);
    }
  }
}