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

时间:2016-07-25 15:28:01

标签: angular angular2-changedetection

我有一个父组件( CategoryComponent ),一个子组件( videoListComponent )和一个ApiService。

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

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

CategoryComponent.html

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

这有效,当父CategoryComponent类改变时,categoryId值通过@Input()传递,但我需要在VideoListComponent中检测到这一点并通过APIService(使用新的categoryId)重新请求视频数组。

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

15 个答案:

答案 0 :(得分:455)

实际上,有两种方法可以在angular2 +中的子组件中输入更改时检测并执行操作:

  1. 您可以使用旧答案中提到的 ngOnChanges()生命周期方法
  2.     @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
    
        }
    

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

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

      文档链接:查看here

      演示示例:查看this plunker

      您应该使用哪种方式?

      如果您的组件有多个输入,那么,如果您使用ngOnChanges(),您将在ngOnChanges()中一次性获得所有输入的所有更改。使用此方法,您还可以比较已更改的输入的当前值和先前值,并相应地执行操作。

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

      编辑2017-07-25:角度变化检测可能在某些情况下仍未消除

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

      1. 如果您使用嵌套对象或数组(而不是JS原始数据类型)将数据从Parent传递给Child,则更改检测(使用setter或ngchanges)可能不会触发,如同在用户回答:muetzerich。对于解决方案,请查看here

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

答案 1 :(得分:25)

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

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

修改

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

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

答案 2 :(得分:8)

最安全的选择是使用@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);
  }
}

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

答案 3 :(得分:2)

我只想补充说,如果DoCheck值不是原始值,那么另一个名为@Input的Lifecycle钩子很有用。

我有一个数组作为Input,因此当内容发生变化时,这不会触发OnChanges事件(因为Angular所做的检查是简单的&#39;而不是很深即使数组中的内容已更改,Array仍然是一个数组。

然后,我实现了一些自定义检查代码,以决定是否要使用更改的Array更新我的视图。

答案 4 :(得分:2)

您还可以使用一个observable触发父组件(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
});
}

答案 5 :(得分:2)

此解决方案使用proxy类,并具有以下优点:

  • 允许消费者利用 RXJS
  • 的功能
  • 比迄今为止提出的其他解决方案更紧凑
  • 类型安全比使用ngOnChanges()

用法示例:

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

答案 6 :(得分:1)

  @Input()
  public set categoryId(categoryId: number) {
      console.log(categoryId)
  }

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

答案 7 :(得分:1)

您可以在外观服务中使用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();
}

答案 8 :(得分:1)

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

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

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

答案 9 :(得分:1)

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

@Input() inputValue: string;

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

答案 10 :(得分:0)

如果您不想使用ngOnChange工具或onChange()方法,还可以通过 valueChanges事件(即ETC)订阅特定项目的更改。

 myForm= new FormGroup({
first: new FormControl()   

});

this.myForm.valueChanges.subscribe(formValue => {
  this.changeDetector.markForCheck();
});

由于在此声明中使用而写的markForCheck():

  changeDetection: ChangeDetectionStrategy.OnPush

答案 11 :(得分:0)

当输入属性更改时,ngOnChanges将始终触发:

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

答案 12 :(得分:0)

您也可以只传递一个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;
 }
}

答案 13 :(得分:0)

如果您正在处理使用 @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;

答案 14 :(得分:0)

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