【问题标题】:Angular2 change detection: ngOnChanges not firing for nested objectAngular2 更改检测:ngOnChanges 未触发嵌套对象
【发布时间】:2016-04-20 05:03:23
【问题描述】:

我知道我不是第一个提出这个问题的人,但我在前面的问题中找不到答案。我在一个组件中有这个

<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 作为触发器来重绘谷歌地图上的标记。问题是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;

【问题讨论】:

  • 您的代码没有显示数据的更新方式或数据的类型。是分配了一个新实例还是只是修改了值的属性?
  • @GünterZöchbauer 我添加了父组件的行
  • 我想在zone.run(...) 中包含这一行应该这样做。
  • 你的数组(引用)没有改变,所以ngOnChanges() 不会被调用。您可以使用ngDoCheck() 并实现自己的逻辑来确定数组内容是否已更改。 lapsData 已更新,因为它具有/是对与 rawLapsData 相同的数组的引用。
  • 1) 在 laps 组件中,您的代码/模板循环遍历 lapsData 数组中的每个条目,并显示内容,因此显示的每条数据都有 Angular 绑定。 2) 即使 Angular 没有检测到对组件输入属性的任何更改(引用检查),它仍然(默认情况下)检查所有模板绑定。这就是圈数如何适应变化。 3) maps 组件的模板中可能没有任何绑定到它的 lapsData 输入属性,对吧?这可以解释差异。

标签: angular angular-lifecycle-hooks


【解决方案1】:

在您更新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 将在子组件中调用

【讨论】:

  • 不应该是 [...somevalue] ,因为你说它是一个数组?
  • 是的,有人编辑了我的帖子,我不知道为什么,@Tim 你为什么错误地编辑了我的答案?
  • 我没有更改您帖子的内容,只是编辑了一些格式,因为它不可读。
【解决方案2】:

不是一个干净的解决方案,但您可以通过以下方式触发检测:

rawLapsData = JSON.parse(JSON.stringify(rawLapsData))

【讨论】:

    【解决方案3】:

    假设你有一个嵌套对象,比如

    var obj = {"parent": {"child": {....}}}

    如果你传递了完整对象的引用,比如

    [wholeObj] = "obj"

    在这种情况下,您无法检测到子对象的变化,因此要解决此问题,您还可以通过另一个属性传递子对象的引用,例如

    [wholeObj] = "obj" [childObj] = "obj.parent.child"

    所以你也可以检测子对象的变化。

    ngOnChanges(changes: SimpleChanges) { 
        if (changes.childObj) {// your logic here}
    }
    

    【讨论】:

      【解决方案4】:

      这是一个使用 IterableDiffer 和 ngDoCheck 的示例。如果您需要跟踪随时间的变化,IterableDiffer 特别有用,因为它可以让您执行诸如仅迭代添加/更改/删除的值等操作。

      一个简单的例子,没有使用 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 计数自动显示/隐藏:

      <app-feedback-messages
        [messages]="myMessagesArray"
      ></app-feedback-messages>

      【讨论】:

        【解决方案5】:

        当您像这样操作数据时:

        this.data.profiles[i].icon.url = '';
        

        那么你应该使用来检测变化:

        let array = this.data.profiles.map(x => Object.assign({}, x)); // It will detect changes
        

        由于 Angular ngOnchanges 无法检测到数组、对象的变化,那么我们必须分配一个新的引用。每次都有效!

        【讨论】:

          【解决方案6】:

          在我的例子中,ngOnChange 没有捕捉到对象值的变化。一些对象值被修改以响应 api 调用。重新初始化对象修复了问题并导致ngOnChange 在子组件中触发。

          类似

           this.pagingObj = new Paging(); //This line did the magic
           this.pagingObj.pageNumber = response.PageNumber;
          

          【讨论】:

            【解决方案7】:

            我必须为它创建一个 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
            

            【讨论】:

              【解决方案8】:

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

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

              这会触发我的 ngOnChanges

              【讨论】:

                【解决方案9】:

                我有 2 个解决方案可以解决您的问题

                1. 使用ngDoCheck检测object数据是否改变
                2. object 分配给来自父组件的object = Object.create(object) 的新内存地址。

                【讨论】:

                • Object.create(object) 与 Object.assign({}, object) 之间会有什么显着差异吗?
                【解决方案10】:

                我偶然发现了同样的需求。我在这方面读了很多,这是我在这个主题上的铜。

                如果您希望在推送时检测更改,那么当您更改内部对象的值时您会拥有它吗?如果以某种方式删除对象,您也会拥有它。

                如前所述,使用 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,并且 [ = ] 符号对其进行了新的引用,每次需要时都会触发更改检测。简单易读:)

                问候,

                【讨论】:

                • 像魅力一样为我工作......很好的答案......谢谢
                【解决方案11】:

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

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

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

                1. 实现 ngDoCheck() 并执行您自己的更改检测逻辑以确定数组内容是否已更改。 (Lifecycle Hooks 文档有 an example。)

                2. 每当您对数组内容进行任何更改时,都会将新数组分配给rawLapsData。然后ngOnChanges() 将被调用,因为数组(引用)将显示为更改。

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

                在 OP 上重复一些 cmets:

                我仍然不明白 laps 是如何接受更改的(肯定它必须使用与 ngOnChanges() 本身等效的东西?)而 map 不能。

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

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

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

                【讨论】:

                • 是的,我正在改变一个深度嵌套的对象,我猜这让 Angular 很难发现结构中的变化。我不喜欢变异,但这是翻译后的 XML,我不能丢失任何周围的数据,因为我想在最后重新创建 xml
                • @SimonH,“Angular 很难发现结构中的变化”——为了清楚起见,Angular 甚至不会查看作为数组或对象的输入属性以进行更改。它只查看值是否发生了变化——对于对象和数组,值是引用。对于原始类型,值是……值。 (我不确定我是否掌握了所有行话,但你明白了。)
                • 很好的答案。 Angular2 团队迫切需要发布一份关于变更检测内部的详细、权威的文档。
                • 如果我在 doCheck 中执行功能,在我的情况下,do check 会调用很多次。你能告诉我其他方法吗?
                • @MarkRajcok 你能帮我解决这个问题吗stackoverflow.com/questions/50166996/…
                【解决方案12】:

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

                import * as _ from 'lodash';
                
                this.foo = _.clone(this.foo);
                

                【讨论】:

                  【解决方案13】:

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

                  每当您对 rawLapsData 进行任何更改时,都会为 数组内容。然后 ngOnChanges() 将被调用,因为数组 (参考)将显示为更改

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

                  rawLapsData = rawLapsData.slice(0);
                  

                  我提到这个是因为

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

                  对我不起作用。我希望这会有所帮助。

                  【讨论】:

                  • 有了 ES6 扩展操作符,现在你可以用 [...rawLapsData] 做同样的事情
                  【解决方案14】:

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

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

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

                  【讨论】:

                  • 如果您不确定目标浏览器是否支持 ,您也可以将其字符串化为 json 字符串并解析回 json,这也会创建一个新对象。 .
                  • @Guntram 还是 polyfill?
                  【解决方案15】:

                  使用 ChangeDetectorRef.detectChanges() 告诉 Angular 在您编辑嵌套对象时运行更改检测(它会因脏检查而错过)。

                  【讨论】:

                  • 但是怎么做呢?我正在尝试使用它,我想在父级以 2 路有界 [( Collection )] 推送新项目后触发子级的 changeDetection。
                  • 问题不是没有发生变化检测,而是变化检测没有检测到变化!所以这似乎不是一个解决方案!为什么这个答案得到了 5 票赞成?
                  • @ShahryarSaljoughi 答案得到了赞成,因为它是解决方案之一。 detectChanges() 将强制重新渲染。
                  【解决方案16】:

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

                  所以与 OP 类似的场景 - 我有一个嵌套的 Angular 组件,需要将数据向下传递给它,但输入指向一个数组,如上所述,Angular 并没有看到它所做的更改不检查数组的内容。

                  因此,为了修复它,我将数组转换为字符串以供 Angular 检测更改,然后在嵌套组件中,我将字符串拆分(',')回数组并再次度过快乐的日子。

                  【讨论】:

                  • 呸! array.slice(0) 方法更简洁。
                  • 当你得到一个带有逗号的元素时,快乐的日子将一去不复返了;)
                  【解决方案17】:

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

                  【讨论】:

                  • 如 OP 的 cmets 中所述,更改不是外部的,而是 json 对象的深处
                  • 正如您的回答所说,我们仍然需要运行一些东西以在 Angular2 中保持同步,比如 angular1 它是 $scope.$apply?
                  • 如果从 Angular 外部启动某些内容,则不会使用按区域修补的 API,并且 Angular 不会收到有关可能更改的通知。是的,zone.run 类似于 $scope.apply
                  【解决方案18】:

                  我的“破解”解决方案是

                     <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 原始类型检测到变化。它并不优雅,但它确实有效。

                  【讨论】:

                  • 我发现很难跟踪模板语法中各种组件的所有更改,特别是在中/大型应用程序上。通常我使用共享事件发射器和订阅来传递数据(快速解决方案),或者为此(有时间计划时)实现 Redux 模式(通过 Rx.Subject)...
                  猜你喜欢
                  • 1970-01-01
                  • 2021-09-02
                  • 2020-11-22
                  • 2019-04-21
                  • 2017-06-19
                  • 1970-01-01
                  • 2017-06-02
                  • 1970-01-01
                  相关资源
                  最近更新 更多