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

Posted

技术标签:

【中文标题】Angular 2 - 模型更改后视图不更新【英文标题】:Angular 2 - View not updating after model changes 【发布时间】:2016-08-23 11:55:43 【问题描述】:

我有一个简单的组件,它每隔几秒调用一次 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) 的结果中看到,recentDetections 对象正在更新,但除非我重新加载页面,否则视图中的表格永远不会改变。

我很难看出我在这里做错了什么。有人可以帮忙吗?

【问题讨论】:

我建议让代码更干净、更简单:***.com/a/48873980/571030 【参考方案1】:

可能是您服务中的代码以某种方式超出了 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());
    

有关调用更改检测的其他方法,请参阅Triggering change detection manually in Angular

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

ChangeDetectorRef.detectChanges()

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

ChangeDetectorRef.markForCheck()

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

ApplicationRef.tick()

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

【讨论】:

另一种方法是注入 ChangeDetectorRef 并调用 cdRef.detectChanges() 而不是 zone.run()。这可能会更有效,因为它不会像 zone.run() 那样对整个组件树进行更改检测。 同意。如果您所做的任何更改是组件及其后代的本地更改,请仅使用 detectChanges()。我的理解是detectChanges()会检查组件及其后代,但zone.run()ApplicationRef.tick()会检查整个组件树。 正是我想要的......使用@Input()没有意义也没有工作。 我真的不明白为什么我们需要所有这些手动的东西,或者为什么 angular2 大神留下了这些必要的东西。为什么@Input 不意味着该组件真的依赖于父组件所说的依赖于其不断变化的数据的任何内容?它不仅可以工作,这似乎非常不雅。我错过了什么? 为我工作。但这是另一个例子,为什么我觉得 Angular 框架开发人员迷失在 Angular 框架的复杂性中。当使用 Angular 作为应用程序开发人员时,您必须花费大量时间来了解 Angular 是如何做到这一点或如何做到这一点的,以及如何解决上述问题。太可怕了!【参考方案2】:

它最初是来自 @Mark Rajcok 的 cmets 中的一个答案,但我想把它放在这里作为测试并使用 ChangeDetectorRef 作为解决方案,我在这里看到了一个好点:

另一种选择是注入ChangeDetectorRef 并调用 cdRef.detectChanges() 而不是 zone.run()。这可能更多 高效,因为它不会在整个过程中运行更改检测 像zone.run() 这样的组件树。 ——马克·拉科克

所以代码必须是这样的:

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());
    
 

编辑: 在 subscibe 中使用 .detectChanges() 可能会导致问题 Attempt to use a destroyed view: 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();解决了我的问题。谢谢! 感谢您的建议,但在多次尝试调用后我得到错误:失败:错误:ViewDestroyedError:尝试使用已破坏的视图:detectChanges 对于我来说 zone.run() 帮助我摆脱了这个错误.【参考方案3】:

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

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

【讨论】:

您能解释一下您是如何使用@ViewChild 更新您的子组件的吗?谢谢 我怀疑,父类调用的子类的方法没有被渲染,只存在于内存中【参考方案4】:

而不是处理区域和更改检测 - 让 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,如上面的代码); 处理请求错误; 阻止浏览器在离线时发出新请求。

【讨论】:

【参考方案5】:

我知道这是一个老问题,但我想分享我的情况。我在一个团队中工作,有人将更改检测策略设置为 onPush 基本上禁用自动更改检测。不确定这是否有帮助,但以防万一

@Component(
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
)

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

【讨论】:

以上是关于Angular 2 - 模型更改后视图不更新的主要内容,如果未能解决你的问题,请参考以下文章

在Angular中更改模型后如何更新视图?

方向更改后视图不更新

Angular2:变量更改时视图不更新

Angular 2 - 在 Promise 中更改模型时 GUI 不会更新

检查 Angular SPA 视图更改的缓存清单更新

Angular 5 - 在输入更改时使用字符串替换更新我的模型