【问题标题】:How can I fix this specific NG0100: ExpressionChangedAfterItHasBeenCheckedError error?如何修复这个特定的 NG0100: ExpressionChangedAfterItHasBeenCheckedError 错误?
【发布时间】:2022-10-25 17:56:49
【问题描述】:

我在 Angular 中读过有关 NG0100:ExpressionChangedAfterItHasBeenCheckedError 的信息,但在 my case 中我无法避免它。

基本上,在拦截器上,我有一个加载“状态”真/假的服务:

拦截(请求:HttpRequest,下一个:HttpHandler):Observable { this.showLoader();

return next.handle(req).pipe(
  catchError((error) => {
    return error;
  }),
  finalize(() => {
    this.hideLoader();
  })
);

}

在应用程序组件中使用 ngAfterViewInit 会引入该错误:

  ngOnInit(): void {}
  ngAfterViewInit() {
    this.getData().subscribe((data) => {
      this.childSelector.loadRecipeRoadmap(data.name);
    });
  }

而且我需要使用它:事实上,当所有孩子都被加载时,父母必须向孩子“发送”(一次)数据(仅在开始时)。在某些时候,我只需要从 child 读取(这就是我使用 ViewChild 而不是 @Output 机制的原因)。

如何解决此特定错误?我应该同步 Observable 吗?不知道怎么...

【问题讨论】:

  • 我建议加载程序的显示/隐藏由主题(由拦截器更新)处理,然后在组件中使用 debounceTime 运算符来防止竞争条件。您可以在debounceTime 上设置低至 5 毫秒的值来限制排放,而不会明显影响性能
  • 答案在您在问题中发布的文章的链接中
  • @Drenai 不是真的。 SetTimeout 和 Promise 给出相同的错误。并且不能使用 ngOnInit,因为此时还没有加载孩子......
  • 也许detectChanges - 这是他们描述的我见过最常用的那个
  • @Drenai 似乎更像是“快速修复”而不是解决方案:(

标签: angular


【解决方案1】:

这是因为你正在执行ngAfterViewInit中的一些代码,它修改了显示的数据.

第一个检测周期将ngIf评估为false,然后执行ngAfterViewInit,然后执行第二个验证检测周期(角度开发模式有这个额外的),这次ngIf被评估为@987654329 @。因此,臭名昭著的错误。

有几个解决方案:

选项 1:下一个宏任务(setTimeout)

由于您的数据获取无论如何都是异步的,因此您可以在 setTimeout 的帮助下以 0 时间延迟将其推迟到下一个宏任务中调用(在 ngAfterViewInit 完成之后):

ngAfterViewInit() {
  setTimeout(() => {
    this.getData().subscribe((data) => {
      this.childSelector.loadRecipeRoadmap(data.name);
    });
  });
}
选项 2:ChangeDetectorRef.detectChanges()

您可以在修改显示的数据后通知 Angular 运行额外的检测周期。尽管出于性能原因,这是比前一个更糟糕的选择。

constructor(private http: HttpClient, public loader: LoaderService, private changeDetectorRef: ChangeDetectorRef) {}

  ngAfterViewInit() {
    this.getData().subscribe((data) => {
      this.childSelector.loadRecipeRoadmap(data.name);
    });
    this.changeDetectorRef.detectChanges();
  }

或者也许更棱角分明的方式:

选项 3:在 OnInit + 数据绑定中获取数据

不要等待子组件被创建并直接与之交互,而是在OnInit 中执行数据加载并将数据绑定到子组件:

export class AppComponent {
  dataForChild: any;

  constructor(private http: HttpClient, public loader: LoaderService) {}

  ngOnInit(): void {
    this.getData().subscribe((data) => this.dataForChild = data.name);
  }

并将其与(或子组件中的课程myData 需要成为并使用@Input() 输入属性)

<app-my-child [myData]="dataForChild"></app-my-child>

额外的待定订阅

您正在订阅 observable,但不要取消订阅(unsubribe()takeUntil() 都不适用)。尽管从 angular http 模块订阅 observables 不会导致内存泄漏,但最好确保您无论如何都会取消订阅它,原因如下:

  • 只是因为它是一种很好的做法,而不用怀疑它是否有必要。今天可能没有必要,但明天有人会修改服务,它可能不再安全可观察;
  • 如果你取消订阅ngOnDestroy,挂起的http请求将被取消,释放浏览器池中的已用请求
  • 订阅和潜在的pipe'd 转换中也没有逻辑,因此不会执行不必​​要的代码,因此性能更好
  • 它将允许垃圾收集器更快地删除组件实例

【讨论】:

  • 出色的。你很好地暴露了问题,有很多解决方案:) 接受。你能帮我吗? stackoverflow.com/questions/71952796/…
  • 请避免使用 setTimeout 解决方案,这是一种不好的做法,因为它在 Angular 生命周期“之外”运行,在某些情况下,它甚至可以触发更多检测更改,并有可能产生与循环调用非常相似的风险。除非完全有必要调用手册detectChanges(因为它开始了新的生命周期),否则也要避免选项 2。选项 3 是最好的方法,因为它与 Angular 生命周期完美融合
  • 此外,@carecki,未订阅的 observable 确实会导致 Angular 中的内存泄漏。在内部,the unsubscribe code 实际上只是将变量设置为null 正是为了避免内存泄漏(因为它是 GC 对象在它们为空时从内存中释放)。
  • @luiscla27 Angular 公司本身提出了一些“不好的做法”,这很有趣,不是吗? angular.io/errors/NG0100
  • @luiscla27: • “外部”角度生命周期?什么?这就是异步代码的工作原理。并且没有循环调用,因为每个组件只调用一次 ngAfterViewInit。 • 您将代码链接到Subject 类,这不是每个可观察对象处理取消订阅的方式。例如,由 angular httpModule 创建的 Observables 会取消正在进行的 http 调用。通常可观察对象会删除对其订阅者的引用,从而避免内存泄漏。
【解决方案2】:

根据生命周期原则构建您的应用程序结构。

阅读这篇文章:

https://angular.io/errors/NG0100

【讨论】:

    【解决方案3】:

    这可能不是最漂亮的解决方案,但它确实修复了与服务中的值更改相关的错误。

    private _myChild: MyChildComponent;
    @ViewChild('childSelector')
    set childSelector(val: MyChildComponent) {
      this._myChild = val;
      this.getData().subscribe((data) => {
        this._myChild.loadRecipeRoadmap(data.name);
      });
    }
    

    您也可以在 afterInit 上使用属性集。它不是超级干净,但它是一种处理它的方法,特别是如果它与 ngIfs 或类似嵌套。

    编辑: 这可能是比上面的 hacky 方法更好的方法。

    由于拦截器在服务中设置了值,它有点不可能控制变化检测之间的可观察到的变化。但是,您可以做一些更简单的事情,而不是直接绑定到服务中的加载程序。

    @Injectable({
      providedIn: 'root',
    })
    export class LoaderService {
      private loaderSource = new BehaviorSubject<boolean>(false);
    
      constructor() {}
    
      show() {
        this.loaderSource.next(true);
      }
      hide() {
        this.loaderSource.next(false);
      }
      getLoader() {
        return this.loaderSource.asObservable();
      }
    }
    

    让它成为您从服务返回的可观察对象。然后将其设置为 afterViewInit

    ngAfterViewInit() {
      this.loader$ = this.loader.getLoader();
      this.getData().subscribe((data) => {
        this.childSelector.loadRecipeRoadmap(data.name);
      });
    }
    

    并使用该值来检查加载。然后你避免它在更改检测之前查看 id 。

    <app-my-child #childSelector></app-my-child>
    
    <div *ngIf="loader$ | async">Loading...</div>
    

    堆栈闪电战https://stackblitz.com/edit/angular-ivy-sfmfrj?file=src%2Fapp%2Fapp.component.ts

    【讨论】:

    • 不要真正得到你的解决方案。我应该在哪里/何时使用二传手?在 AfterInit 上?嗯,你能给我一个真实的例子吗?
    • 我明白你的意思了。抱歉,这不是我要找的。数据必须从父级加载和传播,而不是从子级本身......
    • @markzzz我用一种更简单但应该解决问题的不同方法更新了我的答案
    • 我不想在其他组件上移动“加载器”逻辑。它必须集中在自己的服务上,并且只有在
    • 它仍然在服务中,但在组件准备好之前你不要绑定到它。那么你可以避免这个问题。
    猜你喜欢
    • 2022-11-30
    • 2022-12-01
    • 2021-07-22
    • 2021-09-17
    • 2021-09-15
    • 2022-01-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多