为什么在订阅中变量发生变化时视图没有更新?
我有这段代码:
example.component.ts
testVariable: string;
ngOnInit() {
this.testVariable = 'foo';
this.someService.someObservable.subscribe(
() => console.log('success'),
(error) => console.log('error', error),
() => {
this.testVariable += '-bar';
console.log('completed', this.testVariable);
// prints: foo-Hello-bar
}
);
this.testVariable += '-Hello';
}
example.component.html
{{testVariable}}
但视图显示: foo-Hello 。
为什么不显示: foo-Hello-bar ?
如果我在订阅中调用ChangeDetectorRef.detectChanges()
,它将显示正确的值,但为什么我必须这样做?
我不应该从每个订阅中调用此方法,或者根本不应该(angular应该处理这个)。有没有正确的方法?
我是否遗漏了Angular / rxjs 5到6的更新内容?
现在我有Angular版本6.0.2和rxjs 6.0.0。相同的代码在Angular 5.2和rxjs 5.5.10中正常工作,无需调用detectChanges
。
答案 0 :(得分:11)
据我所知,如果您在“ Angular zone”中更改数据,Angular只会更新视图。您的示例中的异步调用不符合此要求。但是,如果需要,可以将其放在Angular区域中,或使用rxjs或将部分代码提取到新组件中以解决此问题。我将解释所有内容:
1个角度区域
此服务最常见的用途是在启动包含一个或多个不需要Angular处理UI更新或错误处理的异步任务的工作时优化性能。可以通过runOutsideAngular启动此类任务,如果需要,可以通过 run 重新进入Angular区域。 https://angular.io/api/core/NgZone
关键部分是“运行”功能。您可以注入NgZone并将值更新放入NgZone对象的运行回调中:
constructor(private ngZone: NgZone ) { }
testVariable: string;
ngOnInit() {
this.testVariable = 'foo';
this.someService.someObservable.subscribe(
() => console.log('success'),
(error) => console.log('error', error),
() => {
this.ngZone.run( () => {
this.testVariable += '-bar';
});
}
);
}
根据this答案,这将导致整个应用程序检测到更改,而您的ChangeDetectorRef.detectChanges方法将仅检测组件及其后代中的更改。
2个RxJS
另一种方法是使用rxjs更新视图。首次订阅ReplaySubject时,它将为您带来最新的价值。 BehaviourSubject基本相同,但是允许您定义默认值(在您的示例中很有意义,但不一定一直都是正确的选择)。在此初始发射之后,基本上是一个正常的重放主题:
this.testVariable = 'foo';
testEmitter$ = new BehaviourSubject<string>(this.testVariable);
ngOnInit() {
this.someService.someObservable.subscribe(
() => console.log('success'),
(error) => console.log('error', error),
() => {
this.testVariable += '-bar';
this.testEmitter.next(this.testVariable);
}
);
}
您认为,您可以使用async管道订阅主题:
{{testEmitter$ | async}}
3将代码提取到新组件
如果将字符串提交给另一个组件,它也会被更新。您将必须在新组件中使用@Input()选择器。
因此,新组件具有如下代码:
@Input() testVariable = '';
并且像以前一样,在HTML中分配了testVariable并带有卷曲的刹车。
然后,您可以在父HTML视图中将父元素的变量传递给子元素:
<app-child [testVariable]="testVariable"></app-child>
这样,您将进入Angular区域。
4个人偏好
我个人的喜好是使用rxjs或组件方式。对我来说,使用detectChanges或NGZone更难受。
答案 1 :(得分:3)
对我来说,以上方法均无效。但是, ChangeDetectorRef
可以达到目的。
从Angular Docs实现的地方:
class GiantList {
constructor(private ref: ChangeDetectorRef, public dataProvider: DataListProvider) {
ref.detach();
setInterval(() => { this.ref.detectChanges(); }, 5000);
}
}
答案 2 :(得分:1)
重要提示-主题太短会造成问题!
我知道这并非采购订单要求的,但是有时可能会发生非常普遍的问题,也许对某人会有帮助。
这很简单,我也没有深入研究这个问题。
但是在我的情况下,问题是我使用了较短的语法来使主题可观察而不是全部订阅,并且当我对其进行更改时,它解决了问题。
因此,尽管无法正常工作:
myServiceObservableCall.subscribe(this.myBehavSubj)
并具有与日志记录相同的行为,我可以正确地看到更改,而只能在视图上看到。
这确实会更新视图:
myServiceObservableCall.subscribe((res)=>{
this.myBehavSubj.next(res);
} );
尽管看起来和上面的短途语法似乎相同。
请您解释原因
答案 3 :(得分:1)
我发现最有效的方法是将变量赋值包装在 setTimeout()
函数中。 Angular 的 NgZone 更改检测会在调用 setTimeout()
函数时检查更改。使用问题中的代码示例:
ngOnInit() {
this.testVariable = 'foo';
this.someService.someObservable.subscribe(
() => console.log('success'),
(error) => console.log('error', error),
() => {
setTimeout(() => { this.testVariable += '-bar'; }, 0); // Here's the line
console.log('completed', this.testVariable);
// prints: foo-Hello-bar
}
);
this.testVariable += '-Hello';
}
答案 4 :(得分:0)
我来到这里是因为 cdkVirtualFor 没有更新推送到数组中的新项目。 所以如果有人有同样的问题,问题的根源在于 cdkVirtualFor 只有当数组不可变地更新时才会更新。
例如:
addNewItem(){
let newItem= {'name':'stack'};
this.myItemArray = [...this.myList, newItem];
}
答案 5 :(得分:-1)
以上所有解决方案都很棒。但是,我发现了一个更简单的解决方案,我相信在尝试使用来自外部服务的新数据刷新 UI 时,该解决方案适用于大多数情况。
这个想法非常简单,设置一个加载标志,在获取新数据时显示微调器,然后通过重置标志来清除微调器:
getData() {
// Set loading spinner
this.loading = true;
this.service.getData().subscribe((data: any) => {
this.newData = data;
//Remove loading spinner
this.loading = false;
});
}