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 05

Ответы:

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()делает.
Марк Райкок
1
Согласен. Используйте только в том detectChanges()случае, если любые вносимые вами изменения являются локальными для компонента и его потомков. Насколько я понимаю, это detectChanges()будет проверять компонент и его потомки, но zone.run()и ApplicationRef.tick()все дерево компонентов.
Марк Райкок
2
именно то, что я искал ... использование @Input()не имело смысла и не работало.
Йорч
15
Я действительно не понимаю, зачем нам все это руководство или почему боги angular2 оставили такую ​​необходимость. Почему @Input не означает, что компонент действительно зависит от того, что сказал родительский компонент, что зависит от его изменяющихся данных? Кажется крайне неэлегантным, что это не просто работает. Что мне не хватает?
13
Работал у меня. Но это еще один пример того, почему у меня такое чувство, что разработчики фреймворка Angular теряются в сложности фреймворка angular. При использовании angular в качестве разработчика приложений вы должны потратить так много времени, чтобы понять, как angular делает то или иное, и как обходить такие вещи, как вопросы, о которых говорилось выше. Terrrible !!
Karl
32

Изначально это ответ в комментариях от @Mark Rajcok, но я хочу разместить его здесь как протестированное и работающее как решение с использованием 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());
    }
}

Изменить : использование .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();
    }

}
Al-Mothafar
источник
this.cdRef.detectChanges (); исправил мою проблему. Спасибо!
mangesh
Спасибо за ваше предложение, но после нескольких попыток вызова я получаю сообщение об ошибке: отказ: Ошибка: ViewDestroyedError: Попытка использовать разрушенное представление: detectChanges Для меня zone.run () помогает мне выйти из этой ошибки.
Шивам Шривастава
8

Попробуй использовать @Input() recentDetections: Array<RecentDetection>;

РЕДАКТИРОВАТЬ: причина@Input()важности заключается в том, что вы хотите привязать значение в файле typescript / javascript к представлению (html). Представление обновится, если значение, объявленное с помощью@Input()декоратора, изменится. Еслидекоратор@Input()или@Output()изменяется,ngOnChanges-event запускается, и представление обновляется с новым значением. Вы можете сказать, что значение@Input()будет двусторонним.

поиск Input по этой ссылке с помощью angular: глоссарий для получения дополнительной информации

РЕДАКТИРОВАТЬ: узнав больше о разработке Angular 2, я решил, что это@Input()действительно не решение, и, как упоминалось в комментариях,

@Input() применяется только тогда, когда данные изменяются посредством привязки данных извне компонента (привязанные данные в родительском элементе изменены), а не когда данные изменяются из кода внутри компонента.

Если вы посмотрите ответ @ Günter, это более точное и правильное решение проблемы. Я все равно сохраню этот ответ здесь, но, пожалуйста, следуйте ответу Гюнтера как правильному.

Джон
источник
2
Вот и все. Я видел аннотацию @Input () раньше, но всегда связывал ее с вводом формы, никогда не думал, что она мне понадобится здесь. Приветствия.
Мэтт Уотсон
1
@John спасибо за дополнительное объяснение. Но то, что вы добавили, применяется только тогда, когда данные изменяются привязкой данных извне компонента (привязанные данные в родительском элементе изменены), а не когда данные изменяются из кода внутри компонента.
Günter Zöchbauer
При использовании @Input()ключевого слова прикрепляется привязка свойства , и эта привязка гарантирует, что значение обновляется в представлении. значение будет частью события жизненного цикла. Этот пост содержит дополнительную информацию о @Input()ключевом слове
Иоанн
Я сдамся. Я не понимаю, где @Input()здесь участвует или почему это должно быть, и вопрос не дает достаточно информации. В любом случае спасибо за терпение :)
Günter Zöchbauer
3
@Input()это скорее обходной путь, который скрывает корень проблемы. Проблемы должны быть связаны с зонами и обнаружением изменений.
magiccrafter
2

В моем случае у меня была очень похожая проблема. Я обновлял свое представление внутри функции, которая вызывалась родительским компонентом, а в моем родительском компоненте я забыл использовать @ViewChild (NameOfMyChieldComponent). Я потерял как минимум 3 часа только из-за этой глупой ошибки. то есть: мне не нужно было использовать ни один из этих методов:

  • ChangeDetectorRef.detectChanges ()
  • ChangeDetectorRef.markForCheck ()
  • ApplicationRef.tick ()
user3674783
источник
1
Можете ли вы объяснить, как вы обновили дочерний компонент с помощью @ViewChild? спасибо
Лукас Коломбо
Я подозреваю, что родитель вызывал метод дочернего класса, который не был отрисован и существовал только в памяти
glukki 05
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как в коде выше);
  • обрабатывать ошибки запросов;
  • запретить браузеру делать новые запросы в автономном режиме.
glukki
источник