Angular 6 在订阅中更改变量后视图未更新

Angular 6 View is not updated after changing a variable within subscribe

为什么订阅中的变量发生变化时视图没有更新?

我有这个代码:

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.

据我所知,如果您更改 "Angular zone" 中的数据,Angular 只会更新视图。您示例中的异步调用不符合此条件。但是如果你愿意,你可以把它放在 Angular 区域或者使用 rxjs 或者提取部分代码到一个新的组件来解决这个问题。我会全部解释:

编辑:似乎并非所有解决方案都有效。对于大多数用户来说,第一个解决方案 "Angular Zone" 就可以了。

1 Angular 区域

The most common use of this service is to optimize performance when starting a work consisting of one or more asynchronous tasks that don't require UI updates or error handling to be handled by Angular. Such tasks can be kicked off via runOutsideAngular and if needed, these tasks can reenter the Angular zone via run. https://angular.io/api/core/NgZone

关键部分是"run"函数。您可以注入 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, it will give you the latest value. A BehaviorSubject 时基本相同,但允许您定义默认值(在您的示例中有意义,但不一定始终是正确的选择)。在这个初始发射之后,它基本上是一个正常的重播主题:

this.testVariable = 'foo';
testEmitter$ = new BehaviorSubject<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 = '';

并且 testVariable 像以前一样在 HTML 中分配,带有花括号。

在父 HTML 视图中,您可以将父元素的变量传递给子元素:

<app-child [testVariable]="testVariable"></app-child>

这样你就在 Angular 区域了。

4个人喜好

我个人比较喜欢使用rxjs或者组件方式。使用 detectChanges 或 NGZone 对我来说感觉更 hacky。

对我来说 none 以上的方法都有效。然而,ChangeDetectorRef 成功了。

来自 angular 文档;这就是实现它的方法

    class GiantList {
      constructor(private ref: ChangeDetectorRef, public dataProvider: DataListProvider) {
        ref.detach();
        setInterval(() => { this.ref.detectChanges(); }, 5000);
      }
    }

https://angular.io/api/core/ChangeDetectorRef

重要提示 - 科目走短会造成问题!

我知道这不是 PO 所要求的,但有时会发生非常普遍的问题,也许它会对某人有所帮助。

很简单,而且我也没有深入考察问题。

但在我的例子中,问题是我使用了简短的语法来将主题订阅到 observable 而不是完整的,当我更改它时,它解决了问题。

所以虽然这个 没有用 :

myServiceObservableCall.subscribe(this.myBehavSubj)

并且使用与日志相同的行为,我正确地看到了更改,只是在视图上没有。

这确实更新了视图:

myServiceObservableCall.subscribe((res)=>{
        this.myBehavSubj.next(res);
      } );

虽然看起来是一样的,而且上面只是简写语法。

请您说明原因

我发现最有效的方法是将变量赋值包装在 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';
}

以上所有解决方案都很棒。但是,我发现了一个更简单的解决方案,我相信它适用于尝试使用来自外部服务的新数据刷新 UI 的大多数情况。

想法很简单,设置一个加载标志,在获取新数据时显示微调器,然后通过重置标志清除微调器:

getData() {
 // Set loading spinner
 this.loading = true;
 this.service.getData().subscribe((data: any) => {
   this.newData = data;
   //Remove loading spinner
   this.loading = false;
 });

}

我来这里是因为 cdkVirtualFor 没有用数组中推送的新项目进行更新。 所以如果有人有同样的问题,问题的根源是 cdkVirtualFor 只有当数组被不可变地更新时才会被更新。

例如这样:

addNewItem(){
    let  newItem= {'name':'stack'};
    this.myItemArray = [...this.myList, newItem];
  }