如何防止从 Angular 可观察订阅中触发的 API 调用重叠?

How do I prevent API calls fired from an Angular observable subscription from overlapping?

在我的 Angular 应用程序中,我有一个 ProductPageComponent,它显示我从服务器获得的产品。

我还有一个 FiltersService 存储一个可观察值,当我切换过滤器时它会改变值(例如,只显示可用产品,只显示蓝色的产品......)。在我的 ProductPageComponent 中,我已经订阅了这个可观察对象,因此过滤器中的任何更改都会触发 API 调用更新的产品列表:

// This is placed inside the component's constructor
this.filtersService.getStaticFiltersObservable()
      .pipe(
        takeUntil(this.subscriptionSubject$),
        distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))
      )
      .subscribe((filters) => {
        productService.getProductsWithFilters(filters)
          .then((products) => {
            this.productList = products;
          });
      });

这样做的问题是,如果由于某种原因过滤器可观察值的变化非常快两次,那么 API 对产品的调用就会完成两次;如果碰巧第一次调用在第二次调用之后完成(它具有最新版本的过滤器,所以它是我想要的),从第一次调用获得的产品列表将覆盖从第二.

如何防止这种情况并确保我显示的产品列表始终是最新版本?

正如评论已经表明的那样,当来自过滤器的第二个值比第一个请求完成的速度更快时,您希望使用 switchMap 运算符取消第一个请求。

它可能看起来像这样:

this.filtersService.getStaticFiltersObservable()
      .pipe(
        takeUntil(this.subscriptionSubject$),
        distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
        switchMap((filters) => from(productService.getProductsWithFilters(filters)))
      )
      .subscribe((products) => {
           this.productList = products;
      });

一种可接受的方法是使用 debounceTime 运算符,如果您试图忽略的过滤器中的更改非常快(例如,一个问题它们之间的毫秒数):

this.filtersService.getStaticFiltersObservable()
  .pipe(
    debounceTime(300),
    takeUntil(this.subscriptionSubject$)
  )
  .subscribe((filters) => {
    this.productService.getProductsWithFilters(filters)
      .then((products) => {
        this.productList = products;
      });
    });

作为注释,在 99% 的情况下,takeUntil(...) 运算符应该是 pipe 中的最后一个运算符,否则,它不会取消其他运算符在 pipe(...)链。在极少数情况下,您希望在 pipe(...).

中的运算符之间使用它

更好的方法

看着你的代码,在我看来你是 Angular 和整个 observables 世界的新手。更好的方法(“Angular 方式”)是:

// This goes on the declaration, not in the constructor,
// to keep the constructor code cleaner
productList$ = this.filtersService.getStaticFiltersObservable()
  .pipe(
    debounceTime(300),
    switchMap((filters) => from(this.productService.getProductsWithFilters(filters))
    takeUntil(this.subscriptionSubject$)
  );

...

constructor(
  private productService: ProductService,
  private filterService: FilterService
) {}

在您的模板中,您可以这样使用 productList$

<ul>
  <li *ngFor="let p of productsList$ | async">{{p.name}}</li>
</ul>

如果您可以访问 products 服务并且可以将其返回值转化为可观察的而不是承诺,那就更好了。它将节省我们上面使用的 from(...) 运算符来包装对 getProductsWithFilters(...) 方法的调用。

此外,即使 switchMap(...) 可以单独完成,我仍将 debouncTime(...) 用于此用例,因为在我看来它优化了带宽的使用 的服务器资源,通过防止早期请求开始(无论如何以后都会被忽略,因为 switchMap 在订阅一个新的可观察对象时会中止之前正在进行的订阅 - 是的,我知道这听起来很混乱 :D).

在 Angular 14 中(并且可能在更大的版本中)

仅供参考,Angular 14 版本带来了一种注入服务的新方法:inject(...) 函数。在撰写本文时,我会说您可以跟进新闻以了解如何以最佳方式使用它,但我不会立即开始在生产代码中使用它。它只是 Angular 引入的一种新的 DI,并不是要反对构造函数注入。有些人只是认为 inject(...) 函数保持代码干净并引入 new possibilities.

// Instead of injecting in the constructor, we can
// inject the services by using the inject(...) function
// *only* in the declaration part of a class attribute
productService = inject(ProductService);
filterService =  inject(FilterService);

productList$ = this.filtersService.getStaticFiltersObservable()
  .pipe(
    debounceTime(300),
    switchMap((filters) => from(this.productService.getProductsWithFilters(filters))
    takeUntil(this.subscriptionSubject$)
  );
<ul>
  <li *ngFor="let p of productsList$ | async">{{p.name}}</li>
</ul>