You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何ControlValueAccessor的registerOnChange中用takeUntilDestroyed会触发视图销毁报错?

问题描述

我实现了一个简单的ControlValueAccessor组件:

@Component({
  selector: 'app-my-input',
  standalone: true,
  imports: [...],
  templateUrl: './my-input.component.html',
  styleUrls: ['./my-input.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: MyInputComponent, multi: true }],
})
export class MyInputComponent implements ControlValueAccessor {
  private readonly destroyRef = inject(DestroyRef);
  readonly ctrl = new FormControl<string[]>([]);

  registerOnChange(fn: (value: string[]) => void): void {
    this.ctrl.valueChanges
      .pipe(
        takeUntilDestroyed(this.destroyRef),
        map(arr => (arr.length ? arr : null)),
      )
      .subscribe(fn);
  }

  registerOnTouched(): void {
    // nothing to do
  }

  writeValue(uuidsSelected: string[]): void {
    this.ctrl.setValue(uuidsSelected);
  }
}

当组件销毁时,会收到错误:View has already been destroyed.

如果把注册逻辑改成以下实现,组件就能正常工作,无报错:

onChange: (value: string[]) => void = noop;

ngOnInit(): void {
  this.ctrl.valueChanges
    .pipe(
      takeUntilDestroyed(this.destroyRef), // can not use untilDestroyed in registerOnChange
      map(arr => (arr?.length ? arr : null)),
    )
    .subscribe(v => this.onChange(v));
}

registerOnChange(fn: (value: string[]) => void): void {
  this.onChange = fn;
}

请问为何在registerOnChange中使用takeUntilDestroyed(this.destroyRef)会触发错误,而在ngOnInit中使用则不会?

原因分析

核心差异在于**registerOnChange的调用时机和Angular表单控件的订阅生命周期**:

  1. registerOnChange的执行时机与回调风险
    Angular会在组件初始化早期(早于ngOnInit)调用registerOnChange,此时你直接将父表单控件传入的回调fn订阅到ctrl.valueChanges上。这个fn内部通常会触发父组件的变更检测或视图操作,而组件的视图销毁时机可能早于destroyRef发出的实例销毁信号。当组件销毁过程中,如果ctrl.valueChanges仍有值发出(比如writeValue在销毁阶段被调用),fn会尝试操作已销毁的视图,从而抛出View has already been destroyed错误。

  2. 第二种写法的安全逻辑
    在ngOnInit中订阅valueChanges并通过本地onChange变量转发值,解决了两个关键问题:

  • 订阅的关联时机延迟到组件初始化完成后,此时视图状态更稳定;
  • 当组件销毁时,takeUntilDestroyed会及时终止valueChanges的订阅,不会再触发onChange调用;同时父表单的回调fn通过变量引用关联,当视图销毁后,即使有残留逻辑,也不会直接触发已销毁视图的操作。

简单来说,第一种写法中父表单的回调直接绑定到了组件内部的可观察流,订阅清理时机和视图销毁时机不匹配,导致回调在视图销毁后仍被调用;第二种写法通过中间变量隔离了订阅和父回调的直接绑定,规避了这个时序问题。

内容的提问来源于stack exchange,提问作者Alexandre SIRKO

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 08:43:40