如何检测@Input()值在Angular中的变化?

我有一个父组件(组件分类),一个子组件(视频列表接口)和一个ApiService。

我的大部分工作都很好,即每个组件都可以访问json api并通过observable获取其相关数据。

目前视频列表组件只获取所有视频,我想将其过滤为特定类别中的视频,我通过@Input()将分类ID传递给孩子来实现这一点。

CategoryComponent.html

<video-list *ngIf="category" [categoryId]="category.id"></video-list>

这是有效的,当父分类组件类别发生变化时,分类ID值会通过@Input()传递,但我需要在VideoListComponent中检测到这一点,并通过APIService(使用新的分类ID)重新请求视频数组。

在AngularJS中,我会对变量执行$watch。处理此问题的最佳方法是什么?

546545 次浏览

在您的组件中使用ngOnChanges()生命周期方法。

在数据绑定属性被调用后立即调用ngOnChange检查并在查看和内容子级被检查之前,如果至少其中一个已经改变了

这里是文档

在函数签名中使用SimpleChanges类型时,我在控制台以及编译器和IDE中遇到错误。为了防止错误,请改用签名中的any关键字。

ngOnChanges(changes: any) {console.log(changes.myInput.currentValue);}

编辑:

正如Jon在下面指出的,当使用括号符号而不是点符号时,您可以使用SimpleChanges签名。

ngOnChanges(changes: SimpleChanges) {console.log(changes['myInput'].currentValue);}

实际上,当输入在angular2+中的子组件中发生变化时,有两种方法可以检测并采取行动:

  1. 您可以使用生命周期方法,如旧答案中提到的:
    @Input() categoryId: string;        
ngOnChanges(changes: SimpleChanges) {        
this.doSomething(changes.categoryId.currentValue);// You can also use categoryId.previousValue and// categoryId.firstChange for comparing old and new values        
}    

文档链接:ngOn更改,简单的改变,简单变更
示例:查看这个混蛋

  1. 或者,您也可以使用输入属性设置器,如下所示:
    private _categoryId: string;    
@Input() set categoryId(value: string) {    
this._categoryId = value;this.doSomething(this._categoryId);    
}    
get categoryId(): string {    
return this._categoryId;    
}

文档链接:看这里

演示示例:看这个混蛋

你应该使用哪种方法?

如果您的组件有多个输入,那么,如果您使用ngOn更改(),您将在ngOn更改()中一次获得所有输入的所有更改。使用这种方法,您还可以比较已更改的输入的当前值和以前的值,并采取相应的操作。

但是,如果你想在只有特定的单个输入更改时执行某些操作(并且你不关心其他输入),那么使用输入属性设置器可能会更简单。然而,这种方法不提供一种内置的方式来比较更改输入的先前和当前值(你可以使用ngOnChange生命周期方法轻松完成)。

编辑2017-07-25:角度变化检测在某些情况下可能仍然不会开火

通常,每当父组件更改它传递给子组件假设数据是JS原始数据类型(字符串、数字、布尔值)的数据时,setter和ngOnChange的更改检测都会触发。但是,在以下场景中,它不会触发,您必须采取额外的操作才能使其工作。

  1. 如果您使用嵌套对象或数组(而不是JS原始数据类型)将数据从父级传递给子级,则更改检测(使用setter或ngChange)可能不会触发,正如user: muetzerich的回答中也提到的那样。对于解决方案,请查看这里

  2. 如果您在角度上下文之外(即外部)更改数据,那么角度将不知道更改。您可能必须在组件中使用ChangeDetectorRef或NgZone来使角度感知外部更改,从而触发更改检测。请参阅这个

我只是想补充一下,还有另一个名为DoCheck的生命周期钩子,如果@Input值不是原始值,它很有用。

我有一个数组作为Input,所以当内容发生变化时,这不会触发OnChanges事件(因为Angular所做的检查是“简单”的,并不深入,所以数组仍然是一个数组,即使数组上的内容发生了变化)。

然后我实现一些自定义检查代码来决定是否要使用更改后的数组更新我的视图。

你还可以有一个observable,它会触发父component(CategoryComponent)中的更改,并在子组件的订阅中执行你想执行的操作。(videoListComponent

service.ts

public categoryChange$ : ReplaySubject<any> = new ReplaySubject(1);

CategoryComponent.ts

public onCategoryChange(): void {service.categoryChange$.next();}

videoListComponent.ts

public ngOnInit(): void {service.categoryChange$.subscribe(() => {// do your logic});}

最安全的选择是使用@Input参数的共享服务改为。此外,@Input参数不会检测复杂嵌套对象类型的更改。

一个简单的服务示例如下:

Service.ts

import { Injectable } from '@angular/core';import { Subject } from 'rxjs/Subject';
@Injectable()export class SyncService {
private thread_id = new Subject<number>();thread_id$ = this.thread_id.asObservable();
set_thread_id(thread_id: number) {this.thread_id.next(thread_id);}
}

Component.ts

export class ConsumerComponent implements OnInit {
constructor(public sync: SyncService) {this.sync.thread_id$.subscribe(thread_id => {**Process Value Updates Here**}}  
selectChat(thread_id: number) {  <--- How to update valuesthis.sync.set_thread_id(thread_id);}}

您可以在其他组件中使用类似的实现,并且您的所有组件都将共享相同的共享值。

如果您不想使用ngOnChange实现ogonChange()方法,您还可以通过#2事件 ETC订阅特定项目的更改。

myForm = new FormGroup({first: new FormControl(),});
this.myForm.valueChanges.subscribe((formValue) => {this.changeDetector.markForCheck();});

markForCheck()写,因为在这个声明中使用:

changeDetection: ChangeDetectionStrategy.OnPush
@Input() set categoryId(categoryId: number) {console.log(categoryId)}

请尝试使用此方法。希望这有助于

这里ngOnChange将在您的输入属性更改时始终触发:

ngOnChanges(changes: SimpleChanges): void {console.log(changes.categoryId.currentValue)}

你也可以只传递一个事件发射器作为输入。不太确定这是否是最好的做法…

CategoryComponent.ts:

categoryIdEvent: EventEmitter<string> = new EventEmitter<>();
- OTHER CODE -
setCategoryId(id) {this.category.id = id;this.categoryIdEvent.emit(this.category.id);}

CategoryComponent.html:

<video-list *ngIf="category" [categoryId]="categoryIdEvent"></video-list>

VideoListComponent.ts:

@Input() categoryIdEvent: EventEmitter<string>....ngOnInit() {this.categoryIdEvent.subscribe(newID => {this.categoryId = newID;}}

此解决方案使用代理类并提供以下优点:

  • 允许消费者利用RXJS的力量
  • 比迄今为止提出的其他解决方案更多紧凑
  • 比使用ngOnChanges()更多类型安全
  • 您可以通过这种方式观察任何类字段。

示例用法:

@Input()num: number;@Input()str: number;fields = observeFields(this); // <- call our utility function
constructor() {this.fields.str.subscribe(s => console.log(s));}

实用功能:

import { BehaviorSubject, Observable, shareReplay } from 'rxjs';
const observeField = <T, K extends keyof T>(target: T, key: K) => {const subject = new BehaviorSubject<T[K]>(target[key]);Object.defineProperty(target, key, {get: () => subject.getValue() as T[K],set: (newValue: T[K]) => {if (newValue !== subject.getValue()) {subject.next(newValue);}}});return subject;};
export const observeFields = <T extends object>(target: T) => {const subjects = {} as { [key: string]: Observable<any> };return new Proxy(target, {get: (t, prop: string) => {if (subjects[prop]) { return subjects[prop]; }return subjects[prop] = observeField(t, prop as keyof T).pipe(shareReplay({refCount: true, buffer:1}),);}}) as Required<{ [key in keyof T]: Observable<NonNullable<T[key]>> }>;};

演示

你可以在facade服务中使用BehaviorSubject,然后在任何组件中订阅该主题,并且当事件碰巧触发对其的数据调用.next()的更改时。确保关闭在销毁生命周期钩子中的这些订阅。

<强>data-api.facade.ts

@Injectable({providedIn: 'root'})export class DataApiFacade {
currentTabIndex: BehaviorSubject<number> = new BehaviorSubject(0);
}

<强>some.component.ts

constructor(private dataApiFacade: DataApiFacade){}
ngOnInit(): void {this.dataApiFacade.currentTabIndex.pipe(takeUntil(this.destroy$)).subscribe(value => {if (value) {this.currentTabIndex = value;}});}
setTabView(event: MatTabChangeEvent) {this.dataApiFacade.currentTabIndex.next(event.index);}
ngOnDestroy() {this.destroy$.next(true);this.destroy$.complete();}

基本上,两种建议的解决方案在大多数情况下都能正常工作。我对ngOnChange()新增功能的主要负面体验是缺乏类型安全性。

在我的一个项目中,我做了一些重命名,之后一些魔术弦保持不变,bug当然需要一些时间才能浮出水面。

Setters没有这个问题:您的IDE或编译器会让您知道任何不匹配。

您可以使用生命周期方法

@Input() inputValue: string;
ngOnChanges(changes: SimpleChanges) {console.log(changes['inputValue'].currentValue);}

如果您正在处理使用@Input在父组件和子组件之间共享数据的情况,您可以使用生命周期方法检测@Input数据更改:ngOnChanges

 ngOnChanges(changes: SimpleChanges) {if (!changes.categoryId.firstChange) {// only logged upon a change after renderingconsole.log(changes.categoryId.currentValue);}}

我建议你应该关心为子组件实现的更改策略,你应该添加ChangeDettionStrategy. OnPush,因为一些性能原因:

示例代码:

@Component({selector: 'app-hero-detail',templateUrl: './hero-detail.component.html',changeDetection: ChangeDetectionStrategy.OnPush})export class VideoListComponent implements OnChanges {@Input() categoryId: string;

Angular ngOn变化

ngOnChanges()是一个内置的Angular回调方法,如果至少有一个更改,则在默认更改检测器检查数据绑定属性后立即调用。在视图和内容之前,检查子级。

// child.component.ts
import { Component, OnInit, Input, SimpleChanges, OnChanges } from '@angular/core';
@Component({selector: 'app-child',templateUrl: './child.component.html',styleUrls: ['./child.component.css']})export class ChildComponent implements OnInit, OnChanges {
@Input() inputParentData: any;
constructor() { }
ngOnInit(): void {}
ngOnChanges(changes: SimpleChanges): void {console.log(changes);}
}

更多:Angular文档

我坚持@艾伦-s建议的方法,但做了一些修改。首先-我反对使用ngOnChanges。相反,我建议将所有需要更改的内容移动到一个对象下。并使用BehaviorSubject来跟踪它的变化:

  private location$: BehaviorSubject<AbxMapLayers.Location> = new BehaviorSubject<AbxMapLayers.Location>(null);
@Input()set location(value: AbxMapLayers.Location) {this.location$.next(value);}get location(): AbxMapLayers.Location {return this.location$.value;}
<abx-map-layer*ngIf="isInteger(unitForm.get('addressId').value)"[location]="{gpsLatitude: unitForm.get('address.gpsLatitude').value,gpsLongitude: unitForm.get('address.gpsLongitude').value,country: unitForm.get('address.country').value,placeName: unitForm.get('address.placeName').value,postalZip: unitForm.get('address.postalZip').value,streetName: unitForm.get('address.streetName').value,houseNumber: unitForm.get('address.houseNumber').value}"[inactive]="unitAddressForm.disabled"></abx-map-layer>