【问题标题】:How to detect when an @Input() value changes in Angular?如何检测 Angular 中的 @Input() 值何时发生变化?
【发布时间】:2016-11-29 00:50:49
【问题描述】:

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

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

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

CategoryComponent.html

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

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

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

【问题讨论】:

标签: angular angular2-changedetection angular-decorator


【解决方案1】:

其实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
        
    }
    

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

  1. 或者,您也可以使用 输入属性设置器,如下所示:
    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 原始数据类型)将数据从父级传递给子级,则更改检测(使用 setter 或 ngchanges)可能不会触发,正如用户的回答中所提到的那样: muetzerich。如需解决方案,请查看here

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

【讨论】:

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

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

ngOnChanges 在数据绑定属性被调用后立即调用 检查并在查看和内容子项之前检查是否至少 其中一个发生了变化。

这里是Docs

【讨论】:

  • 这在变量设置为新值时有效,例如MyComponent.myArray = [],但如果输入值被更改,例如MyComponent.myArray.push(newValue)ngOnChanges() 未触发。有关如何捕捉这种变化的任何想法?
  • ngOnChanges() 在嵌套对象发生更改时不会被调用。也许你找到了解决方案here
【解决方案3】:

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

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

编辑:

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

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

【讨论】:

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

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

【讨论】:

  • 这绝对有帮助!
【解决方案5】:

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

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

【讨论】:

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

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

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

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

【讨论】:

    【解决方案7】:

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

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

    【讨论】:

      【解决方案8】:

      你也可以,有一个可观察的,它触发父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
        });
      }
      

      【讨论】:

        【解决方案9】:

        此解决方案使用proxy 类并提供以下优势:

        • 允许消费者利用 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;
        }
        

        【讨论】:

        • 请注意,如果发出的值是对象,subject 将始终发出。
        • @LuisDiegoHernández,我不完全确定你的意思。它使用浅相等比较来确定是否应该发出新值。
        【解决方案10】:

        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

        【讨论】:

          【解决方案11】:

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

          【讨论】:

            【解决方案12】:

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

            【讨论】:

            • 请提供有用的链接、代码 sn-ps 以支持您的解决方案。
            • 我加了一个例子。
            【解决方案13】:

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

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

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

            【讨论】:

              【解决方案14】:

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

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

              【讨论】:

                【解决方案15】:

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

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

                markForCheck() 是因为在此声明中使用而写的:

                changeDetection: ChangeDetectionStrategy.OnPush
                

                【讨论】:

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

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

                【讨论】:

                  猜你喜欢
                  • 2021-10-02
                  • 1970-01-01
                  • 2017-08-15
                  • 2011-02-19
                  • 1970-01-01
                  • 2016-12-28
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多