Angular Signal Forms 重建后 computed 为何持续重算?
结论
runInInjectionContext 只在回调执行期间提供注入上下文,不会自动为每个 FormContext 建立独立的销毁边界。如果创建表单时注册了 effect 等需要清理的响应式资源,而旧上下文没有销毁,这些资源就可能继续存活。
不过,现有代码不足以确定持续触发的唯一原因。给出的同类示例显示,在 computed 中创建表单会触发 NG0602:表单内部会注册 effect,而响应式计算期间不允许创建 effect。应将表单创建移出 computed,并检查 convertToRequestModel 是否读写了会触发自身依赖的信号。linkedSignal 也不能代替表单上下文的生命周期管理。
为每个表单上下文建立可销毁的 Injector
创建一个子 DestroyableInjector,并通过 form 的 injector 选项传入。上下文替换时调用 dispose() 销毁子 injector;如果父上下文先销毁,也通过回调清理子 injector:
class FormContext<T> {
readonly controls: FieldTree<T>;
private readonly injector: DestroyableInjector;
constructor(parent: Injector, initial: T) {
this.injector = Injector.create({ providers: [], parent });
this.controls = form<T>(signal(initial), { injector: this.injector });
const unregisterParentCleanup = parent
.get(DestroyRef)
.onDestroy(() => this.dispose());
this.injector
.get(DestroyRef)
.onDestroy(unregisterParentCleanup);
}
dispose(): void {
this.injector.destroy();
}
}
这里沿用题目中的 form(model, { injector }) 写法。如果所用的 Signal Forms 库版本签名不同,请按该版本的 API 调整。这种清理方式适用于通过 DestroyRef 管理生命周期的资源。其他异步任务仍需自行取消,或确认结果是否属于当前上下文。
路由变化时替换上下文
不要在 computed 中创建有生命周期的表单。路由流中可以先销毁当前上下文,再创建新的上下文:
let current: FormContext<User> | undefined;
readonly context = toSignal(
toObservable(this.userId).pipe(
map((userId) => {
current?.dispose();
current = new FormContext(this.injector, this.initialUser(userId));
return current;
})
),
{ initialValue: new FormContext(this.injector, this.initialUser(this.userId())) }
);
实际使用时,要确保 initialValue 和流的首次发射不会造成重复创建。也可以在组件或 service 中通过明确的路由订阅管理当前实例,并在替换前调用 dispose()。表单应在普通命令式流程中创建,旧实例也要显式销毁。
排查持续重算
- 确认表单没有在
computed、linkedSignal或其他响应式计算回调中创建。如果出现NG0602,先将表单创建移出该响应式上下文。 - 为每个表单上下文使用独立的 injector,并在替换时销毁旧 injector。
- 检查
convertToRequestModel:它应从控件读取数据并返回模型,不应在计算过程中写回同一组信号或更新表单。 - 如果销毁后仍持续触发,记录
FormContext的创建和销毁次数,以及request的读取和写入路径,以判断问题来自遗留 effect 还是转换函数造成的依赖循环。
题目中的示例没有提供 Angular 版本、Signal Forms 实现或 convertToRequestModel 代码,因此仅凭现有信息无法断定具体的触发链。类似问题的讨论也提到,在 computed 中创建表单会触发 NG0602,并介绍了使用子 injector 和显式销毁的处理方式。参考讨论
备注:内容仅供参考。
