Angular 2 - Xem không cập nhật sau khi thay đổi mô hình


128

Tôi có một thành phần đơn giản gọi api REST mỗi vài giây và nhận lại một số dữ liệu JSON. Tôi có thể thấy từ các báo cáo nhật ký của mình và lưu lượng truy cập mạng rằng dữ liệu JSON được trả về đang thay đổi và mô hình của tôi đang được cập nhật, tuy nhiên, chế độ xem không thay đổi.

Thành phần của tôi trông như:

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

Và quan điểm của tôi trông như:

<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>

Tôi có thể thấy từ kết quả của console.log(this.recentDetections[0].macAddress)đối tượng RecentDetections đang được cập nhật, nhưng bảng trong dạng xem không bao giờ thay đổi trừ khi tôi tải lại trang.

Tôi đang đấu tranh để xem những gì tôi đang làm sai ở đây. Có ai giúp được không?


Tôi khuyên bạn nên làm cho mã sạch hơn và ít phức tạp hơn: stackoverflow.com/a/48873980/571030
glukki

Câu trả lời:


214

Có thể là mã trong dịch vụ của bạn bằng cách nào đó thoát ra khỏi vùng của Angular. Điều này phá vỡ phát hiện thay đổi. Điều này sẽ làm việc:

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

Để biết các cách khác để gọi phát hiện thay đổi, hãy xem Phát hiện thay đổi kích hoạt theo cách thủ công trong Angular

Các cách khác để gọi phát hiện thay đổi là

ChangeDetectorRef.detectChanges()

để ngay lập tức chạy phát hiện thay đổi cho thành phần hiện tại và con của nó

ChangeDetectorRef.markForCheck()

để bao gồm thành phần hiện tại vào lần tiếp theo Angular chạy phát hiện thay đổi

ApplicationRef.tick()

để chạy phát hiện thay đổi cho toàn bộ ứng dụng


9
Một cách khác là tiêm ChangeDetectorRef và gọi cdRef.detectChanges()thay vì zone.run(). Điều này có thể hiệu quả hơn, vì nó sẽ không chạy phát hiện thay đổi trên toàn bộ cây thành phần như thế zone.run().
Mark Rajcok

1
Đồng ý. Chỉ sử dụng detectChanges()nếu bất kỳ thay đổi nào bạn thực hiện là cục bộ đối với thành phần và hậu duệ của nó. Sự hiểu biết của tôi là detectChanges()sẽ kiểm tra các thành phần và các hậu duệ của nó, nhưng zone.run()ApplicationRef.tick()sẽ kiểm tra toàn bộ cây thành phần.
Mark Rajcok

2
chính xác những gì tôi đang tìm kiếm .. sử dụng @Input()không có ý nghĩa cũng không hoạt động.
vào

15
Tôi thực sự không hiểu tại sao chúng ta cần tất cả những thứ thủ công này hoặc tại sao các vị thần angular2 lại cần thiết như vậy. Tại sao @Input không có nghĩa là thành phần đó thực sự phụ thuộc vào bất cứ điều gì mà thành phần cha mẹ nói phụ thuộc vào dữ liệu thay đổi của nó? Có vẻ như vô cùng thiếu sót khi nó không chỉ hoạt động. Tôi đang thiếu gì?

13
Đã làm cho tôi. Nhưng đây là một ví dụ khác tại sao tôi có cảm giác các nhà phát triển khung góc bị mất trong sự phức tạp của khung góc. Khi sử dụng angular như một nhà phát triển ứng dụng, bạn phải dành rất nhiều thời gian để hiểu làm thế nào angular làm điều này hoặc làm điều đó và làm thế nào để giải quyết những thứ như câu hỏi ở trên. Kinh khủng !!
Karl

32

Ban đầu nó là một câu trả lời trong các bình luận từ @Mark Rajcok, nhưng tôi muốn đặt nó ở đây dưới dạng thử nghiệm và hoạt động như một giải pháp sử dụng ChangeDetectorRef , tôi thấy một điểm tốt ở đây:

Một cách khác là tiêm ChangeDetectorRefvà gọi cdRef.detectChanges()thay vì zone.run(). Điều này có thể hiệu quả hơn, vì nó sẽ không chạy phát hiện thay đổi trên toàn bộ cây thành phần như thế zone.run(). - Mark Rajcok

Vì vậy, mã phải giống như:

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

Chỉnh sửa : Sử dụng .detectChanges()bên trong subscibe có thể dẫn đến sự cố Cố gắng sử dụng chế độ xem bị hủy: DetChanges

Để giải quyết nó, bạn cần phải unsubscribehủy trước khi hủy thành phần, vì vậy mã đầy đủ sẽ như sau:

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 (); đã khắc phục sự cố của tôi. Cảm ơn bạn!
mangesh

Cảm ơn lời đề nghị của bạn nhưng sau nhiều lần gọi, tôi gặp lỗi: fail: Error: ViewDestroyedError: Cố gắng sử dụng chế độ xem bị hủy: DetChanges For me zone.run () giúp tôi thoát khỏi lỗi này.
shivam srivastava

8

Thử sử dụng @Input() recentDetections: Array<RecentDetection>;

EDIT: Lý do tại sao@Input()quan trọng là vì bạn muốn liên kết giá trị trong tệp typcript / javascript vào chế độ xem (html). Khung nhìn sẽ tự cập nhật nếu một giá trị được khai báo với trình@Input()trang trí bị thay đổi. Nếu một@Input()hoặc@Output()trang trí được thay đổi, mộtngOnChanges-event sẽ kích hoạt và chế độ xem sẽ tự cập nhật với giá trị mới. Bạn có thể nói rằng ý@Input()chí hai chiều ràng buộc giá trị.

tìm kiếm Đầu vào trên liên kết này theo góc: bảng chú giải để biết thêm thông tin

EDIT: sau khi tìm hiểu thêm về sự phát triển của Angular 2, tôi đã hình dung ra rằng@Input()thực sự không phải là giải pháp và như đã đề cập trong các bình luận,

@Input() chỉ áp dụng khi dữ liệu bị thay đổi bởi liên kết dữ liệu từ bên ngoài thành phần (dữ liệu bị ràng buộc trong cha mẹ thay đổi) chứ không phải khi dữ liệu được thay đổi từ mã trong thành phần.

Nếu bạn có một cái nhìn của @ Günter, đó là một giải pháp chính xác và chính xác hơn cho vấn đề. Tôi vẫn sẽ giữ câu trả lời này ở đây, nhưng vui lòng làm theo câu trả lời của Günter là câu trả lời đúng.


2
Điều đó là vậy đó. Tôi đã thấy chú thích @Input () trước đây nhưng luôn kết hợp nó với đầu vào biểu mẫu, không bao giờ nghĩ rằng tôi sẽ cần nó ở đây. Chúc mừng.
Matt Watson

1
@ John cảm ơn vì lời giải thích bổ sung. Nhưng những gì bạn đã thêm chỉ áp dụng khi dữ liệu bị thay đổi bởi liên kết dữ liệu từ bên ngoài thành phần (dữ liệu bị ràng buộc trong cha mẹ thay đổi) chứ không phải khi dữ liệu được thay đổi từ mã trong thành phần.
Günter Zöchbauer

Có một ràng buộc thuộc tính được đính kèm khi sử dụng @Input()từ khóa và ràng buộc đó đảm bảo rằng giá trị được cập nhật trong dạng xem. giá trị sẽ là một phần của sự kiện vòng đời. Bài đăng này chứa một số thông tin thêm về từ @Input()khóa
John

Tôi sẽ bỏ cuộc. Tôi không thấy nơi nào có @Input()liên quan ở đây hoặc tại sao nó nên và câu hỏi không cung cấp đủ thông tin. Dù sao cũng cảm ơn bạn đã kiên nhẫn :)
Günter Zöchbauer

3
@Input()là một cách giải quyết khác để che giấu gốc rễ của vấn đề. Các vấn đề phải liên quan đến các khu vực và phát hiện thay đổi.
ma thuật sau

2

Trong trường hợp của tôi, tôi đã có một vấn đề rất giống nhau. Tôi đã cập nhật quan điểm của mình bên trong một hàm được gọi bởi một thành phần cha mẹ và trong thành phần cha mẹ của tôi, tôi đã quên sử dụng @ViewChild (NameOfMyChieldComponent). Tôi đã mất ít nhất 3 giờ chỉ vì sai lầm ngu ngốc này. tức là: Tôi không cần sử dụng bất kỳ phương pháp nào trong số đó:

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

1
bạn có thể giải thích làm thế nào bạn có được thành phần con của bạn được cập nhật với @ViewChild không? cảm ơn
Lucas Colombo

Tôi nghi ngờ, phụ huynh đã gọi một phương thức của một lớp con không được kết xuất và chỉ tồn tại trong bộ nhớ
glukki

1

Thay vì xử lý các vùng và thay đổi phát hiện - hãy để AsyncPipe xử lý sự phức tạp. Điều này sẽ đặt đăng ký có thể quan sát, hủy đăng ký (để tránh rò rỉ bộ nhớ) và phát hiện thay đổi trên vai Angular.

Thay đổi lớp của bạn để làm cho có thể quan sát, điều đó sẽ phát ra kết quả của các yêu cầu mới:

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

Và cập nhật chế độ xem của bạn để sử dụng AsyncPipe:

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

Muốn thêm vào, tốt hơn là tạo một dịch vụ với phương thức sẽ đưa ra intervalđối số và:

  • tạo các yêu cầu mới (bằng cách sử dụng exhaustMapnhư trong mã ở trên);
  • xử lý lỗi yêu cầu;
  • ngăn trình duyệt thực hiện các yêu cầu mới trong khi ngoại tuyến.
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.