假设我有两个可观察器要被模板消耗,一个是数据,另一个是加载指示器:
public readonly data$ = this.backend.get<Data>(...).pipe(
finalize((
{
this.isLoadingSubject$.next(1);
return () => this.isLoadingSubject$.next(-1);
})()
);
private readonly isLoadingSubject$ = new Subject<number>();
public readonly isLoading$ = isLoadingSubject$.pipe(
scan((acc, value) => acc + value, 0),
map((i) => i > 0),
);
关于finalize
的说明:我使用IIFE在data$
的管道被触发时开始加载,并在它完成时结束加载。我使用的是一个递增或递减的数字,而不是布尔值,因为可能有多个同时的请求(isLoading$
机制被许多可观察的管道使用(。
在我的模板中,我这样使用它:
<ng-container *ngIf="data$ | async as data">
{{ data }}
<button [disabled]="isLoading$ | async">some button</button>
</ng-container>
问题是对isLoading$
的订阅很晚:此订阅只在data$
发出后发生,而第一个.next(+1)
由于没有订阅而被忽略。
如何优雅地解决此问题?
我尝试过的解决方法,但我不喜欢:
- 立即订阅
isLoading$
以使其变热似乎很浪费,而且在阅读代码时,不清楚为什么要这样做。正因为如此,如果仅从代码中不清楚它的用途,这似乎是一个糟糕的解决方案 - 重新排列模板,使
isLoading$
位于第一个<ng-container>
中,然后data$
位于第二个<ng-container>
中,但当加载为false
时,我必须处理*ngIf
不渲染模板的问题,因此我必须将其封装在对象中,这似乎又是浪费。而且,这会导致每次加载切换时都会重新渲染所有内容,这很愚蠢 - 查看了
publishReplay()
运算符,但该运算符已被弃用 - 将
data$
和isLoading$
封装在同一个<ng-container>
内的对象中,但每当加载指示符发生变化时,整个模板都会被重新渲染,这是非常浪费的——我只想禁用一个按钮
我认为处理此问题最干净的方法是使用您的"工作区#4";;将两段数据放在一个对象中(又名视图模型。Sander Elias在本视频中解释了这种模式(:
const INITIAL_STATE = {
data : undefined,
isLoading : false,
};
@Component()
class MyComponent {
private isLoadingSubject$ = new Subject<number>();
private isLoading$ = isLoadingSubject$.pipe(
scan((acc, value) => acc + value, 0),
map(i => i > 0),
);
public vm$ = combineLatest({
data : this.getData() .pipe(startWith(INITIAL_STATE.data)),
isLoading : this.isLoading$ .pipe(startWith(INITIAL_STATE.isLoading))
});
private getData() {
this.isLoadingSubject.next(1);
return this.backend.get<Data>(...).pipe(
finalize(() => this.isLoadingSubject.next(-1))
);
}
}
<ng-container *ngIf="vm$ | async as vm">
{{ vm.data }}
<button [disabled]="vm.isLoading">some button</button>
</ng-container>
无需担心:
,但每当加载指示器更改时,整个模板都会重新呈现
在前面提到的视频的这一部分提到了这个问题。
当组合视图模型的任何部分发生更改时,确实会在组件上运行更改检测,但只有实际发生更改的元素才会被重新绘制;因此,如果只有isLoading
属性发生更改,则UI中只有按钮会更新。与data
的绑定不会受到影响,因为它没有更改。