Виявлення змін Angular2: ngOnChanges не запускається для вкладеного об'єкта


129

Я знаю, що я не перший, хто запитав про це, але не можу знайти відповідь у попередніх питаннях. Я маю це в одному компоненті

<div class="col-sm-5">
    <laps
        [lapsData]="rawLapsData"
        [selectedTps]="selectedTps"
        (lapsHandler)="lapsHandler($event)">
    </laps>
</div>

<map
    [lapsData]="rawLapsData"
    class="col-sm-7">
</map>

У контролері rawLapsdataчас від часу мутується.

В laps, дані виводяться в вигляді HTML в текстовому форматі. Це змінюється щоразу, коли rawLapsdataзмінюється.

Мій mapкомпонент потрібно використовувати ngOnChangesяк тригер для перемальовування маркерів на карті Google. Проблема полягає в тому, що ngOnChanges не спрацьовує при rawLapsDataзмінах у батьків. Що я можу зробити?

import {Component, Input, OnInit, OnChanges, SimpleChange} from 'angular2/core';

@Component({
    selector: 'map',
    templateUrl: './components/edMap/edMap.html',
    styleUrls: ['./components/edMap/edMap.css']
})
export class MapCmp implements OnInit, OnChanges {
    @Input() lapsData: any;
    map: google.maps.Map;

    ngOnInit() {
        ...
    }

    ngOnChanges(changes: { [propName: string]: SimpleChange }) {
        console.log('ngOnChanges = ', changes['lapsData']);
        if (this.map) this.drawMarkers();
    }

Оновлення: ngOnChanges не працює, але виглядає так, ніби lapsData оновлюється. У ngInit - слухач подій для зміни масштабу, який також викликає this.drawmarkers. Коли я змінюю зум, я дійсно бачу зміну маркерів. Тож єдине питання полягає в тому, що я не отримую сповіщення під час зміни вхідних даних.

У батьків є такий рядок. (Нагадаємо, що зміна відображена в колі, але не в карті).

this.rawLapsData = deletePoints(this.rawLapsData, this.selectedTps);

І зверніть увагу, що this.rawLapsDataсам по собі вказівник на середину великого об'єкта json

this.rawLapsData = this.main.data.TrainingCenterDatabase.Activities[0].Activity[0].Lap;

У вашому коді не відображається спосіб оновлення даних або тип даних. Чи присвоєно новий екземпляр чи просто властивість значення змінено?
Günter Zöchbauer

@ GünterZöchbauer Я додав рядок від батьківського компонента
Simon H

Я думаю, що введення цього рядка zone.run(...)має робити це тоді.
Günter Zöchbauer

1
Ваш масив (посилання) не змінюється, тому ngOnChanges()не буде викликаний. Ви можете використовувати ngDoCheck()та реалізувати власну логіку, щоб визначити, чи змінився вміст масиву. lapsDataоновлюється, оскільки має / посилається на той самий масив, що і rawLapsData.
Марк Райчкок

1
1) У компонентах колів ваш код / ​​шаблон замикається над кожним записом у масиві lapsData та відображає вміст, тому на кожному фрагменті даних, що відображаються, є кутові прив'язки. 2) Навіть якщо Angular не виявляє жодних змін (перевірка посилань) на вхідні властивості компонента, він все одно (за замовчуванням) перевіряє всі прив'язки шаблону. Ось так круги підбирають зміни. 3) компонент карт, ймовірно, не має прив'язки у своєму шаблоні до властивості введення lapsData, правда? Це пояснило б різницю.
Марк Райкок

Відповіді:


153

rawLapsData продовжує вказувати на той самий масив, навіть якщо ви змінюєте вміст масиву (наприклад, додавати елементи, видаляти елементи, змінювати елемент).

Під час виявлення змін, коли Angular перевіряє вхідні властивості компонентів на зміну, він використовує (по суті) ===для брудної перевірки. Для масивів це означає, що посилання на масиви (лише) перевірені брудно. Оскільки rawLapsDataпосилання на масив не змінюється, ngOnChanges()не буде викликано.

Я можу придумати два можливі рішення:

  1. Реалізуйте ngDoCheck()та виконайте власну логіку виявлення змін, щоб визначити, чи змінився вміст масиву. (Документ "Lifecycle Hooks" має приклад .)

  2. Призначте новий масив rawLapsDataкожного разу, коли ви вносите зміни до вмісту масиву. Потім ngOnChanges()буде викликано, тому що масив (посилання) з'явиться як зміна.

У своїй відповіді ви придумали інше рішення.

Повторюючи тут деякі коментарі щодо ОП:

Я досі не бачу, як lapsможна отримати зміну (напевно, вона повинна використовувати щось еквівалентне ngOnChanges()собі?), Але mapне можу.

  • У складі lapsкомпонента ваш код / ​​шаблон замикається над кожним записом у lapsDataмасиві та відображає вміст, тому на кожному фрагменті даних, що відображаються, є кутові прив'язки.
  • Навіть коли Angular не виявляє жодних змін у вхідних властивостях компонента (використовуючи ===перевірку), він все одно (за замовчуванням) брудно перевіряє всі прив'язки шаблону. Коли будь-яка з цих змін зміниться, Angular оновить DOM. Це те, що ти бачиш.
  • mapsКомпонент , ймовірно , не має яких - небудь прив'язок в своєму шаблоні його lapsDataвведення нерухомості, НЕ так? Це пояснило б різницю.

Зверніть увагу, що lapsDataв обох компонентах і rawLapsDataв батьківському компоненті всі вказують на один і той же / один масив. Тож навіть якщо Angular не помічає жодних (посилальних) змін у lapsDataвхідних властивостях, компоненти "отримують" / бачать зміст вмісту масиву, оскільки всі вони ділять / посилаються на один масив. Нам не потрібен Angular для розповсюдження цих змін, як це було б з примітивним типом (рядок, число, булевий). Але при примітивному типі будь-яка зміна значення завжди спричинить ngOnChanges()- що ви використовуєте у своїй відповіді / рішенні.

Як ви, напевно, зрозуміли, властивості введення об'єкта мають таку саму поведінку, як і властивості введення масиву.


Так, я мутував глибоко вкладений об’єкт, і я думаю, що Angular важко помітив зміни в структурі. Я не мою перевагу мутувати, але це перекладено XML, і я не можу дозволити собі втратити будь-які оточуючі дані, оскільки я хочу в кінці знову відтворити XML
Simon H

7
@SimonH, "Важко для Angular помітити зміни в структурі" - Просто для того, щоб бути зрозумілим, Angular навіть не дивиться всередині вхідних властивостей, які є масивами чи об'єктами для змін. Він лише бачить, чи змінилося значення - для об'єктів і масивів значення є еталонним. Для примітивних типів значення - це ... значення. (Я не впевнений, що у мене все жаргон прямо, але ви
розумієте

11
Чудова відповідь. Команді Angular2 відчайдушно потрібно опублікувати детальний, авторитетний документ про внутрішнє виявлення змін.

Якщо я виконую функціональні можливості в doCheck, у моєму випадку перевірка дій викликає стільки разів. Чи можете ви мені скажіть будь-який інший спосіб?
Mr_Perfect

@MarkRajcok, будь ласка, допоможіть мені вирішити цю проблему stackoverflow.com/questions/50166996/…
Ніксон

27

Не найчистіший підхід, але ви можете просто клонувати об’єкт щоразу, коли змінюєте значення?

   rawLapsData = Object.assign({}, rawLapsData);

Я думаю, що я віддаю перевагу такому підходу над впровадженням вашого власного, ngDoCheck()але, можливо, хтось на кшталт @ GünterZöchbauer міг би задзвонити.


Якщо ви не впевнені, чи підтримує цільовий браузер <Object.assign ()>, ви також можете ввести рядок у json рядок і проаналізувати назад до json, що також створить новий об’єкт ...
Guntram

1
@Guntram Або поліфіл?
Девід

12

У випадку з масивами ви можете це зробити так:

У .tsфайлі (батьківський компонент), де ви оновлюєте, rawLapsDataзробіть це так:

rawLapsData = somevalue; // change detection will not happen

Рішення:

rawLapsData = {...somevalue}; //change detection will happen

і ngOnChangesбуде викликано в дочірньому компоненті


10

Як доповнення до другого рішення Марка Райкока

Призначте новий масив rawLapsData, коли ви вносите будь-які зміни до вмісту масиву. Тоді ngOnChanges () буде викликаний тому, що масив (посилання) з'явиться як зміна

ви можете клонувати вміст масиву так:

rawLapsData = rawLapsData.slice(0);

Я згадую про це тому, що

rawLapsData = Object.assign ({}, rawLapsData);

не працювало для мене. Я сподіваюся, що це допомагає.


8

Якщо дані надходять із зовнішньої бібліотеки, можливо, вам потрібно буде запустити оператор upate даних всередині zone.run(...). zone: NgZoneДля цього роблять ін’єкцію . Якщо ви можете запустити інстанціювання зовнішньої бібліотеки zone.run()вже протягом , можливо, вона не знадобиться zone.run()пізніше.


Як зазначається в коментарях до ОП, зміни були не зовнішніми, а глибокими в межах об'єкта json
Саймон Х

1
Як говориться у вашій відповіді, все ж нам потрібно запустити щось для того, щоб річ синхронізувалася у Angular2, як angular1 це було $scope.$apply?
Панкай Паркар

1
Якщо щось запускається за межами Angular, API, виправлене зоною, не використовується, і Angular не отримує повідомлення про можливі зміни. Так, zone.runсхоже на $scope.apply.
Günter Zöchbauer

6

Використовуйте, ChangeDetectorRef.detectChanges()щоб сказати Angular, щоб запустити виявлення змін, коли ви редагуєте вкладений об'єкт (що він не вистачає під час брудної перевірки).


Але як ? Я намагаюся використовувати це, я хочу викликати changeDetection у дитини після того, як батько натиснув новий елемент двостороннім обмеженням [(Колекція)].
Деунц

Проблема не в тому, що виявлення змін не відбувається, а те, що виявлення змін не виявляє змін! тому це, здається, не є рішенням! чому ця відповідь отримала п'ять результатів?
Шахряр Саляфі

5

У мене є 2 рішення, щоб вирішити вашу проблему

  1. Використовуйте ngDoCheckдля виявлення objectданих, змінених чи ні
  2. Призначити objectнову адресу пам'яті object = Object.create(object)від батьківського компонента.

2
Чи буде якась помітна різниця між Object.create (object) та Object.assign ({}, object)?
Даніель Галарза

3

Моє рішення "зламати"

   <div class="col-sm-5">
        <laps
            [lapsData]="rawLapsData"
            [selectedTps]="selectedTps"
            (lapsHandler)="lapsHandler($event)">
        </laps>
    </div>
    <map
        [lapsData]="rawLapsData"
        [selectedTps]="selectedTps"   // <--------
        class="col-sm-7">
    </map>

selectedTps змінюється одночасно з rawLapsData, що дає карті ще один шанс виявити зміни за допомогою більш простого об'єкта примітивного типу. Це НЕ елегантно, але працює.


Мені важко відслідковувати всі зміни різних компонентів у синтаксисі шаблонів, особливо у додатках середнього / великого масштабу. Зазвичай я використовую спільний емітер подій та підписку для передачі даних (швидке рішення) або впроваджую для цього шаблон Redux (через Rx.Subject) (коли є час для планування) ...
Сакса

3

Виявлення змін не спрацьовує, коли ви змінюєте властивість об'єкта (включаючи вкладений об'єкт). Одним із варіантів рішення було б перепризначення нової посилання на об'єкт за допомогою функції "lodash" clone ().

import * as _ from 'lodash';

this.foo = _.clone(this.foo);

2

Ось хак, який щойно визволив мене з цієї проблеми.

Так що аналогічний сценарій з ОП - у мене є вкладений кутовий компонент, якому потрібні дані, передані на нього, але вхідні точки вказують на масив, і, як було сказано вище, Angular не бачить змін, оскільки він не вивчає вміст масиву.

Отже, щоб виправити це, я перетворюю масив у рядок для Angular, щоб виявити зміни, а потім у вкладеному компоненті я розділив (',') рядок назад до масиву та його щасливих днів знову.


1
Гидота! Підхід array.slice (0) набагато чистіший.
Кріс Хайнс

2

Я натрапив на ту саму потребу. І я багато читав на цьому так, ось моя мідь на цю тему.

Якщо ви хочете, щоб ваше виявлення змін було натиснене, то у вас виникне це, коли ви зміните значення об'єкта всередині права? І у вас це теж буде, якщо якось ви видалите предмети.

Як вже було сказано, використовуйте changeDetectionStrategy.onPush

Скажіть, у вас є цей компонент, який ви створили, за допомогою changeDetectionStrategy.onPush:

<component [collection]="myCollection"></component>

Тоді ви натискаєте елемент і запускаєте виявлення змін:

myCollection.push(anItem);
refresh();

або ви видалите елемент і запустить виявлення змін:

myCollection.splice(0,1);
refresh();

або ви б змінили значення attrbibute для елемента і запустили виявлення змін:

myCollection[5].attribute = 'new value';
refresh();

Вміст оновлення:

refresh() : void {
    this.myCollection = this.myCollection.slice();
}

Метод зрізу повертає той самий масив, і знак [=] робить нове посилання на нього, запускаючи виявлення змін кожного разу, коли вам це потрібно. Легко і читано :)

З повагою,


2

Я спробував усі згадані тут рішення, але чомусь ngOnChanges()все ще не вистрілив для мене. Тож я зателефонував до нього this.ngOnChanges()після виклику служби, яка перенаселяє мої масиви, і вона працювала .... правильно певно, ні. Акуратний? чорт візьми, ні. Працює? так!


1

гаразд, моє рішення для цього було:

this.arrayWeNeed.DoWhatWeNeedWithThisArray();
const tempArray = [...arrayWeNeed];
this.arrayWeNeed = [];
this.arrayWeNeed = tempArray;

І це викликає мене ngOnChanges


0

Мені довелося створити хак для цього -

I created a Boolean Input variable and toggled it whenever array changed, which triggered change detection in the child component, hence achieving the purpose

0

У моєму випадку це були зміни вартості об'єкта, які ngOnChangeне фіксували. Кілька значень об'єкта змінюються у відповідь на виклик api. Повторна ініціалізація об'єкта вирішила проблему і призвела ngOnChangeдо запуску в дочірньому компоненті.

Щось на зразок

 this.pagingObj = new Paging(); //This line did the magic
 this.pagingObj.pageNumber = response.PageNumber;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.