Angular2 变化检测:ngOnChanges 不触发嵌套对象
Angular2 change detection: ngOnChanges not firing for nested object
我知道我不是第一个问这个问题的人,但我在之前的问题中找不到答案。我在一个组件中有这个
<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
正在更新。在 ngOnInit
中是缩放更改的事件侦听器,它也调用 this.drawmarkers
。当我改变缩放比例时,我确实看到了标记的变化。所以唯一的问题是我在输入数据更改时没有收到通知。
在父级中,我有这一行。 (回想一下,变化反映在圈数中,而不是 map
)。
this.rawLapsData = deletePoints(this.rawLapsData, this.selectedTps);
注意this.rawLapsData
本身就是一个指向大json对象中间的指针
this.rawLapsData = this.main.data.TrainingCenterDatabase.Activities[0].Activity[0].Lap;
如果数据来自外部库,您可能需要 运行 zone.run(...)
中的数据更新语句。为此注入 zone: NgZone
。如果您已经可以 运行 在 zone.run()
中实例化外部库,那么以后您可能不需要 zone.run()
。
我的'hack'解决方案是
<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 有机会通过更简单的 object 原始类型检测变化。它不优雅,但它有效。
rawLapsData
继续指向同一个数组,即使您修改了数组的内容(例如,添加项目、删除项目、更改项目)。
在更改检测期间,当 Angular 检查组件的输入属性是否发生更改时,它(基本上)使用 ===
进行脏检查。对于数组,这意味着(仅)对数组引用进行了脏检查。由于 rawLapsData
数组引用未更改,因此不会调用 ngOnChanges()
。
我能想到两种可能的解决方案:
实施ngDoCheck()
并执行您自己的更改检测逻辑以确定数组内容是否已更改。 (Lifecycle Hooks 文档有 an example。)
每当您对数组内容进行任何更改时,将新数组分配给 rawLapsData
。然后ngOnChanges()
会被调用,因为数组(引用)会出现变化
在您的回答中,您提出了另一种解决方案。
在这里重复一些关于 OP 的评论:
I still don't see how laps
can pick up on the change (surely it must be using something equivalent to ngOnChanges()
itself?) while map
can't.
- 在
laps
组件中,您的 code/template 遍历 lapsData
数组中的每个条目,并显示内容,因此每个片段上都有 Angular 绑定显示的数据。
- 即使 Angular 没有检测到组件输入属性的任何更改(使用
===
检查),它仍然(默认情况下)对所有模板绑定进行脏检查。当其中任何一个发生变化时,Angular 将更新 DOM。这就是您所看到的。
maps
组件在其模板中可能没有任何绑定到其 lapsData
输入 属性,对吗?这将解释差异。
请注意,两个组件中的lapsData
和父组件中的rawLapsData
都指向same/one数组。因此,即使 Angular 没有注意到对 lapsData
输入属性的任何(参考)更改,组件 "get"/看到任何数组内容也会更改,因为它们都 share/reference 那个大批。我们不需要 Angular 来传播这些更改,就像我们使用原始类型(字符串、数字、布尔值)一样。但是对于原始类型,对值的任何更改总是会触发 ngOnChanges()
– 这是您在 answer/solution.
中利用的东西
您现在可能已经发现,对象输入属性与数组输入属性具有相同的行为。
这里有一个 hack 让我摆脱了这个问题。
所以与 OP 的情况类似 - 我有一个嵌套的 Angular 组件需要向下传递数据,但输入指向一个数组,如上所述,Angular 没有看到变化,因为它不检查数组的内容。
因此,为了修复它,我将数组转换为 Angular 的字符串以检测更改,然后在嵌套组件中,我将字符串拆分 (',') 返回数组及其快乐的日子再次.
使用 ChangeDetectorRef.detectChanges()
告诉 Angular 在您编辑嵌套对象时 运行 进行更改检测(它未通过脏检查)。
这不是最干净的方法,但您可以在每次更改值时克隆对象吗?
rawLapsData = Object.assign({}, rawLapsData);
我想我更喜欢这种方法而不是实现你自己的方法 ngDoCheck()
但也许像 @GünterZöchbauer 这样的人可以插话。
作为 Mark Rajcok 的第二个解决方案的扩展
Assign a new array to rawLapsData whenever you make any changes to the
array contents. Then ngOnChanges() will be called because the array
(reference) will appear as a change
您可以像这样克隆数组的内容:
rawLapsData = rawLapsData.slice(0);
我提到这个是因为
rawLapsData = Object.assign({}, rawLapsData);
对我不起作用。希望对您有所帮助。
更改对象(包括嵌套对象)的 属性 时不会触发更改检测。一种解决方案是使用 'lodash' clone() 函数重新分配新的对象引用。
import * as _ from 'lodash';
this.foo = _.clone(this.foo);
在您要更新 rawLapsData
的 .ts
文件(父组件)中,这样做:
rawLapsData = somevalue; // change detection will not happen
解决方案:
rawLapsData = {...somevalue}; //for Object, change detection will happen
rawLapsData = [...somevalue]; //for Array, change detection will happen
和ngOnChanges
将调用子组件
我偶然发现了同样的需求。我在这方面读了很多,所以,这是我关于这个问题的铜板。
如果你想在推送时进行更改检测,那么当你更改内部对象的值时你会得到它吗?如果你以某种方式移除对象,你也会拥有它。
如前所述,使用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();
}
切片方法returns完全相同的数组,[=]符号对其进行新引用,每次需要时触发变化检测。简单易读:)
此致,
我有 2 个解决方案可以解决您的问题
- 使用
ngDoCheck
检测object
数据是否改变
- 通过父组件
object = Object.create(object)
将object
分配给新的内存地址。
好的,所以我的解决方案是:
this.arrayWeNeed.DoWhatWeNeedWithThisArray();
const tempArray = [...arrayWeNeed];
this.arrayWeNeed = [];
this.arrayWeNeed = tempArray;
这触发了我 ngOnChanges
我不得不为它创建一个 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
在我的例子中,ngOnChange
没有捕捉到对象值的变化。响应 api 调用修改了一些对象值。重新初始化对象修复了问题并导致 ngOnChange
在子组件中触发。
类似
this.pagingObj = new Paging(); //This line did the magic
this.pagingObj.pageNumber = response.PageNumber;
当您像这样操作数据时:
this.data.profiles[i].icon.url = '';
那么你应该使用以检测变化:
let array = this.data.profiles.map(x => Object.assign({}, x)); // It will detect changes
由于angular ngOnchanges 无法检测到数组中的变化,因此我们必须为对象分配一个新的引用。每次都有效!
这是一个将 IterableDiffer 与 ngDoCheck 结合使用的示例。如果您需要跟踪随时间变化的变化,IterableDiffer 特别有用,因为它可以让您执行仅迭代 added/changed/removed 个值等操作
一个没有使用 IterableDiffer 的所有优点的简单示例,但它可以工作并显示原理:
export class FeedbackMessagesComponent implements DoCheck {
@Input()
messages: UserFeedback[] = [];
// Example UserFeedback instance { message = 'Ooops', type = Notice }
@HostBinding('style.display')
display = 'none';
private _iterableDiffer: IterableDiffer<UserFeedback>;
constructor(private _iterableDiffers: IterableDiffers) {
this._iterableDiffer = this._iterableDiffers.find([]).create(null);
}
ngDoCheck(): void {
const changes = this._iterableDiffer.diff(this.messages);
if (changes) {
// Here you can do stuff like changes.forEachRemovedItem()
// We know contents of this.messages was changed so update visibility:
this.display = this.messages.length > 0 ? 'block' : 'none';
}
}
}
现在这将根据 myMessagesArray 计数自动 show/hide:
<app-feedback-messages
[messages]="myMessagesArray"
></app-feedback-messages>
假设你有一个嵌套对象,比如
var obj = {"parent": {"child": {....}}}
如果你传递了完整对象的引用,比如
[wholeObj] = "obj"
在那种情况下,你无法检测到子对象的变化,所以为了克服这个问题你也可以通过另一个属性传递子对象的引用,比如
[wholeObj] = "obj" [childObj] = "obj.parent.child"
因此您也可以检测子对象的变化。
ngOnChanges(changes: SimpleChanges) {
if (changes.childObj) {// your logic here}
}
不是一个干净的解决方案,但您可以通过以下方式触发检测:
rawLapsData = JSON.parse(JSON.stringify(rawLapsData))
我知道我不是第一个问这个问题的人,但我在之前的问题中找不到答案。我在一个组件中有这个
<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
正在更新。在 ngOnInit
中是缩放更改的事件侦听器,它也调用 this.drawmarkers
。当我改变缩放比例时,我确实看到了标记的变化。所以唯一的问题是我在输入数据更改时没有收到通知。
在父级中,我有这一行。 (回想一下,变化反映在圈数中,而不是 map
)。
this.rawLapsData = deletePoints(this.rawLapsData, this.selectedTps);
注意this.rawLapsData
本身就是一个指向大json对象中间的指针
this.rawLapsData = this.main.data.TrainingCenterDatabase.Activities[0].Activity[0].Lap;
如果数据来自外部库,您可能需要 运行 zone.run(...)
中的数据更新语句。为此注入 zone: NgZone
。如果您已经可以 运行 在 zone.run()
中实例化外部库,那么以后您可能不需要 zone.run()
。
我的'hack'解决方案是
<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 有机会通过更简单的 object 原始类型检测变化。它不优雅,但它有效。
rawLapsData
继续指向同一个数组,即使您修改了数组的内容(例如,添加项目、删除项目、更改项目)。
在更改检测期间,当 Angular 检查组件的输入属性是否发生更改时,它(基本上)使用 ===
进行脏检查。对于数组,这意味着(仅)对数组引用进行了脏检查。由于 rawLapsData
数组引用未更改,因此不会调用 ngOnChanges()
。
我能想到两种可能的解决方案:
实施
ngDoCheck()
并执行您自己的更改检测逻辑以确定数组内容是否已更改。 (Lifecycle Hooks 文档有 an example。)每当您对数组内容进行任何更改时,将新数组分配给
rawLapsData
。然后ngOnChanges()
会被调用,因为数组(引用)会出现变化
在您的回答中,您提出了另一种解决方案。
在这里重复一些关于 OP 的评论:
I still don't see how
laps
can pick up on the change (surely it must be using something equivalent tongOnChanges()
itself?) whilemap
can't.
- 在
laps
组件中,您的 code/template 遍历lapsData
数组中的每个条目,并显示内容,因此每个片段上都有 Angular 绑定显示的数据。 - 即使 Angular 没有检测到组件输入属性的任何更改(使用
===
检查),它仍然(默认情况下)对所有模板绑定进行脏检查。当其中任何一个发生变化时,Angular 将更新 DOM。这就是您所看到的。 maps
组件在其模板中可能没有任何绑定到其lapsData
输入 属性,对吗?这将解释差异。
请注意,两个组件中的lapsData
和父组件中的rawLapsData
都指向same/one数组。因此,即使 Angular 没有注意到对 lapsData
输入属性的任何(参考)更改,组件 "get"/看到任何数组内容也会更改,因为它们都 share/reference 那个大批。我们不需要 Angular 来传播这些更改,就像我们使用原始类型(字符串、数字、布尔值)一样。但是对于原始类型,对值的任何更改总是会触发 ngOnChanges()
– 这是您在 answer/solution.
您现在可能已经发现,对象输入属性与数组输入属性具有相同的行为。
这里有一个 hack 让我摆脱了这个问题。
所以与 OP 的情况类似 - 我有一个嵌套的 Angular 组件需要向下传递数据,但输入指向一个数组,如上所述,Angular 没有看到变化,因为它不检查数组的内容。
因此,为了修复它,我将数组转换为 Angular 的字符串以检测更改,然后在嵌套组件中,我将字符串拆分 (',') 返回数组及其快乐的日子再次.
使用 ChangeDetectorRef.detectChanges()
告诉 Angular 在您编辑嵌套对象时 运行 进行更改检测(它未通过脏检查)。
这不是最干净的方法,但您可以在每次更改值时克隆对象吗?
rawLapsData = Object.assign({}, rawLapsData);
我想我更喜欢这种方法而不是实现你自己的方法 ngDoCheck()
但也许像 @GünterZöchbauer 这样的人可以插话。
作为 Mark Rajcok 的第二个解决方案的扩展
Assign a new array to rawLapsData whenever you make any changes to the array contents. Then ngOnChanges() will be called because the array (reference) will appear as a change
您可以像这样克隆数组的内容:
rawLapsData = rawLapsData.slice(0);
我提到这个是因为
rawLapsData = Object.assign({}, rawLapsData);
对我不起作用。希望对您有所帮助。
更改对象(包括嵌套对象)的 属性 时不会触发更改检测。一种解决方案是使用 'lodash' clone() 函数重新分配新的对象引用。
import * as _ from 'lodash';
this.foo = _.clone(this.foo);
在您要更新 rawLapsData
的 .ts
文件(父组件)中,这样做:
rawLapsData = somevalue; // change detection will not happen
解决方案:
rawLapsData = {...somevalue}; //for Object, change detection will happen
rawLapsData = [...somevalue]; //for Array, change detection will happen
和ngOnChanges
将调用子组件
我偶然发现了同样的需求。我在这方面读了很多,所以,这是我关于这个问题的铜板。
如果你想在推送时进行更改检测,那么当你更改内部对象的值时你会得到它吗?如果你以某种方式移除对象,你也会拥有它。
如前所述,使用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();
}
切片方法returns完全相同的数组,[=]符号对其进行新引用,每次需要时触发变化检测。简单易读:)
此致,
我有 2 个解决方案可以解决您的问题
- 使用
ngDoCheck
检测object
数据是否改变 - 通过父组件
object = Object.create(object)
将object
分配给新的内存地址。
好的,所以我的解决方案是:
this.arrayWeNeed.DoWhatWeNeedWithThisArray();
const tempArray = [...arrayWeNeed];
this.arrayWeNeed = [];
this.arrayWeNeed = tempArray;
这触发了我 ngOnChanges
我不得不为它创建一个 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
在我的例子中,ngOnChange
没有捕捉到对象值的变化。响应 api 调用修改了一些对象值。重新初始化对象修复了问题并导致 ngOnChange
在子组件中触发。
类似
this.pagingObj = new Paging(); //This line did the magic
this.pagingObj.pageNumber = response.PageNumber;
当您像这样操作数据时:
this.data.profiles[i].icon.url = '';
那么你应该使用以检测变化:
let array = this.data.profiles.map(x => Object.assign({}, x)); // It will detect changes
由于angular ngOnchanges 无法检测到数组中的变化,因此我们必须为对象分配一个新的引用。每次都有效!
这是一个将 IterableDiffer 与 ngDoCheck 结合使用的示例。如果您需要跟踪随时间变化的变化,IterableDiffer 特别有用,因为它可以让您执行仅迭代 added/changed/removed 个值等操作
一个没有使用 IterableDiffer 的所有优点的简单示例,但它可以工作并显示原理:
export class FeedbackMessagesComponent implements DoCheck {
@Input()
messages: UserFeedback[] = [];
// Example UserFeedback instance { message = 'Ooops', type = Notice }
@HostBinding('style.display')
display = 'none';
private _iterableDiffer: IterableDiffer<UserFeedback>;
constructor(private _iterableDiffers: IterableDiffers) {
this._iterableDiffer = this._iterableDiffers.find([]).create(null);
}
ngDoCheck(): void {
const changes = this._iterableDiffer.diff(this.messages);
if (changes) {
// Here you can do stuff like changes.forEachRemovedItem()
// We know contents of this.messages was changed so update visibility:
this.display = this.messages.length > 0 ? 'block' : 'none';
}
}
}
现在这将根据 myMessagesArray 计数自动 show/hide:
<app-feedback-messages
[messages]="myMessagesArray"
></app-feedback-messages>
假设你有一个嵌套对象,比如
var obj = {"parent": {"child": {....}}}
如果你传递了完整对象的引用,比如
[wholeObj] = "obj"
在那种情况下,你无法检测到子对象的变化,所以为了克服这个问题你也可以通过另一个属性传递子对象的引用,比如
[wholeObj] = "obj" [childObj] = "obj.parent.child"
因此您也可以检测子对象的变化。
ngOnChanges(changes: SimpleChanges) {
if (changes.childObj) {// your logic here}
}
不是一个干净的解决方案,但您可以通过以下方式触发检测:
rawLapsData = JSON.parse(JSON.stringify(rawLapsData))