ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

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

类别组件.html

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

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

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


T
Thom

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

您可以使用旧答案中也提到的 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
        
    }
    

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

或者,您还可以使用输入属性设置器,如下所示:

    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 原始数据类型(字符串、数字、布尔值)。但是,在以下情况下,它不会触发,您必须采取额外的措施才能使其工作。

如果您使用嵌套对象或数组(而不是 JS 原始数据类型)将数据从父级传递给子级,则更改检测(使用 setter 或 ngchanges)可能不会触发,正如用户回答中提到的:muetzerich。解决方案看这里。如果您在角度上下文之外(即外部)改变数据,那么角度将不知道这些变化。您可能必须在组件中使用 ChangeDetectorRef 或 NgZone 来从角度感知外部变化,从而触发变化检测。参考这个。


关于使用带有输入的 setter 非常好的一点,我会将其更新为可接受的答案,因为它更全面。谢谢。
@trichetriche,只要父项更改输入,就会调用setter(set方法)。同样,ngOnChanges() 也是如此。
@trichetriche,请在我上面的回答中查看 EDIT 2017-07-25,了解为什么在您的情况下更改检测可能不会触发。最有可能的是,这可能是编辑中列出的第 1 个原因。
我只是偶然发现了这个问题,注意到您说使用 setter 不允许比较值,但这不是真的,您可以调用 doSomething 方法并在实际设置新值之前将 2 个参数作为新值和旧值,另一种方法是在设置之前存储旧值,然后调用该方法。
@T.Aoukar 我要说的是,输入设置器本身并不支持像 ngOnChanges() 那样比较旧/新值。您始终可以使用 hacks 来存储旧值(如您所提到的)以将其与新值进行比较。
m
muetzerich

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

ngOnChanges 在数据绑定属性被检查之后和视图和内容子元素被检查之前被调用,如果它们中的至少一个已经改变。

这是Docs


这适用于将变量设置为新值(例如 MyComponent.myArray = []),但如果更改输入值(例如 MyComponent.myArray.push(newValue)),则不会触发 ngOnChanges()。关于如何捕捉这种变化的任何想法?
当嵌套对象发生更改时,不会调用 ngOnChanges()。也许您找到了解决方案here
l
luiscla27

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

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

编辑:

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

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

您是否在文件顶部导入了 SimpleChanges 界面?
@Jon - 是的。问题不在于签名本身,而在于访问在运行时分配给 SimpleChanges 对象的参数。例如,在我的组件中,我将我的 User 类绑定到输入(即 <my-user-details [user]="selectedUser"></my-user-details>)。通过调用 changes.user.currentValue 从 SimpleChanges 类访问此属性。注意 user 在运行时绑定,而不是 SimpleChanges 对象的一部分
你试过这个吗? ngOnChanges(changes: {[propName: string]: SimpleChange}) { console.log('onChanges - myProp = ' + changes['myProp'].currentValue); }
@Jon - 切换到括号符号就可以了。我更新了我的答案,将其作为替代方案。
从“@angular/core”导入 { SimpleChanges };如果它在角度文档中,而不是本机打字稿类型,那么它可能来自角度模块,很可能是“角度/核心”
l
limbo93
@Input() set categoryId(categoryId: number) {
      console.log(categoryId)
}

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


这绝对有帮助!
l
luiscla27

最安全的选择是使用 @Input 参数的共享 service instead。此外,@Input 参数不会检测复杂嵌套对象类型的变化。

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

服务.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);
    }

}

组件.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);
  }
}

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


谢谢桑克特。这是一个非常好的观点,并且在适当的情况下是一种更好的方法。我将保留已接受的答案,因为它回答了我关于检测@Input 更改的具体问题。
@Input 参数不会检测复杂嵌套对象类型内部的变化,但如果对象本身发生变化,它就会触发,对吗?例如,我有输入参数“parent”,所以如果我将 parent 作为一个整体更改,更改检测会触发,但如果我更改 parent.name,它不会?
您应该说明您的解决方案更安全的原因
@JoshuaKemmerer @Input 参数不会检测复杂嵌套对象类型的变化,但它会检测简单的本机对象。
下面是一个示例,说明“setter”在传递整个对象时的行为方式与仅通过更新其属性之一来改变现有对象时的行为方式的区别:stackblitz.com/edit/angular-q5ixah?file=src/app/…
T
Tharindu Lakshan

角 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


S
Stevo

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

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

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


l
luiscla27

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

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

l
luiscla27

您还可以拥有一个触发父 component(CategoryComponent) 更改的可观察对象,并在子组件的订阅中执行您想要执行的操作。 (videoListComponent)

服务.ts

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

类别组件.ts

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

视频列表组件.ts

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

A
Alexander

我会坚持采用@alan-cs 建议的方法,但要进行一些修改。首先 - 我反对使用 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>

l
luiscla27

此解决方案使用 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;
}

请注意,如果发出的值是对象,subject 将始终发出。
@LuisDiegoHernández,我不完全确定你的意思。它使用浅相等比较来确定是否应该发出新值。
l
luiscla27

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

类别组件.ts:

categoryIdEvent: EventEmitter<string> = new EventEmitter<>();

- OTHER CODE -

setCategoryId(id) {
 this.category.id = id;
 this.categoryIdEvent.emit(this.category.id);
}

类别组件.html:

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

在 VideoListComponent.ts 中:

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

请提供有用的链接、代码片段来支持您的解决方案。
我添加了一个例子。
l
luiscla27

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

数据-api.facade.ts

@Injectable({
  providedIn: 'root'
})
export class DataApiFacade {

currentTabIndex: BehaviorSubject<number> = new BehaviorSubject(0);

}

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

N
Naeem Bashir

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

@Input() inputValue: string;

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

R
Reuben Sivan

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

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

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


l
luiscla27

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

myForm = new FormGroup({
  first: new FormControl(),
});

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

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

changeDetection: ChangeDetectionStrategy.OnPush

这与问题完全不同,尽管乐于助人的人应该知道您的代码是关于不同类型的更改。该问题询问了来自组件外部的数据更改,然后让您的组件知道并响应数据已更改的事实。您的回答是关于检测组件本身内部的更改,例如表单上的输入,这些对组件来说是本地的。
R
Rebai Ahmed

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