73

当订阅中的变量更改时,为什么视图没有更新?

我有这个代码:

例子.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/rxjs 5 到 6 的更新吗?

现在我有 Angular 6.0.2 和 rxjs 6.0.0。相同的代码在 Angular 5.2 和 rxjs 5.5.10 中运行正常,无需调用detectChanges.

4

6 回答 6

101

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

编辑:似乎并不是所有的解决方案都在起作用。对于大多数用户来说,第一个解决方案“Angular Zone”可以完成这项工作。

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';
      });
      }
   );
}

根据这个答案,它会导致整个应用程序检测到更改,而您的 ChangeDetectorRef.detectChanges 方法只会检测到您的组件及其后代中的更改。

2 RxJS

另一种方法是使用rxjs更新视图。当你第一次订阅ReplaySubject时,它会给你最新的值。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);
      }
   );
}

在您看来,您可以使用异步管道订阅主题:

{{testEmitter$ | async}}

3 将代码提取到新组件

如果您将字符串提交给另一个组件,它也会被更新。您必须@Input()在新组件中使用选择器。

所以新组件的代码如下:

@Input() testVariable = '';

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

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

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

这样你就在 Angular 区域。

4 个人喜好

我个人的偏好是使用 rxjs 或者组件方式。使用detectChanges oder NGZone 对我来说感觉更hacky。

于 2018-07-04T08:52:39.240 回答
10

对我来说,以上都不起作用。然而,ChangeDetectorRef成功了。

来自角度文档;这就是你如何实现它

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

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

于 2020-03-11T15:08:10.427 回答
2

重要通知 - 科目的捷径可能会造成问题!

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

这很简单,我也没有深入研究这个问题。

但在我的情况下,问题是我使用简短的语法将主题订阅为 observable 而不是完整的,当我更改它时,它解决了这个问题。

所以虽然这不起作用:

myServiceObservableCall.subscribe(this.myBehavSubj)

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

这确实更新了视图:

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

虽然它似乎是相同的,并且上面只是 short-way syntax 。

请您解释原因

于 2020-07-07T11:15:58.750 回答
2

我发现最有效的方法是将变量赋值包装在一个setTimeout()函数中。setTimeout()Angular 的 NgZone 更改检测会在任何时候调用函数时检查更改。使用问题中的代码的示例:


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';
}
于 2021-03-22T18:10:22.090 回答
0

我来到这里是因为cdkVirtualFor没有更新数组中推送的新项目。因此,如果有人有同样的问题,问题的根源是cdkVirtualFor 只会在数组不可变更新时更新。

像这样的例子:

addNewItem(){
    let  newItem= {'name':'stack'};
    this.myItemArray = [...this.myList, newItem];
  }
于 2021-06-30T08:51:04.933 回答
-1

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

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

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

}

于 2021-04-12T17:22:38.117 回答