Angular 2-模型更改后视图未更新


128

我有一个简单的组件,该组件每隔几秒钟调用一次REST API,并接收一些JSON数据。从我的日志语句和网络流量中,我可以看到返回的JSON数据正在更改,并且我的模型正在更新,但是视图没有更改。

我的组件看起来像:

import {Component, OnInit} from 'angular2/core';
import {RecentDetectionService} from '../services/recentdetection.service';
import {RecentDetection} from '../model/recentdetection';
import {Observable} from 'rxjs/Rx';

@Component({
    selector: 'recent-detections',
    templateUrl: '/app/components/recentdetection.template.html',
    providers: [RecentDetectionService]
})



export class RecentDetectionComponent implements OnInit {

    recentDetections: Array<RecentDetection>;

    constructor(private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { this.recentDetections = recent;
             console.log(this.recentDetections[0].macAddress) });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        timer.subscribe(() => this.getRecentDetections());
    }
}

而且我的观点看起来像:

<div class="panel panel-default">
    <!-- Default panel contents -->
    <div class="panel-heading"><h3>Recently detected</h3></div>
    <div class="panel-body">
        <p>Recently detected devices</p>
    </div>

    <!-- Table -->
    <table class="table" style="table-layout: fixed;  word-wrap: break-word;">
        <thead>
            <tr>
                <th>Id</th>
                <th>Vendor</th>
                <th>Time</th>
                <th>Mac</th>
            </tr>
        </thead>
        <tbody  >
            <tr *ngFor="#detected of recentDetections">
                <td>{{detected.broadcastId}}</td>
                <td>{{detected.vendor}}</td>
                <td>{{detected.timeStamp | date:'yyyy-MM-dd HH:mm:ss'}}</td>
                <td>{{detected.macAddress}}</td>
            </tr>
        </tbody>
    </table>
</div>

从结果中可以看出console.log(this.recentDetections[0].macAddress),recentlyDetections对象正在更新,但是视图中的表永远不会改变,除非重新加载页面。

我正在努力查看我在做什么错。有人可以帮忙吗?


我建议使代码更简洁,更简单:stackoverflow.com/a/48873980/571030
glukki

Answers:


214

服务中的代码可能会以某种方式超出Angular的区域。这会中断更改检测。这应该工作:

import {Component, OnInit, NgZone} from 'angular2/core';

export class RecentDetectionComponent implements OnInit {

    recentDetections: Array<RecentDetection>;

    constructor(private zone:NgZone, // <== added
        private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { 
                 this.zone.run(() => { // <== added
                     this.recentDetections = recent;
                     console.log(this.recentDetections[0].macAddress) 
                 });
        });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        timer.subscribe(() => this.getRecentDetections());
    }
}

有关调用变更检测的其他方法,请参见在Angular中手动触发变更检测

调用变更检测的替代方法是

ChangeDetectorRef.detectChanges()

立即对当前组件及其子组件运行更改检测

ChangeDetectorRef.markForCheck()

下次包含Angular运行变化检测时包含当前组件

ApplicationRef.tick()

为整个应用程序运行更改检测


9
另一种选择是注入ChangeDetectorRef并调用cdRef.detectChanges()代替zone.run()。这可能会更有效率,因为它不会像在整个组件树上那样运行更改检测zone.run()
Mark Rajcok '16

1
同意。仅detectChanges()当您进行的任何更改在组件及其后代本地时才使用。我的理解是,detectChanges()将检查组件及其后代,但zone.run()ApplicationRef.tick()会检查整个组件树。
Mark Rajcok '16

2
正是我想要的..使用@Input()没有意义也没有用。
yorch '16

15
我真的不明白为什么我们需要所有这些手册,或者为什么让angular2众神如此必要。为什么@Input并不意味着该组件实际上确实依赖于父组件所说的依赖于其更改数据的任何内容?似乎不仅行之有效,而且非常优雅。我想念什么?

13
为我工作。但这是另一个例子,为什么我觉得有角度的框架开发人员会迷失在有角度的框架的复杂性中。当将angular用作应用程序开发人员时,您必须花费大量时间来了解angular的功能或工作方式,以及如何解决上述问题。太糟糕了!!
卡尔

32

它最初是@Mark Rajcok的评论中的答案,但我想将其作为经过测试并用作使用ChangeDetectorRef的解决方案的地方,在这里看到一个好处:

另一种选择是注入ChangeDetectorRef和调用 cdRef.detectChanges()而不是zone.run()。这可能会更有效率,因为它不会像在整个组件树上那样运行更改检测zone.run()。– Mark Rajcok

因此代码必须类似于:

import {Component, OnInit, ChangeDetectorRef} from 'angular2/core';

export class RecentDetectionComponent implements OnInit {

    recentDetections: Array<RecentDetection>;

    constructor(private cdRef: ChangeDetectorRef, // <== added
        private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { 
                this.recentDetections = recent;
                console.log(this.recentDetections[0].macAddress);
                this.cdRef.detectChanges(); // <== added
            });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        timer.subscribe(() => this.getRecentDetections());
    }
}

编辑.detectChanges()在内部子目录中使用可能会导致尝试使用被破坏的视图的问题:detectChanges

为了解决这个问题,您需要unsubscribe在销毁组件之前,所以完整的代码将如下所示:

import {Component, OnInit, ChangeDetectorRef, OnDestroy} from 'angular2/core';

export class RecentDetectionComponent implements OnInit, OnDestroy {

    recentDetections: Array<RecentDetection>;
    private timerObserver: Subscription;

    constructor(private cdRef: ChangeDetectorRef, // <== added
        private recentDetectionService: RecentDetectionService) {
        this.recentDetections = new Array<RecentDetection>();
    }

    getRecentDetections(): void {
        this.recentDetectionService.getJsonFromApi()
            .subscribe(recent => { 
                this.recentDetections = recent;
                console.log(this.recentDetections[0].macAddress);
                this.cdRef.detectChanges(); // <== added
            });
    }

    ngOnInit() {
        this.getRecentDetections();
        let timer = Observable.timer(2000, 5000);
        this.timerObserver = timer.subscribe(() => this.getRecentDetections());
    }

    ngOnDestroy() {
        this.timerObserver.unsubscribe();
    }

}

this.cdRef.detectChanges(); 解决了我的问题。谢谢!
mangesh

感谢您的建议,但是在多次尝试通话后出现错误:failure:Error:ViewDestroyedError:尝试使用被破坏的视图:detectChanges对我来说zone.run()帮助我摆脱了这个错误。
shivam srivastava

8

尝试使用 @Input() recentDetections: Array<RecentDetection>;

编辑: 之所以@Input()重要很重要,是因为您要将Typescript / javascript文件中的值绑定到视图(html)。如果用@Input()装饰器声明的值发生更改,则视图将自动更新。如果更改了装饰器@Input()@Output()装饰器,ngOnChanges则会触发-event,并且视图将使用新值进行更新。您可能会说@Input()将双向绑定值。

通过角度搜索此链接上的输入:词汇表以获取更多信息

编辑: 在学习了有关Angular 2开发的更多信息之后,我认为做一个@Input()真正的解决方案不是真正的解决方案,正如评论中所述,

@Input() 仅适用于从组件外部通过数据绑定更改数据的情况(更改了父项中的绑定数据),不适用于从组件内部的代码更改数据的情况。

如果您看一下@Günter的答案,它是解决此问题的更准确和正确的方法。我仍然会在此处保留此答案,但请遵循昆特的答案作为正确答案。


2
就是这样 我之前看过@Input()批注,但始终将其与表单输入相关联,从没想过我在这里需要它。干杯。
马特·沃森

1
@John感谢您的补充解释。但是添加的内容仅适用于通过组件外部的数据绑定更改数据的情况(更改了父项中的绑定数据),而不适用于通过组件中的代码更改数据的情况。
君特Zöchbauer

使用@Input()关键字时,会附加一个属性绑定,该绑定可确保在视图中更新值。该值将成为生命周期事件的一部分。这篇文章包含有关一些信息@Input()键字
约翰

我放弃 我看不到此处@Input()涉及的内容或原因,并且该问题未提供足够的信息。感谢您的耐心反正:)
君特Zöchbauer

3
@Input()更能解决问题的根源。问题必须与区域和变更检测有关。
magiccrafter

2

就我而言,我有一个非常相似的问题。我正在更新由父组件调用的函数中的视图,而在父组件中,我忘记了使用@ViewChild(NameOfMyChieldComponent)。我仅仅因为这个愚蠢的错误而损失了至少3个小时。即:我不需要使用任何这些方法:

  • ChangeDetectorRef.detectChanges()
  • ChangeDetectorRef.markForCheck()
  • ApplicationRef.tick()

1
您能解释一下如何使用@ViewChild更新子组件吗?谢谢
Lucas Colombo '18

我怀疑父母正在调用一个未渲染的子类方法,该方法仅存在于内存中
glukki

1

与其处理区域和更改检测,不如让AsyncPipe处理复杂性。这样可以观察到订阅,取消订阅(以防止内存泄漏)并更改Angular肩上的检测。

将您的类更改为可观察的,将发出新请求的结果:

export class RecentDetectionComponent implements OnInit {

    recentDetections$: Observable<Array<RecentDetection>>;

    constructor(private recentDetectionService: RecentDetectionService) {
    }

    ngOnInit() {
        this.recentDetections$ = Observable.interval(5000)
            .exhaustMap(() => this.recentDetectionService.getJsonFromApi())
            .do(recent => console.log(recent[0].macAddress));
    }
}

并更新您的视图以使用AsyncPipe:

<tr *ngFor="let detected of recentDetections$ | async">
    ...
</tr>

要补充一点,最好是使用带有interval参数的方法来提供服务,并且:

  • 创建新请求(通过使用exhaustMap上面的代码中的like);
  • 处理请求错误;
  • 阻止浏览器离线时发出新请求。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.