如何检测 Angular 中的 @Input() 值何时发生变化?

How to detect when an @Input() value changes in Angular?

我有一个 parent 组件 (CategoryComponent)、一个 child 组件 (videoListComponent) 和一个ApiService.

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

目前视频列表组件只获取所有视频,我想将其过滤为特定类别中的视频,我通过 @Input() 将 categoryId 传递给 child 来实现此目的。

CategoryComponent.html

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

这有效,当 parent CategoryComponent 类别发生变化时,categoryId 值通过 @Input() 传递,但我需要在 VideoListComponent 和 re-request 视频数组中检测到这一点APIService(使用新的 categoryId)。

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

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

ngOnChanges is called right after the data-bound properties have been checked and before view and content children are checked if at least one of them has changed.

这是Docs.

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

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

编辑:

正如 Jon 在下面指出的那样,在使用括号表示法而不是点表示法时,您可以使用 SimpleChanges 签名。

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

实际上,在 angular2+ 的子组件中,当输入发生变化时,有两种检测和操作的方法:

  1. 您可以使用 ngOnChanges() 生命周期方法,如较早的答案中所述:
    @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
        
    }
    

文档 Links:ngOnChanges, SimpleChanges, SimpleChange
演示示例:查看 this plunker

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

文档Link:查看here

演示示例:查看 this plunker

您应该使用哪种方法?

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

然而,如果你想在只有一个特定的单一输入发生变化时做某事(并且你不关心其他输入),那么使用输入 属性 [=72 可能更简单=].但是,这种方法没有提供内置的方法来比较已更改输入的先前值和当前值(您可以使用 ngOnChanges 生命周期方法轻松完成)。

编辑 2017-07-25:ANGULAR 在某些情况下更改检测可能仍不会触发

通常,只要父组件更改它传递给子组件的数据,setter 和 ngOnChanges 的更改检测都会触发,前提是数据是 JS 原始数据类型(字符串,数字,布尔值)。但是,在以下情况下,它不会触发,您必须采取额外的操作才能使其正常工作。

  1. 如果您使用嵌套对象或数组(而不是 JS 基本数据类型)将数据从父级传递到子级,更改检测(使用 setter 或 ngchanges)可能不火,正如用户在回答中也提到的:muetzerich。有关解决方案,请查看 .

  2. 如果您在 angular 上下文之外(即外部)更改数据,那么 angular 将不知道更改。您可能必须在组件中使用 ChangeDetectorRef 或 NgZone 来使 angular 了解外部更改,从而触发更改检测。参考this.

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

我有一个数组作为 Input,所以当内容改变时这不会触发 OnChanges 事件(因为 Angular 所做的检查是 'simple' 和不深,所以数组仍然是一个数组,即使数组上的内容已经改变。

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

您还可以拥有一个触发父组件 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 参数的共享 service instead。 此外,@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 values
    this.sync.set_thread_id(thread_id);
  }
}

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

如果你不想使用ngOnChange实现og onChange()方法,你也可以通过valueChanges事件[=订阅特定项目的变化、等等

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

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

此处 ngOnChanges 将始终在您的输入 属性 更改时触发:

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

您也可以只传递一个 EventEmitter 作为输入。不太确定这是否是最佳实践...

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

此解决方案使用 proxy class 并具有以下优势:

  • 允许消费者利用 RXJS
  • 的力量
  • 比目前提出的其他解决方案更紧凑
  • 比使用 ngOnChanges()
  • typesafe

用法示例:

@Input()
public num: number;
numChanges$ = observeProperty(this as MyComponent, 'num');

效用函数:

export function observeProperty<T, K extends keyof T>(target: T, key: K) {
  const subject = new BehaviorSubject<T[K]>(target[key]);
  Object.defineProperty(target, key, {
    get(): T[K] { return subject.getValue(); },
    set(newValue: T[K]): void {
      if (newValue !== subject.getValue()) {
        subject.next(newValue);
      }
    }
  });
  return subject;
}

您可以在外观服务中使用 BehaviorSubject,然后在任何组件中订阅该主题,当事件恰好触发数据调用 .next() 更改时。确保在 on destroy 生命周期挂钩中关闭这些订阅。

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() 的主要负面体验是缺乏类型安全。

在我的一个项目中我做了一些重命名,之后一些魔法字符串保持不变,当然这个错误需要一些时间才能浮出水面。

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

您可以使用 ngOnChanges() 生命周期方法

@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 rendering
      console.log(changes.categoryId.currentValue);
    }
  }

而且我建议您应该注意为子组件实施的更改策略,您应该出于某些性能原因添加 ChangeDetectionStrategy.OnPush:

示例代码:

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

Angular ngOnChanges

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 Docs

我会坚持@alan-c-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>