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 ปัญหาคือ 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ünterZöchbauer

@ GünterZöchbauerฉันเพิ่มบรรทัดจากองค์ประกอบหลัก
Simon H

ฉันเดาว่าการตัดบรรทัดนี้zone.run(...)ควรทำในตอนนั้น
GünterZöchbauer

1
อาร์เรย์ (การอ้างอิง) ของคุณไม่เปลี่ยนแปลงดังนั้นngOnChanges()จะไม่ถูกเรียก คุณสามารถใช้ngDoCheck()และปรับใช้ตรรกะของคุณเองเพื่อตรวจสอบว่าเนื้อหาอาร์เรย์มีการเปลี่ยนแปลงหรือไม่ lapsDataมีการปรับปรุงเพราะมันมี / rawLapsDataมีการอ้างอิงถึงอาร์เรย์เดียวกับ
Mark Rajcok

1
1) ในองค์ประกอบรอบรหัส / แม่แบบของคุณจะวนซ้ำแต่ละรายการในอาร์เรย์ lapsData และแสดงเนื้อหาดังนั้นจึงมีการผูกเชิงมุมกับข้อมูลแต่ละชิ้นที่แสดง 2) แม้ว่า Angular จะไม่ตรวจพบการเปลี่ยนแปลงใด ๆ (การตรวจสอบการอ้างอิง) กับคุณสมบัติการป้อนข้อมูลของคอมโพเนนต์ แต่ก็ยังตรวจสอบการเชื่อมโยงเทมเพลตทั้งหมด (โดยค่าเริ่มต้น) นั่นเป็นวิธีที่รอบรับการเปลี่ยนแปลง 3) องค์ประกอบแผนที่น่าจะไม่มีการผูกใด ๆ ในเทมเพลตกับคุณสมบัติการป้อนข้อมูล lapsData ใช่ไหม นั่นจะอธิบายความแตกต่าง
Mark Rajcok

คำตอบ:


154

rawLapsData ยังคงชี้ไปที่อาร์เรย์เดียวกันแม้ว่าคุณจะแก้ไขเนื้อหาของอาร์เรย์ (เช่นเพิ่มรายการลบรายการเปลี่ยนรายการ)

ในระหว่างการตรวจจับการเปลี่ยนแปลงเมื่อ Angular ตรวจสอบคุณสมบัติอินพุตของคอมโพเนนต์สำหรับการเปลี่ยนแปลงจะใช้ (โดยพื้นฐาน) ===สำหรับการตรวจสอบสกปรก สำหรับอาร์เรย์หมายความว่าการอ้างอิงอาร์เรย์ (เท่านั้น) ถูกตรวจสอบว่าสกปรก เนื่องจากการrawLapsDataอ้างอิงอาร์เรย์ไม่เปลี่ยนแปลงจึงngOnChanges()ไม่ถูกเรียก

ฉันคิดวิธีแก้ปัญหาที่เป็นไปได้สองวิธี:

  1. ใช้งานngDoCheck()และดำเนินการตรรกะการตรวจจับการเปลี่ยนแปลงของคุณเองเพื่อตรวจสอบว่าเนื้อหาอาร์เรย์มีการเปลี่ยนแปลงหรือไม่ (เอกสาร Lifecycle Hooks มีตัวอย่าง )

  2. กำหนดอาร์เรย์ใหม่rawLapsDataเมื่อใดก็ตามที่คุณทำการเปลี่ยนแปลงใด ๆ กับเนื้อหาอาร์เรย์ จากนั้นngOnChanges()จะถูกเรียกใช้เนื่องจากอาร์เรย์ (การอ้างอิง) จะปรากฏเป็นการเปลี่ยนแปลง

ในคำตอบของคุณคุณได้หาวิธีแก้ปัญหาอื่น

แสดงความคิดเห็นซ้ำที่นี่ใน OP:

ฉันยังไม่เห็นว่าlapsจะรับการเปลี่ยนแปลงได้อย่างไร (แน่นอนว่าต้องใช้สิ่งที่เทียบเท่ากับngOnChanges()ตัวมันเอง?) ในขณะที่mapทำไม่ได้

  • ในlapsคอมโพเนนต์โค้ด / เทมเพลตของคุณจะวนลูปเหนือแต่ละรายการในlapsDataอาร์เรย์และแสดงเนื้อหาดังนั้นจึงมีการผูกเชิงมุมกับข้อมูลแต่ละชิ้นที่แสดง
  • แม้ว่า Angular จะตรวจไม่พบการเปลี่ยนแปลงใด ๆ ในคุณสมบัติอินพุตของคอมโพเนนต์ (โดยใช้การ===ตรวจสอบ) แต่ก็ยังตรวจสอบการผูกเทมเพลตทั้งหมด (โดยค่าเริ่มต้น) สกปรก เมื่อมีการเปลี่ยนแปลง Angular จะอัปเดต DOM นั่นคือสิ่งที่คุณเห็น
  • mapsองค์ประกอบแนวโน้มที่ไม่ได้มีการผูกใด ๆ ในแม่แบบในการของlapsDataสถานที่ให้บริการการป้อนข้อมูลใช่มั้ย? นั่นจะอธิบายความแตกต่าง

โปรดสังเกตว่าlapsDataในคอมโพเนนต์ทั้งสองและrawLapsDataในองค์ประกอบหลักทั้งหมดชี้ไปที่อาร์เรย์เดียวกัน / หนึ่งอาร์เรย์ ดังนั้นแม้ว่า Angular จะไม่สังเกตเห็นการเปลี่ยนแปลงใด ๆ (การอ้างอิง) ในlapsDataคุณสมบัติอินพุต แต่คอมโพเนนต์ "get" / ดูเนื้อหาอาร์เรย์ใด ๆ จะเปลี่ยนแปลงเนื่องจากทั้งหมดแชร์ / อ้างอิงอาร์เรย์เดียว เราไม่จำเป็นต้องใช้ Angular เพื่อเผยแพร่การเปลี่ยนแปลงเหล่านี้เช่นเดียวกับที่เราทำกับประเภทดั้งเดิม (สตริง, ตัวเลข, บูลีน) แต่ด้วยประเภทดั้งเดิมการเปลี่ยนแปลงค่าใด ๆ จะทริกเกอร์เสมอngOnChanges()ซึ่งเป็นสิ่งที่คุณใช้ประโยชน์จากคำตอบ / วิธีแก้ปัญหาของคุณ

ดังที่คุณอาจทราบได้แล้วว่าคุณสมบัติการป้อนข้อมูลวัตถุมีลักษณะการทำงานเช่นเดียวกับคุณสมบัติอินพุตอาร์เรย์


ใช่ฉันกำลังกลายพันธุ์วัตถุที่ซ้อนกันอยู่ลึก ๆ และฉันเดาว่านั่นทำให้ Angular มองเห็นการเปลี่ยนแปลงในโครงสร้างได้ยาก ไม่ใช่ความชอบของฉันที่จะกลายพันธุ์ แต่เป็น XML ที่แปลแล้วและฉันไม่สามารถสูญเสียข้อมูลรอบข้างใด ๆ ได้เนื่องจากฉันต้องการสร้าง xml ใหม่อีกครั้งในตอนท้าย
Simon H

7
@SimonH "ยากสำหรับ Angular ที่จะสังเกตเห็นการเปลี่ยนแปลงในโครงสร้าง" - เพื่อความชัดเจน Angular ไม่ได้มองเข้าไปในคุณสมบัติอินพุตที่เป็นอาร์เรย์หรือวัตถุสำหรับการเปลี่ยนแปลง เพียงแค่ดูว่าค่าเปลี่ยนไปหรือไม่ - สำหรับวัตถุและอาร์เรย์ค่านี้เป็นข้อมูลอ้างอิง สำหรับประเภทดั้งเดิมค่าคือ ... ค่า (ฉันไม่แน่ใจว่าฉันมีศัพท์เฉพาะตรงทั้งหมด แต่คุณเข้าใจแล้ว)
Mark Rajcok

11
คำตอบที่ดี ทีม Angular2 จำเป็นอย่างยิ่งที่จะต้องเผยแพร่เอกสารที่มีรายละเอียดและเชื่อถือได้เกี่ยวกับการตรวจจับการเปลี่ยนแปลงภายใน

ถ้าฉันใช้ฟังก์ชันใน doCheck ในกรณีของฉันการตรวจสอบกำลังเรียกร้องหลายครั้ง คุณช่วยบอกวิธีอื่นได้ไหม?
Mr_Perfect

@MarkRajcok ได้โปรดช่วยฉันแก้ปัญหานี้stackoverflow.com/questions/50166996/…
Nikson

27

ไม่ใช่วิธีที่สะอาดที่สุด แต่คุณสามารถโคลนวัตถุทุกครั้งที่คุณเปลี่ยนค่าได้หรือไม่?

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

ฉันคิดว่าฉันชอบแนวทางนี้มากกว่าการใช้วิธีของคุณเองngDoCheck()แต่อาจมีคนอย่าง @ GünterZöchbauerเข้ามา


หากคุณไม่แน่ใจว่าเบราว์เซอร์เป้าหมายรองรับ <Object.assign ()> หรือไม่คุณยังสามารถรวมสตริงเข้าไปในสตริง json และแยกวิเคราะห์กลับไปที่ json ซึ่งจะสร้างอ็อบเจกต์ใหม่ด้วย ...
Guntram

1
@Guntram หรือ polyfill?
เดวิด

12

ในกรณีของ Arrays คุณสามารถทำได้ดังนี้:

ใน.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

หากข้อมูลที่มาจากห้องสมุดภายนอกคุณอาจจำเป็นต้องเรียกใช้คำสั่งข้อมูล upate zone.run(...)ภายใน ฉีดzone: NgZoneเพื่อสิ่งนี้ หากคุณสามารถเรียกใช้การสร้างอินสแตนซ์ของไลบรารีภายนอกภายในzone.run()อยู่แล้วคุณอาจไม่ต้องการในzone.run()ภายหลัง


ตามที่ระบุไว้ในความคิดเห็นต่อ OP การเปลี่ยนแปลงไม่ได้อยู่ภายนอก แต่อยู่ลึกเข้าไปในวัตถุ json
Simon H

1
ตามคำตอบของคุณเรายังคงต้องเรียกใช้บางสิ่งเพื่อให้สิ่งต่างๆสอดคล้องกันใน Angular2 เช่นเดียวกับ angular1 $scope.$applyหรือไม่?
Pankaj Parkar

1
หากบางสิ่งเริ่มต้นจากภายนอก Angular จะไม่ใช้ API ที่แพตช์ตามโซนและ Angular จะไม่ได้รับการแจ้งเตือนเกี่ยวกับการเปลี่ยนแปลงที่เป็นไปได้ ใช่zone.runคล้ายกับ$scope.apply.
GünterZöchbauer

6

ใช้ChangeDetectorRef.detectChanges()เพื่อบอกให้ Angular รันการตรวจจับการเปลี่ยนแปลงเมื่อคุณแก้ไขอ็อบเจ็กต์ที่ซ้อนกัน (ว่าพลาดไปกับการตรวจสอบสกปรก)


แต่อย่างไร? ฉันกำลังพยายามใช้สิ่งนี้ฉันต้องการทริกเกอร์ changeDetection ในเด็กหลังจากที่ผู้ปกครองผลักดันรายการใหม่ใน 2 ทางที่มีขอบเขต [(คอลเล็กชัน)]
Deunz

ปัญหาไม่ได้อยู่ที่การตรวจจับการเปลี่ยนแปลงไม่เกิดขึ้น แต่การตรวจจับการเปลี่ยนแปลงนั้นไม่พบการเปลี่ยนแปลง! ดังนั้นสิ่งนี้ดูเหมือนจะไม่ใช่วิธีแก้ปัญหา! ทำไมคำตอบนี้ถึงได้รับคะแนนโหวตถึง 5 ครั้ง
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 และนั่นทำให้แผนที่มีโอกาสอีกครั้งในการตรวจจับการเปลี่ยนแปลงผ่านประเภทวัตถุดั้งเดิมที่ง่ายกว่า มันไม่หรูหรา แต่ใช้งานได้


ฉันพบว่ามันยากที่จะติดตามการเปลี่ยนแปลงทั้งหมดของส่วนประกอบต่างๆในไวยากรณ์เทมเพลตโดยเฉพาะในแอปขนาดกลาง / ใหญ่ โดยปกติฉันจะใช้ตัวปล่อยเหตุการณ์ที่แชร์และการสมัครสมาชิกเพื่อส่งผ่านข้อมูล (วิธีแก้ปัญหาอย่างรวดเร็ว) หรือใช้รูปแบบ Redux (ผ่าน Rx.Subject) สำหรับสิ่งนี้ (เมื่อมีเวลาวางแผน) ...
Sasxa

3

การตรวจจับการเปลี่ยนแปลงจะไม่ถูกทริกเกอร์เมื่อคุณเปลี่ยนคุณสมบัติของอ็อบเจ็กต์ (รวมถึงอ็อบเจ็กต์ที่ซ้อนกัน) วิธีแก้ปัญหาหนึ่งคือการกำหนดการอ้างอิงวัตถุใหม่โดยใช้ฟังก์ชัน 'lodash' clone ()

import * as _ from 'lodash';

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

2

นี่คือแฮ็คที่ทำให้ฉันหมดปัญหากับอันนี้

ดังนั้นสถานการณ์ที่คล้ายกันกับ OP - ฉันมีส่วนประกอบเชิงมุมที่ซ้อนกันซึ่งต้องการข้อมูลที่ส่งผ่านลงไป แต่อินพุตชี้ไปที่อาร์เรย์และดังที่ได้กล่าวมาแล้ว Angular ไม่เห็นการเปลี่ยนแปลงเนื่องจากไม่ได้ตรวจสอบ เนื้อหาของอาร์เรย์

ดังนั้นเพื่อแก้ไขฉันแปลงอาร์เรย์เป็นสตริงสำหรับ Angular เพื่อตรวจจับการเปลี่ยนแปลงจากนั้นในองค์ประกอบที่ซ้อนกันฉันแยก (',') สตริงกลับไปที่อาร์เรย์และวันแห่งความสุขอีกครั้ง


1
yuck! วิธี array.slice (0) นั้นสะอาดกว่ามาก
Chris Haines

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();
}

เมธอด slice จะส่งคืน Array เดียวกันทุกประการและเครื่องหมาย [=] จะทำการอ้างอิงใหม่เพื่อเรียกใช้การตรวจจับการเปลี่ยนแปลงทุกครั้งที่คุณต้องการ ง่ายและอ่านได้ :)

ความนับถือ,


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;
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.