使用rxjs时,为什么switchMap不会触发完整的事件?

时间:2017-10-31 09:27:40

标签: javascript angular rxjs reactive-programming

最近,在我的Angular应用程序中,我开始在几种不同的场景中使用rxjs switchMap运算符。我很快意识到,在使用switchMap时,当您订阅此流时,完成块不会触发(我不认为错误块也可以)。我在网上看到的所有例子似乎都没有处理完成块,我对这是什么原因感到困惑?

我显然遗漏了关于switchMap或如何使用它的内容,但我不知道是什么。

理想情况下,我想调用带有触发器Http请求的函数,然后处理错误块中的错误,然后在完成块中处理请求后的内容。

以下是我正在做的事情的例子:

export class ResultsComponent {

  ngAfterViewInit() {

    Observable.combineLatest(...filters)
        .debounceTime(500)
        .distinctUntilChanged()
        .switchMap((activeFilters: Array<ActiveFilter>) => {
            const filters = this.mapFilters(activeFilters);
            return this.doSearch(this.term$.getValue(), filters);
        })
        .subscribe((res) => {
           this.onSearchSuccess(res);
        },
        (err) => {
            // THIS NEVER FIRES
            console.error(err);
            this.loading$.next(false);
        ,() => {
            // THIS NEVER FIRES
            this.loading$.next(false);
        });
  }

  private doSearch(input: string, filters: object): Observable<object> {
    return this.searchService.search(input, filters);
  }
}

服务

export class SearchService {

  private baseUrl: string = 'http://mydomainhere.com/api';

  constructor(private http: Http) {}

  public search(input: string, filters: object): Observable<object> {
    const params = {
      "keyword": input,
      "filters": filters
    };
    const url = `${this.baseUrl}/search`;
    return this.http.post(url, params)
       .map(res => res.json())
       .catch(this.handleError);
  }
}

2 个答案:

答案 0 :(得分:4)

对于switchMap,除非外部observable已经完成,否则内部observable的完成不会触发流的完成。这是一个说明这个的例子:

&#13;
&#13;
const first = Rx.Observable.interval(2000).take(2)
	.do(console.log.bind(null, 'first next'),
      console.log.bind(null, 'first error'),
      console.log.bind(null, 'first complete'));
const second = Rx.Observable.interval(200).take(2)
	.do(console.log.bind(null, 'second next'),
      console.log.bind(null, 'second error'),
      console.log.bind(null, 'second complete'));

first.switchMap(() => second)
	.subscribe(console.log.bind(null, 'stream next'),
      console.log.bind(null, 'stream error'),
      console.log.bind(null, 'stream complete'));
&#13;
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.2/Rx.js"></script>
&#13;
&#13;
&#13;

内部observable中抛出的错误将调用外部observable上的错误块。这是一个说明这个的例子:

&#13;
&#13;
const source = Rx.Observable.interval(2000).take(4)
	.do(console.log.bind(null, 'source next'),
      console.log.bind(null, 'source error'),
      console.log.bind(null, 'source complete'));
const error = Rx.Observable.create((o) => {
  o.error();
}).do(console.log.bind(null, 'error next'),
      console.log.bind(null, 'error error'),
      console.log.bind(null, 'error complete'));

source.switchMap(() => error)
	.subscribe(console.log.bind(null, 'stream next'),
      console.log.bind(null, 'stream error'),
      console.log.bind(null, 'stream complete'));
&#13;
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.2/Rx.js"></script>
&#13;
&#13;
&#13;

所以你可以在外部observable上设置一个catch,如果你愿意,可以得到错误。

如果你想观察内部observable的完成,你将不得不在switchMap中观察它。

至于为什么你没有看到很多关于在线使用完成块的事情,我不能为所有人说话,但我个人并不觉得自己在我的应用程序中需要它。我只关心来自next的数据。

答案 1 :(得分:1)

用内部完成外部 Observable

有多种方法可以使外部 observable 与内部 observable 一起完成。 (下一节将解释为什么您可能不想这样做,然后是在外部 observable 未完成时检测内部 observable 完成的示例。)

如果您知道内部 observable 在完成之前只会发出一个值,就像 API 调用一样,您只需将 first 通过管道传输到外部 observable。

const { of , pipe } = rxjs;
const { switchMap, first } = rxjs.operators;

const stream = of(1, 2, 3).pipe(
    switchMap(() => of(4)),
    first()
  )
  .subscribe({
    next: (x) => console.log(x),
    complete: () => console.log('outer complete')
  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.6.7/rxjs.umd.js"></script>

但是如果内部 observable 发出多个值,一个简单的改变就是使用 endWithtakeWhile 来告诉外部 observable 何时完成。这假设我们知道内部 observable 永远不会发出 null

const { of , pipe } = rxjs;
const { switchMap, endWith, takeWhile } = rxjs.operators;

const stream = of(1, 2, 3).pipe(
    switchMap(() => of(4, 5, 6).pipe(
      endWith(null)
    )),
    takeWhile((x) => x != null)
  )
  .subscribe({
    next: (x) => console.log(x),
    complete: () => console.log('outer complete')
  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.6.7/rxjs.umd.js"></script>

一般的解决方案是当内部 observable 完成时让 Subject 发出,并在 Subject 发出时让外部 observable 完成,用 takeUntil 监视它。

const { of , pipe, Subject } = rxjs;
const { switchMap, tap, takeUntil } = rxjs.operators;

const innerComplete = new Subject();

const stream = of(1, 2, 3).pipe(
    switchMap(() => of(4, 5, 6).pipe(
      tap({
        complete: () => innerComplete.next()
      })
    )),
    takeUntil(innerComplete)
  )
  .subscribe({
    next: (x) => console.log(x),
    complete: () => console.log('outer complete')
  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.6.7/rxjs.umd.js"></script>

为什么没有完成?

当我第一次开始使用 RxJS 时,我主要是将现有的 API 调用转换为使用 observables 处理。实际上,这意味着当内部 observable 完成时,外部 observable 也会完成。但重要的是要注意外部 observable 没有完成,因为内部 observable 完成了。它完成是因为它只会发出一个值。如果它是一个可以发出多个值的 observable,比如来自鼠标点击事件的值,那么它就不会用内部 observable 完成。

这是一件好事。它允许您拥有一个外部 observable,通过内部 observable 映射其排放,而不会像内部 observable 那样第一次完成。例如,假设您想在每次单击鼠标时触发动画,并且动画由计时器控制。鼠标点击将由外部 observable 发出。内部 observable 会运行一个计时器几秒钟来控制动画。动画完成后,您仍希望捕获鼠标单击事件,以便动画可以再次启动。

以下代码段将在每次点击时将一系列数字记录到控制台(我们的临时动画)。由于我们使用了 switchMap,如果您在中间单击,之前的“动画”将停止(concatMap 部分只是在每次发射之间增加了延迟)。您可以在 https://rxmarbles.com/#switchMap

处的 switchMap 大理石图中直观地看到这一点

const { of , pipe, fromEvent, Subject } = rxjs;
const { switchMap, concatMap, delay } = rxjs.operators;

const innerComplete = new Subject();

const stream = fromEvent(document, 'click').pipe(
    switchMap(() => of(1, 2, 3).pipe(
      concatMap(x => of(x).pipe(delay(500)))
    ))
  )
  .subscribe({
    next: (x) => console.log(x),
    complete: () => console.log('outer complete')
  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.6.7/rxjs.umd.js"></script>

<p>Click here and watch the console.</p>

作用于内部可观察完成

考虑到当内部 observable 完成时外部 observable 不需要完成是有道理的,您可能想要一种在内部 observable 完成时做某事而不必完成外部 observable 的方法。当您将 Observer 作为参数传递时,tap 会让您这样做。

const { of , pipe } = rxjs;
const { switchMap, tap } = rxjs.operators;

const stream = of (1, 2, 3).pipe(
    switchMap(() => of (4, 5, 6).pipe(tap({
      complete: () => console.log("Inner observable completed")
    }))))
    .subscribe({
      next: (x) => console.log(x),
      complete: () => console.log('Outer observable completed')
    });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.6.7/rxjs.umd.js"></script>