我正在尝试使用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 groupBy
和concatMap
的文档不会为我提供任何有关此处可能发生的情况的线索。而reactivex.io中有关RxJS concatMap
的部分使我相信这应该可行。
有人可以帮助我了解这里第一种情况的情况吗?我该如何使第一个方案起作用?
答案 0 :(得分:5)
我似乎终于明白了这里的问题所在。
在上述问题的方案#1中,代码首先将源流传输到groupBy
运算符中,然后再传输到concatMap
运算符中。而且这些运算符的组合似乎正在导致此问题。
groupBy
和mergeMap
的内部运作方式通读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
发出的源流中的所有值都将丢失。
这不是这些运算符的工作方式的“问题”,而是我对这些运算符的理解以及文档(尤其是Subject
的文档)的“问题”,这可能会使刚接触该文档的人有些困惑RxJS)。
可以通过使concatMap
运算符使用groupBy
而不是ReplaySubject
来发出分组的值来轻松解决此问题。 Subject
接受一个groupBy
参数,该参数使我们可以将subjectSelector
实例切换为Subject
实例。
以下代码有效:
ReplaySubject
在我的问题中,方案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);
}
}
}