如何防止从 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>
在我的 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>