我正在使用 Angular 17 和信号进行一些 POC 工作,并遇到了一个用例,我不确定如何在不涉及
ngOnChanges
和 @Input
处理的情况下解决该问题。
因此,假设您有一个带有输入信号的组件。然后,您希望将这些相同的值复制/设置到服务中,以便该层次结构中的组件可以访问它。
我尝试将可写的
signal
放入服务中,但技巧是尝试在正确的时机从组件输入中设置服务值。因此,我可以使用组件初始信号值将值设置到服务中,但随后它们不会跟踪更改。
所以这似乎是
toObservable
或 effect
的不错用途,但听起来 toObservable
使用效果。似乎发生的情况是,效果在子组件初始化后运行(ngOnInit
被调用)。所以,我的问题是如何监听信号值的变化,能够将它们设置在其他东西(提供商/服务)上而不传递实际信号,这似乎不是一个好主意。
仅使用“正常”组件,使用
ngOnChanges
可以轻松跟踪更改,但我想知道如何使用具有相同时序的信号来跟踪它。
大大简化的示例,但显示了问题
@Injectable()
export class ExampleProvider {
readonly name = signal('');
}
@Component({
...
providers: [ExampleProvider]
})
export class ExampleComponent {
readonly name = input<string>();
private readonly exampleProvider = inject(ExampleProvider);
constructor() {
effect(() => {
// This will run after `ngOnInit` of child components
this.exampleProvider.name.set(this.name());
});
}
}
如何将组件输入的更改传播到服务并在子组件初始化/重新渲染之前对其进行设置?
https://stackblitz.com/edit/stackblitz-starters-hjmcgz?file=src%2Fmain.ts
您可以运行这个示例。 有 4 个输入。虽然 UI 将显示正确的值,但如果您打开控制台,您会注意到在子组件的
ngOnInit
期间,服务尚未更新为 effect
或 toObservable
设置的值。
您将看到在输入更改期间,将运行两个更改检测。您将看到第一个具有旧值,然后第二个将具有更新值。
此外,在第一次渲染期间,您将看到 2 和 4 的值尚未设置:
top-level.component.ts:66 TopLevelComponent ngOnInit
input1: a
input2: b
input3: c
input4: d
child.component.ts:39 ChildComponent ngOnInit
input1: a
input2:
input3: c
input4:
答案是不要为此使用效果。 Angular 文档指出:
避免使用效果来传播状态变化。这可能会导致 ExpressionChangedAfterItHasBeenChecked 错误、无限循环更新或不必要的更改检测周期。
由于这些风险,Angular 默认情况下会阻止您设置效果信号。如果绝对必要,可以通过在创建效果时设置 allowedSignalWrites 标志来启用它。
任何时候您发现自己在常规应用程序代码中使用
allowSignalWrites: true
,通常都有更好的方法。
一种解决方案是重复传递输入,直到它们到达所有需要它们的组件。如果这是不可能或不可取的,那么通常解决方案将是依赖项注入,但具体如何执行取决于您的需求。
您可以让设置输入的组件在服务上设置值,而不是将输入连接到服务。例如,在这里我删除了输入,将 ExampleService 上移到根组件,并将服务的属性直接连接到 ngModels (StackBlitz):
@Component({
selector: 'app-root',
standalone: true,
template: `
<div><label>A: <input [(ngModel)]="x.prop1"></label></div>
<div><label>B: <input [(ngModel)]="x.prop2"></label></div>
<div><label>C: <input [(ngModel)]="x.prop3"></label></div>
<div><label>D: <input [(ngModel)]="x.prop4"></label></div>
<top-level />
`,
imports: [TopLevelComponent, FormsModule],
providers: [ExampleService],
})
export class App {
x = inject(ExampleService);
}
如果您愿意,您也可以更改应用程序组件来实现ExampleService:
@Component({
// ...
providers: [{ provide: ExampleService, useExisting: App }],
})
export class App implements ExampleService {
prop1 = signal('a');
prop2 = signal('b');
prop3 = signal('c');
prop4 = signal('d');
}
不过,这可能会对您的应用程序造成相当大的破坏。如果您仍然希望在组件层次结构的较低位置提供ExampleService,则可以让您的TopLevelComponent实现ExampleService,或者您可以使用工厂提供程序将TopLevelComponent的输入映射到ExampleService,如下所示(StackBlitz):
@Directive({
selector: '[example]',
standalone: true,
providers: [{
provide: ExampleService,
useFactory: (): ExampleService => {
const c = inject(TopLevelComponent);
// or new ExampleService(c.input1, ...) if you prefer that
return { prop1: c.input1, prop2: c.input2, prop3: c.input3, prop4: c.input4 };
},
}],
})
export class ExampleServiceDirective {}
您可能会使用这些模式的多种变体,具体如何实现应取决于您使用ExampleService 到底做了什么。