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 Map上重绘标记。问题在于,父级中的rawLapsData更改不会触发ngOnChanges 。我能做什么?

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;

您的代码没有显示如何更新数据或数据的类型。是否分配了新实例,还是只是修改了值的属性?
君特Zöchbauer

@GünterZöchbauer我添加了来自父组件的行
Simon H,

我想把这行包装起来zone.run(...)就可以了。
君特Zöchbauer

1
您的数组(引用)没有改变,因此ngOnChanges()不会被调用。您可以使用ngDoCheck()和实现自己的逻辑来确定数组内容是否已更改。 lapsData已更新,因为它具有/引用与相同的数组rawLapsData
Mark Rajcok '16

1
1)在laps组件中,您的代码/模板在lapsData数组中的每个条目上循环,并显示内容,因此在显示的每条数据上都有Angular绑定。2)即使Angular没有检测到对组件输入属性的任何更改(引用检查),它仍然(默认情况下)检查所有模板绑定。这就是圈数如何适应变化的方式。3)maps组件的模板中可能没有与lapsData输入属性的任何绑定,对吗?那将解释差异。
Mark Rajcok '16

Answers:


153

rawLapsData 即使您修改了数组的内容(例如,添加项目,删除项目,更改项目),仍然会继续指向同一数组。

在更改检测期间,当Angular检查组件的输入属性是否更改时,它将(基本上)===用于脏检查。对于数组,这意味着(仅)对数组引用进行了脏检查。由于rawLapsData数组引用未更改,ngOnChanges()因此不会调用。

我可以想到两种可能的解决方案:

  1. 实现ngDoCheck()并执行您自己的更改检测逻辑,以确定阵列内容是否已更改。(Lifecycle Hooks文档中有一个示例。)

  2. rawLapsData每当您对数组内容进行任何更改时,都要为其分配一个新数组。然后ngOnChanges()将被调用,因为数组(引用)将作为更改出现。

在回答中,您想出了另一个解决方案。

在OP上重复一些评论:

我还是不明白 laps进行更改(肯定必须使用与ngOnChanges()自身等效的内容?)而map不能这样做。

  • laps组件中,您的代码/模板在lapsData数组中的每个条目上循环,并显示内容,因此在显示的每条数据上都有Angular绑定。
  • 即使Angular(使用===检查)未检测到组件输入属性的任何更改,它(默认情况下)仍会脏检查所有模板绑定。当这些更改中的任何一个更改时,Angular将更新DOM。那就是你所看到的。
  • maps组件的模板中可能没有任何绑定lapsData输入属性的,对吗?那将解释差异。

请注意,lapsData在两个组件和rawLapsData父组件中,所有都指向相同/一个数组。因此,即使Angular没有注意到对(lapsData输入属性的,组件“获取” /查看任何数组内容更改,因为它们都共享/引用一个数组。我们不需要Angular来传播这些更改,就像我们使用原始类型(字符串,数字,布尔值)一样。但是对于原始类型,对值的任何更改都将始终触发ngOnChanges()-这是您在答案/解决方案中利用的东西。

您可能现在已经知道对象输入属性与数组输入属性具有相同的行为。


是的,我正在突变一个深层嵌套的对象,我想这使Angular很难发现结构中的更改。不是我偏爱突变,而是经过转换的XML,我舍不得丢失任何周围的数据,因为我想在末尾再次重新创建xml
Simon H

7
@SimonH,“ Angular很难发现结构中的更改” —只需清楚一点,Angular甚至不会在输入属性(即数组或对象)中查找更改。它仅查看值是否更改-对于对象和数组,该值是引用。对于基本类型,值是...值。(我不确定我是否直通所有行话,但是您明白了。)
Mark Rajcok,2016年

11
好答案。Angular2团队迫切需要发布有关变更检测内部的详细,权威性文档。

如果我在doCheck中执行功能,就我而言,do check调用了很多次。你能以其他方式告诉我吗?
Mr_Perfect

@MarkRajcok您能帮我解决这个问题

27

这不是最干净的方法,但是每次更改值时您都可以克隆对象吗?

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

我想我更喜欢这种方法,ngDoCheck()而不是自己实施,但也许像@GünterZöchbauer这样的人可以加入。


如果不确定目标浏览器是否支持<Object.assign()>,则还可以将其字符串化为json字符串并解析回json,这还将创建一个新对象...
Guntram

1
@Guntram还是polyfill?
戴维(David)

12

在数组的情况下,您可以这样操作:

.ts要更新的文件(父组件)中rawLapsData,如下所示:

rawLapsData = somevalue; // change detection will not happen

解:

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

并将ngOnChanges在子组件中调用


10

作为Mark Rajcok第二种解决方案的扩展

每当您对数组内容进行任何更改时,都将一个新数组分配给rawLapsData。然后将调用ngOnChanges(),因为数组(引用)将作为更改出现

您可以像这样克隆数组的内容:

rawLapsData = rawLapsData.slice(0);

我说这是因为

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

没有为我工作。我希望这有帮助。


8

如果数据来自外部库,则可能需要在中运行数据更新语句zone.run(...)zone: NgZone为此注入。如果您已经可以在其中运行外部库的实例化zone.run(),那么zone.run()以后可能不需要。


正如对OP的评论所指出的,这些更改不是外部的,而是深入到json对象中的
Simon H

1
就像您的回答所说,我们仍然需要运行一些东西来使Angular2中的东西保持同步,就像angular1一样$scope.$apply吗?
Pankaj Parkar '16

1
如果是从Angular外部启动的,则不会使用按区域修补的API,并且不会通知Angular可能的更改。是的,zone.run类似于$scope.apply
君特Zöchbauer

6

使用ChangeDetectorRef.detectChanges()告诉角运行变化检测,当你编辑一个嵌套的对象(即它与它的脏检查错过)。


但是如何?我正在尝试使用此方法,我想在父级以2方式限制[[Collection]]推送新项目后,在孩子中触发changeDetection。
Deunz

问题不在于未发生更改检测,而是更改检测未检测到更改!所以这似乎不是解决方案!为什么这个答案得到了五票?
Shahryar Saljoughi

5

我有2种解决方案来解决您的问题

  1. 使用ngDoCheck检测object的数据改变或不
  2. object通过object = Object.create(object)父组件分配一个新的内存地址。

2
Object.create(object)与Object.assign({},object)之间会有明显的区别吗?
Daniel Galarza

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同时更改,这为map提供了通过更简单的对象原始类型检测更改的机会。它不是很优雅,但是可以。


我发现很难用模板语法来跟踪各个组件上的所有更改,尤其是在中/大型应用程序上。通常我使用共享事件发射器和订阅来传递数据(快速解决方案),或为此(在有计划时间的情况下)实现Redux模式(通过Rx.Subject)...
Sasxa 2016年

3

更改对象(包括嵌套对象)的属性时,不会触发更改检测。一种解决方案是使用“ lodash” clone()函数重新分配新的对象引用。

import * as _ from 'lodash';

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

2

这是一个让我摆脱这个麻烦的技巧。

因此,与OP类似的情况-我有一个嵌套的Angular组件,需要将数据向下传递给它,但是输入指向数组,并且如上所述,Angular看不到变化,因为它不检查数组的内容。

因此,要解决此问题,我将数组转换为字符串以供Angular检测到更改,然后在嵌套组件中将字符串split(',')拆分回数组,并重新获得快乐的日子。


1
!array.slice(0)方法更加简洁。
克里斯·海恩斯

2

我偶然发现了同样的需求。我对此读了很多,所以这是我的主题。

如果要在推送时进行更改检测,则在更改对象的值时可以使用它吗?如果以某种方式删除对象,您也将拥有它。

如前所述,使用changeDetectionStrategy.onPush

假设您使用changeDetectionStrategy.onPush制作了此组件:

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

然后,您将推送一个项目并触发更改检测:

myCollection.push(anItem);
refresh();

否则您将删除一个项目并触发更改检测:

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

或者您将更改某项的属性值并触发更改检测:

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

刷新内容:

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

slice方法返回完全相同的Array,[=]符号对其进行新引用,每次需要时都会触发更改检测。简单易读:)

问候,


2

我已经尝试了这里提到的所有解决方案,但由于某种原因ngOnChanges()仍然无法解决。所以我this.ngOnChanges()在调用重新填充数组的服务后调用了它,它工作了....对吗?可能不是。整齐?一定不行。作品?是!


1

好的,所以我的解决方案是:

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

这触发了我ngOnChanges


0

我必须为此创建一个hack-

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;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.