Angular 2 - มุมมองไม่อัปเดตหลังจากเปลี่ยนโมเดล


129

ฉันมีส่วนประกอบง่ายๆที่เรียก 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)อ็อบเจ็กต์ latestDetections ที่กำลังอัปเดต แต่ตารางในมุมมองไม่เคยเปลี่ยนแปลงเว้นแต่ฉันจะโหลดเพจซ้ำ

ฉันกำลังดิ้นรนเพื่อดูว่าฉันทำอะไรผิดที่นี่ ใครสามารถช่วย?


ฉันแนะนำให้สร้างโค้ดให้สะอาดและซับซ้อนน้อยลง: stackoverflow.com/a/48873980/571030
glukki

คำตอบ:


216

อาจเป็นไปได้ว่าโค้ดในบริการของคุณแตกออกจากโซนของ 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

1
ตกลง. ใช้เฉพาะในdetectChanges()กรณีที่การเปลี่ยนแปลงใด ๆ ที่คุณทำเป็นแบบโลคัลของคอมโพเนนต์นั้น ความเข้าใจของฉันคือการที่detectChanges()จะตรวจสอบส่วนประกอบและลูกหลานของมัน แต่zone.run()และApplicationRef.tick()จะตรวจสอบต้นไม้องค์ประกอบทั้งหมด
Mark Rajcok

2
สิ่งที่ฉันกำลังมองหา .. การใช้@Input()ไม่สมเหตุสมผลหรือไม่ได้ผล

15
ฉันไม่เข้าใจจริงๆว่าทำไมเราต้องมีคู่มือทั้งหมดนี้หรือทำไมเทพ angular2 ถึงจำเป็น เหตุใด @Input จึงไม่ได้หมายความว่าส่วนประกอบนั้นขึ้นอยู่กับสิ่งที่องค์ประกอบหลักกล่าวว่าขึ้นอยู่กับข้อมูลที่เปลี่ยนแปลง ดูเหมือนไม่สง่างามอย่างยิ่งที่มันไม่เพียง ฉันขาดอะไรไป?

13
ทำงานให้ฉัน แต่นี่เป็นอีกตัวอย่างหนึ่งว่าทำไมฉันถึงรู้สึกว่านักพัฒนาเฟรมเวิร์กเชิงมุมหลงอยู่ในความซับซ้อนของกรอบเชิงมุม เมื่อใช้เชิงมุมเป็นนักพัฒนาแอปพลิเคชันคุณต้องใช้เวลามากในการทำความเข้าใจว่าเชิงมุมทำสิ่งนี้หรือทำสิ่งนั้นได้อย่างไรและจะแก้ปัญหาอย่างไรในสิ่งที่สงสัยข้างต้น Terrrible !!
Karl

32

เดิมเป็นคำตอบในความคิดเห็นจาก @Mark Rajcok แต่ฉันต้องการวางไว้ที่นี่เพื่อทดสอบและทำงานเป็นโซลูชันโดยใช้ChangeDetectorRefฉันเห็นจุดที่ดีที่นี่:

อีกทางเลือกหนึ่งคือการฉีด ChangeDetectorRefและเรียก แทนcdRef.detectChanges() zone.run()สิ่งนี้อาจมีประสิทธิภาพมากกว่าเนื่องจากจะไม่เรียกใช้การตรวจจับการเปลี่ยนแปลงในโครงสร้างส่วนประกอบทั้งหมดเหมือนที่zone.run()ทำ - มาร์ค 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()ภายใน subscibe อาจทำให้เกิดปัญหาได้พยายามใช้มุมมองที่ถูกทำลาย: 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 () ช่วยให้ฉันออกจากข้อผิดพลาดนี้
shivam srivastava

8

ลองนำไปใช้ @Input() recentDetections: Array<RecentDetection>;

แก้ไข: สาเหตุที่@Input()สำคัญเนื่องจากคุณต้องการผูกค่าในไฟล์ typescript / javascript กับมุมมอง (html) มุมมองจะอัปเดตเองหากมีการเปลี่ยนแปลงค่าที่ประกาศด้วย@Input()มัณฑนากร หากมีการเปลี่ยนแปลง@Input()หรือ@Output()มัณฑนากรngOnChanges-event จะทริกเกอร์และมุมมองจะอัปเดตตัวเองด้วยค่าใหม่ คุณอาจพูดได้ว่า@Input()จะผูกค่าสองทาง

ค้นหาอินพุตในลิงค์นี้ตามเชิงมุม: อภิธานศัพท์สำหรับข้อมูลเพิ่มเติม

แก้ไข: หลังจากเรียนรู้เพิ่มเติมเกี่ยวกับการพัฒนา Angular 2 ฉันพบว่าการทำ@Input()จริงไม่ใช่วิธีแก้ปัญหาและตามที่กล่าวไว้ในความคิดเห็น

@Input() ใช้เฉพาะเมื่อข้อมูลมีการเปลี่ยนแปลงโดยการผูกข้อมูลจากภายนอกส่วนประกอบ (ข้อมูลที่ถูกผูกไว้ในพาเรนต์เปลี่ยนไป) ไม่ใช่เมื่อข้อมูลถูกเปลี่ยนจากโค้ดภายในคอมโพเนนต์

หากคุณได้ดูคำตอบของ@Günterมันเป็นวิธีแก้ปัญหาที่ถูกต้องและถูกต้องกว่า ฉันจะยังคงเก็บคำตอบนี้ไว้ที่นี่ แต่โปรดปฏิบัติตามคำตอบของGünterว่าเป็นคำตอบที่ถูกต้อง


2
นั่นแหล่ะ ฉันเคยเห็นคำอธิบายประกอบ @Input () มาก่อน แต่เชื่อมโยงกับอินพุตแบบฟอร์มเสมอไม่เคยคิดว่าจะต้องใช้ที่นี่ ไชโย
Matt Watson

1
@ จอห์นขอบคุณสำหรับคำอธิบายเพิ่มเติม แต่สิ่งที่คุณเพิ่มจะมีผลเฉพาะเมื่อข้อมูลถูกเปลี่ยนแปลงโดยการผูกข้อมูลจากภายนอกคอมโพเนนต์ (ข้อมูลที่ถูกผูกไว้ในพาเรนต์เปลี่ยนไป) ไม่ใช่เมื่อข้อมูลถูกเปลี่ยนจากโค้ดภายในคอมโพเนนต์
GünterZöchbauer

มีการผูกคุณสมบัติที่แนบมาเมื่อใช้@Input()คำสำคัญและการเชื่อมโยงนั้นทำให้แน่ใจว่ามีการอัพเดตค่าในมุมมอง ค่าจะเป็นส่วนหนึ่งของเหตุการณ์วงจรชีวิต โพสต์นี้มีข้อมูลเพิ่มเติมเกี่ยวกับคำ@Input()สำคัญ
จอห์น

ฉันจะยอมแพ้ ฉันไม่เห็นว่ามี@Input()ส่วนเกี่ยวข้องตรงไหนหรือทำไมจึงควรเป็นและคำถามก็ให้ข้อมูลไม่เพียงพอ ขอบคุณที่อดทนรอ :)
GünterZöchbauer

3
@Input()เป็นวิธีแก้ปัญหาที่ซ่อนต้นตอของปัญหามากกว่า ปัญหาจะต้องเกี่ยวข้องกับโซนและการตรวจจับการเปลี่ยนแปลง
magiccrafter

2

ในกรณีของฉันฉันมีปัญหาที่คล้ายกันมาก ฉันกำลังอัปเดตมุมมองของฉันภายในฟังก์ชันที่ถูกเรียกโดยคอมโพเนนต์หลักและในองค์ประกอบหลักของฉันฉันลืมใช้ @ViewChild (NameOfMyChieldComponent) ฉันเสียเวลาอย่างน้อย 3 ชั่วโมงสำหรับความผิดพลาดโง่ ๆ นี้ กล่าวคือฉันไม่จำเป็นต้องใช้วิธีการใด ๆ เหล่านี้:

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

1
คุณช่วยอธิบายได้ไหมว่าคุณอัปเดตองค์ประกอบลูกของคุณกับ @ViewChild ได้อย่างไร ขอบคุณ
Lucas Colombo

ฉันสงสัยว่าผู้ปกครองเรียกเมธอดของคลาสย่อยที่ไม่ได้แสดงผลและมีอยู่ในหน่วยความจำเท่านั้น
glukki

1

แทนที่จะจัดการกับโซนและเปลี่ยนแปลงการตรวจจับให้ AsyncPipe จัดการกับความซับซ้อน สิ่งนี้จะทำให้การสมัครสมาชิกที่สังเกตได้การยกเลิกการสมัคร (เพื่อป้องกันการรั่วไหลของหน่วยความจำ) และการตรวจจับการเปลี่ยนแปลงบนไหล่เชิงมุม

เปลี่ยนชั้นเรียนของคุณเพื่อให้สังเกตได้ซึ่งจะส่งผลลัพธ์ของคำขอใหม่:

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อาร์กิวเมนต์และ:

  • สร้างคำขอใหม่ (โดยใช้exhaustMaplike ในโค้ดด้านบน);
  • จัดการข้อผิดพลาดการร้องขอ
  • หยุดเบราว์เซอร์ไม่ให้ส่งคำขอใหม่ขณะออฟไลน์
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.