PAYATHON 2026

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()。表单应在普通命令式流程中创建,旧实例也要显式销毁。

排查持续重算

  1. 确认表单没有在 computed、linkedSignal 或其他响应式计算回调中创建。如果出现 NG0602,先将表单创建移出该响应式上下文。
  2. 为每个表单上下文使用独立的 injector,并在替换时销毁旧 injector。
  3. 检查 convertToRequestModel:它应从控件读取数据并返回模型,不应在计算过程中写回同一组信号或更新表单。
  4. 如果销毁后仍持续触发,记录 FormContext 的创建和销毁次数,以及 request 的读取和写入路径,以判断问题来自遗留 effect 还是转换函数造成的依赖循环。

题目中的示例没有提供 Angular 版本、Signal Forms 实现或 convertToRequestModel 代码,因此仅凭现有信息无法断定具体的触发链。类似问题的讨论也提到,在 computed 中创建表单会触发 NG0602,并介绍了使用子 injector 和显式销毁的处理方式。参考讨论

备注:内容仅供参考。