在ngOnInit-Angular 13中,用共享服务更新父组件的正确方法是什么



我曾多次遇到这样的情况:我在组件的ngOnInit中使用共享服务来更新另一个组件中的值,而该组件恰好是父组件。

这导致了开发模式中臭名昭著的Error: NG0100: Expression has changed after it was checked,并且更改检测不会检测到更改。我理解为什么会发生这种情况,这是意料之中的行为。

我的策略是使用零延迟setTimeout()来推迟代码执行,直到初始更改检测完成之后。供参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#zero_delays

对我来说,这似乎是一种解决常见问题的方法,我觉得Angular可能有另一个我不知道的解决方案。所以我的问题是:是否有Angular的方法来用ngOnInit中的共享服务更新父组件

我已经尝试了所有其他的生命周期挂钩,它们都会导致相同的错误。使用主题和异步管道是相同的。

下面是一个示例:一个服务,当打开子组件时,它会更改主组件的字体颜色:https://stackblitz.com/edit/angular-ivy-kkvarp?file=src/app/global-style.service.ts

服务

export class GlobalStyleService {
private _green = false;
set green(value: boolean) {
// Uncomment to get an error
// this._green = value;
//For use in lifecycle hooks
//Delay setting until after change detection completes
setTimeout(() => (this._green = value));
}
get green() {
return this._green;
}
}

应用程序组件.ts

export class AppComponent {
constructor(public globalStyle: GlobalStyleService) {}
}

app.component.html

<app-main [class.green]="globalStyle.green"></app-main>

样式.css

.green {
color: green;
}

main.component.html

<div class="container">
<h1>Main Component</h1>
<button (click)="testComponentOpen = !testComponentOpen">
Toggle Test Component
</button>
<app-test *ngIf="testComponentOpen"></app-test>
</div>

测试组件.ts

export class TestComponent implements OnInit, OnDestroy {
constructor(private globalStyle: GlobalStyleService) {}
ngOnInit() {
this.globalStyle.green = true;
}
ngOnDestroy() {
this.globalStyle.green = false;
}
}

打开测试组件会设置服务变量,从而更改主组件的样式。如果在不使用setTimeout()的情况下设置变量,则会出现以下错误:

错误:NG0100:ExpressionChangedAfterItHasBeenCheckedError:表达式在检查后发生了更改。"green"的上一个值:"false"。当前值:"true">

在触发另一轮更改检测之前,文本不会变为绿色。

使用setTimeout()是可行的,但有角度的方法吗?如果更改检测已经在进行中,那么会显式推迟代码执行吗?

在围绕同一个问题做了大量自己的研究之后(特别是,一个加载微调器服务引用了顶级组件中的覆盖div,我认为这是一个非常常见的用例(,我非常有信心,不幸的是,你的特定问题(有角度方法吗?(的答案是:没有,不是真的。

Angular的状态稳定性检查在设计上是分层的和单向的(这意味着当子组件解析时,父组件状态被认为是不可变的(,这需要组件知道它们在层次结构中的位置才能正常工作,这与创建通用的、可重用的组件非常不兼容。

特别是对于通过服务进行更改通信(正如您和我所做的那样(,除了通过异步方式等待状态检查之后,没有其他可靠的解决方案。

所以设计的问题是把这些乱七八糟的东西藏在哪里。你已经把它放在服务中,这有助于保护组件不处理它,但这确实意味着所有的集合都是异步的,这是非直观的代码,并使你暴露在竞争条件下(例如,你可以让一些组件使用这个服务,设置并获取值,但不清楚为什么get与你刚刚调用的集合不匹配(。

我想到的最好的方法是创建一个AsyncComponent基类,它接管ngOnInit(和其他生命周期回调(并异步调用派生类ngOnXXXAsync。这仍然意味着组件需要知道他们需要这样做,但这使他们无法处理异步逻辑,并意味着init方法中的所有代码仍然在内部同步执行。

基于您的示例:

异步组件.ts

import { Component, OnInit, OnDestroy } from '@angular/core';
@Component({
template: '',
})
export abstract class AsyncComponent implements OnInit, OnDestroy {
ngOnInit(): void {
Promise.resolve(null).then(() => this.ngOnInitAsync());
}
ngOnDestroy(): void {
Promise.resolve(null).then(() => this.ngOnDestroyAsync());
}
// Override for async initialization.
ngOnInitAsync(): void {}
// Override for async destruction.
ngOnDestroyAsync(): void {}
}

测试组件.ts

export class TestComponent extends AsyncComponent {
constructor(private globalStyle: GlobalStyleService) {
super();
}
ngOnInitAsync() {
this.globalStyle.green = true;
}
ngOnDestroyAsync() {
this.globalStyle.green = false;
}
}

我在这里用这个解决方案分叉了你的堆栈:https://stackblitz.com/edit/angular-ivy-9djhyh?file=src%2Fapp%2Fasync.component.ts,src%2App%2Ftest%2Ftest.component.ts

它还演示了使服务中的集合异步的问题。。如果你重新打开它,当.get与之前的.set不匹配时,它会抛出一个错误。

最新更新