Phát hiện thay đổi Angular2: ngOnChanges không bắn cho đối tượng lồng nhau


129

Tôi biết tôi không phải là người đầu tiên hỏi về điều này, nhưng tôi không thể tìm thấy câu trả lời trong các câu hỏi trước. Tôi có cái này trong một thành phần

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

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

Trong bộ điều khiển rawLapsdatađược đột biến theo thời gian.

Trong laps, dữ liệu được xuất dưới dạng HTML ở dạng bảng. Điều này thay đổi bất cứ khi nào rawLapsdatathay đổi.

mapThành phần của tôi cần sử dụng ngOnChangeslàm công cụ kích hoạt để vẽ lại các điểm đánh dấu trên Google Map. Vấn đề là ngOnChanges không kích hoạt khi rawLapsDatathay đổi cha mẹ. Tôi có thể làm gì?

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

Cập nhật: ngOnChanges không hoạt động, nhưng có vẻ như lapsData đang được cập nhật. Trong ngInit là một trình lắng nghe sự kiện để thay đổi phóng to cũng gọi this.drawmarkers. Khi tôi thay đổi thu phóng, tôi thực sự thấy một sự thay đổi trong các điểm đánh dấu. Vì vậy, vấn đề duy nhất là tôi không nhận được thông báo tại thời điểm dữ liệu đầu vào thay đổi.

Trong cha mẹ, tôi có dòng này. (Hãy nhớ lại rằng sự thay đổi được phản ánh trong vòng, nhưng không phải trong bản đồ).

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

Và lưu ý rằng this.rawLapsDatachính nó là một con trỏ đến giữa một đối tượng json lớn

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

Mã của bạn không hiển thị cách dữ liệu được cập nhật hoặc loại dữ liệu. Là một thể hiện mới được chỉ định hay chỉ là một thuộc tính của giá trị được sửa đổi?
Günter Zöchbauer

@ GünterZöchbauer Tôi đã thêm dòng từ thành phần chính
Simon H

Tôi đoán gói dòng này zone.run(...)nên làm điều đó sau đó.
Günter Zöchbauer

1
Mảng của bạn (tham chiếu) không thay đổi, vì vậy ngOnChanges()sẽ không được gọi. Bạn có thể sử dụng ngDoCheck()và thực hiện logic của riêng mình để xác định xem nội dung mảng có thay đổi hay không. lapsDatađược cập nhật vì nó có / là một tham chiếu đến cùng một mảng với rawLapsData.
Mark Rajcok

1
1) Trong thành phần vòng lặp, mã / mẫu của bạn lặp qua từng mục trong mảng lapsData và hiển thị nội dung, do đó, có các ràng buộc góc trên mỗi phần dữ liệu được hiển thị. 2) Ngay cả khi Angular không phát hiện bất kỳ thay đổi nào (kiểm tra tham chiếu) đối với các thuộc tính đầu vào của thành phần, nó vẫn (theo mặc định) kiểm tra tất cả các ràng buộc mẫu. Đó là cách mất đi những thay đổi. 3) Thành phần bản đồ có khả năng không có bất kỳ ràng buộc nào trong mẫu của nó với thuộc tính đầu vào lapsData của nó, phải không? Điều đó sẽ giải thích sự khác biệt.
Mark Rajcok

Câu trả lời:


153

rawLapsData tiếp tục trỏ đến cùng một mảng, ngay cả khi bạn sửa đổi nội dung của mảng (ví dụ: thêm các mục, xóa các mục, thay đổi một mục).

Trong quá trình phát hiện thay đổi, khi Angular kiểm tra các thuộc tính đầu vào của các thành phần để thay đổi, nó sử dụng (về cơ bản) ===để kiểm tra bẩn. Đối với mảng, điều này có nghĩa là các tham chiếu mảng (chỉ) được kiểm tra bẩn. Vì rawLapsDatatham chiếu mảng không thay đổi, ngOnChanges()sẽ không được gọi.

Tôi có thể nghĩ về hai giải pháp có thể:

  1. Thực hiện ngDoCheck()và thực hiện logic phát hiện thay đổi của riêng bạn để xác định xem nội dung mảng có thay đổi hay không. (Tài liệu móc vòng đời có một ví dụ .)

  2. Chỉ định một mảng mới cho rawLapsDatabất cứ khi nào bạn thực hiện bất kỳ thay đổi nào đối với nội dung mảng. Sau đó ngOnChanges()sẽ được gọi vì mảng (tham chiếu) sẽ xuất hiện dưới dạng thay đổi.

Trong câu trả lời của bạn, bạn đã đưa ra một giải pháp khác.

Lặp lại một số ý kiến ​​ở đây trên OP:

Tôi vẫn không thấy làm thế nào lapscó thể nhận được sự thay đổi (chắc chắn nó phải sử dụng thứ gì đó tương đương với ngOnChanges()chính nó?) Trong khi mapkhông thể.

  • Trong lapsthành phần, mã / mẫu của bạn lặp qua từng mục trong lapsDatamảng và hiển thị nội dung, do đó, có các ràng buộc góc trên mỗi phần dữ liệu được hiển thị.
  • Ngay cả khi Angular không phát hiện bất kỳ thay đổi nào đối với các thuộc tính đầu vào của thành phần (sử dụng ===kiểm tra), nó vẫn (theo mặc định) kiểm tra bẩn tất cả các ràng buộc mẫu. Khi bất kỳ thay đổi nào trong số đó, Angular sẽ cập nhật DOM. Đó là những gì bạn đang thấy.
  • Thành mapsphần có khả năng không có bất kỳ ràng buộc nào trong mẫu của nó với thuộc tính lapsDatađầu vào của nó , phải không? Điều đó sẽ giải thích sự khác biệt.

Lưu ý rằng lapsDatatrong cả hai thành phần và rawLapsDatatrong thành phần cha mẹ đều trỏ đến cùng một / một mảng. Vì vậy, mặc dù Angular không nhận thấy bất kỳ thay đổi (tham chiếu) nào đối với các lapsDatathuộc tính đầu vào, các thành phần "get" / xem bất kỳ nội dung mảng nào đều thay đổi vì tất cả chúng đều chia sẻ / tham chiếu một mảng. Chúng ta không cần Angular để truyền bá những thay đổi này, giống như chúng ta sẽ làm với kiểu nguyên thủy (chuỗi, số, boolean). Nhưng với loại nguyên thủy, mọi thay đổi đối với giá trị sẽ luôn kích hoạt ngOnChanges()- đó là điều bạn khai thác trong câu trả lời / giải pháp của mình.

Như bạn có thể nhận ra bây giờ các thuộc tính đầu vào đối tượng có hành vi tương tự như các thuộc tính đầu vào mảng.


Vâng, tôi đã biến đổi một vật thể lồng nhau sâu sắc và tôi đoán điều đó khiến Angular khó phát hiện ra những thay đổi trong cấu trúc. Không phải sở thích của tôi để thay đổi nhưng đây là bản dịch XML và tôi không thể để mất bất kỳ dữ liệu nào xung quanh vì tôi muốn tạo lại xml một lần nữa vào cuối
Simon H

7
@SimonH, "khó để Angular phát hiện ra các thay đổi trong cấu trúc" - Nói rõ hơn, Angular thậm chí không nhìn vào bên trong các thuộc tính đầu vào là mảng hoặc đối tượng để thay đổi. Nó chỉ xem nếu giá trị thay đổi - đối với các đối tượng và mảng, giá trị là tham chiếu. Đối với các kiểu nguyên thủy, giá trị là ... giá trị. (Tôi không chắc là tôi có tất cả các biệt ngữ thẳng, nhưng bạn hiểu ý.)
Mark Rajcok

11
Câu trả lời chính xác. Nhóm Angular2 rất cần xuất bản một tài liệu chi tiết, có thẩm quyền về nội bộ của phát hiện thay đổi.

Nếu tôi thực hiện chức năng trong doCheck, trong trường hợp của tôi, kiểm tra do đang gọi rất nhiều lần. Bạn có thể vui lòng cho tôi biết bất kỳ cách nào khác?
Mr_Perinf

@MarkRajcok bạn có thể vui lòng giúp tôi giải quyết vấn đề này stackoverflow.com/questions/50166996/ không
Nikson

27

Không phải là cách tiếp cận sạch nhất, nhưng bạn chỉ có thể sao chép đối tượng mỗi khi bạn thay đổi giá trị?

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

Tôi nghĩ rằng tôi thích cách tiếp cận này hơn là thực hiện của riêng bạn ngDoCheck()nhưng có lẽ ai đó như @ GünterZöchbauer có thể hòa nhập.


Nếu bạn không chắc chắn liệu trình duyệt được nhắm mục tiêu có hỗ trợ <Object.assign ()> hay không, bạn cũng có thể xâu chuỗi thành chuỗi json và phân tích lại thành json, điều này cũng sẽ tạo ra một đối tượng mới ...
Guntram

1
@Guntram Hoặc một polyfill?
David

12

Trong trường hợp Mảng bạn có thể làm như thế này:

Trong .tstệp (Thành phần cha mẹ) nơi bạn đang cập nhật, hãy rawLapsDatalàm như thế này:

rawLapsData = somevalue; // change detection will not happen

Giải pháp:

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

ngOnChangessẽ gọi thành phần con


10

Là một phần mở rộng cho giải pháp thứ hai của Mark Rajcok

Gán một mảng mới cho rawLapsData bất cứ khi nào bạn thực hiện bất kỳ thay đổi nào đối với nội dung mảng. Sau đó ngOnChanges () sẽ được gọi vì mảng (tham chiếu) sẽ xuất hiện dưới dạng thay đổi

bạn có thể sao chép nội dung của mảng như thế này:

rawLapsData = rawLapsData.slice(0);

Tôi đang đề cập đến điều này bởi vì

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

không làm việc cho tôi. Tôi hi vọng cái này giúp được.


8

Nếu dữ liệu đến từ thư viện bên ngoài, bạn có thể cần chạy câu lệnh upate dữ liệu bên trong zone.run(...). Tiêm zone: NgZonecho điều này. Nếu bạn có thể chạy khởi tạo thư viện bên ngoài zone.run(), thì bạn có thể không cần zone.run()sau này.


Như đã lưu ý trong các bình luận cho OP, những thay đổi không nằm ngoài mà nằm sâu trong một đối tượng json
Simon H

1
Như câu trả lời của bạn nói, chúng ta vẫn cần chạy một cái gì đó để giữ mọi thứ đồng bộ trong Angular2, như angular1 phải $scope.$applykhông?
Pankaj Parkar

1
Nếu một cái gì đó được bắt đầu từ bên ngoài Angular, API, được vá bởi vùng không được sử dụng và Angular sẽ không được thông báo về những thay đổi có thể. Vâng, zone.runtương tự như $scope.apply.
Günter Zöchbauer

6

Sử dụng ChangeDetectorRef.detectChanges()để báo cho Angular chạy phát hiện thay đổi khi bạn chỉnh sửa một đối tượng lồng nhau (mà nó bỏ lỡ với kiểm tra bẩn của nó).


Nhưng bằng cách nào ? Tôi đang cố gắng sử dụng cái này, tôi muốn kích hoạt ChangeDetection ở một đứa trẻ sau khi cha mẹ đẩy một vật phẩm mới theo cách 2 giới hạn [(Bộ sưu tập)].
Deunz

Vấn đề không phải là phát hiện thay đổi không xảy ra, mà là phát hiện thay đổi đó không phát hiện ra các thay đổi! Vì vậy, điều này dường như không phải là một giải pháp! Tại sao câu trả lời này có năm upvote?
Shahryar Saljoughi

5

Tôi có 2 giải pháp để giải quyết vấn đề của bạn

  1. Sử dụng ngDoCheckđể phát hiệnobject dữ liệu thay đổi hay không
  2. Gán objectvào một địa chỉ bộ nhớ mới object = Object.create(object)từ thành phần cha.

2
Liệu có sự khác biệt đáng chú ý nào giữa Object.create (object) so với Object.assign ({}, object) không?
Daniel Galarza

3

Giải pháp 'hack' của tôi là

   <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>

selectTps thay đổi cùng lúc với rawLapsData và điều đó cho phép ánh xạ một cơ hội khác để phát hiện sự thay đổi thông qua một kiểu nguyên thủy đối tượng đơn giản hơn . Nó KHÔNG thanh lịch, nhưng nó hoạt động.


Tôi thấy khó theo dõi tất cả các thay đổi trên các thành phần khác nhau trong cú pháp mẫu, đặc biệt trên các ứng dụng quy mô trung bình / lớn. Thông thường tôi sử dụng trình phát và đăng ký sự kiện được chia sẻ để truyền dữ liệu (giải pháp nhanh) hoặc triển khai mẫu Redux (thông qua Rx.Subject) cho việc này (khi có thời gian để lên kế hoạch) ...
Sasxa

3

Phát hiện thay đổi không được kích hoạt khi bạn thay đổi thuộc tính của một đối tượng (bao gồm cả đối tượng lồng nhau). Một giải pháp sẽ là gán lại một tham chiếu đối tượng mới bằng cách sử dụng hàm 'lodash' clone ().

import * as _ from 'lodash';

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

2

Đây là một bản hack khiến tôi gặp rắc rối với cái này.

Vì vậy, một kịch bản tương tự với OP - Tôi đã có một thành phần Angular lồng nhau cần dữ liệu được truyền vào nó, nhưng các đầu vào trỏ đến một mảng và như đã đề cập ở trên, Angular không thấy sự thay đổi vì nó không kiểm tra nội dung của mảng.

Vì vậy, để sửa nó, tôi chuyển đổi mảng thành một chuỗi cho Angular để phát hiện sự thay đổi, và sau đó trong thành phần lồng nhau, tôi tách (',') chuỗi trở lại thành một mảng và ngày hạnh phúc của nó một lần nữa.


1
Kinh quá! Cách tiếp cận mảng.slice (0) sạch hơn nhiều.
Chris Haines

2

Tôi vấp phải cùng một nhu cầu. Và tôi đã đọc rất nhiều về điều này vì vậy, đây là đồng của tôi về chủ đề này.

Nếu bạn muốn phát hiện thay đổi của mình khi đẩy, thì bạn sẽ có nó khi bạn thay đổi giá trị của một đối tượng bên trong phải không? Và bạn cũng sẽ có nó nếu bằng cách nào đó, bạn loại bỏ các đối tượng.

Như đã nói, sử dụng thay đổiDetectionStrargety.onPush

Giả sử bạn có thành phần này bạn đã tạo, với changeDetectionStrargety.onPush:

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

Sau đó, bạn sẽ đẩy một mục và kích hoạt phát hiện thay đổi:

myCollection.push(anItem);
refresh();

hoặc bạn sẽ xóa một mục và kích hoạt phát hiện thay đổi:

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

hoặc bạn sẽ thay đổi giá trị attrbibute cho một mục và kích hoạt phát hiện thay đổi:

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

Nội dung làm mới:

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

Phương thức lát trả về cùng một Mảng chính xác và dấu [=] tạo tham chiếu mới cho nó, kích hoạt phát hiện thay đổi mỗi khi bạn cần. Dễ dàng và dễ đọc :)

Trân trọng,


2

Tôi đã thử tất cả các giải pháp được đề cập ở đây, nhưng vì một số lý do ngOnChanges()vẫn không kích hoạt cho tôi. Vì vậy, tôi đã gọi nó với this.ngOnChanges()sau khi gọi dịch vụ phục hồi mảng của tôi và nó hoạt động .... đúng không? chắc là không. Khéo léo? trời ơi không. Làm? Đúng!


1

ok vậy giải pháp của tôi cho việc này là:

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

Và điều này kích hoạt tôi ngOnChanges


0

Tôi đã phải tạo một hack cho nó -

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

Trong trường hợp của tôi, đó là những thay đổi về giá trị đối tượng mà ngOnChangekhông bắt được. Một vài giá trị đối tượng được sửa đổi để đáp ứng cuộc gọi api. Tái khởi động lại đối tượng đã khắc phục sự cố và gây rangOnChange kích hoạt trong thành phần con.

Cái gì đó như

 this.pagingObj = new Paging(); //This line did the magic
 this.pagingObj.pageNumber = response.PageNumber;
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.