为何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表单控件的订阅生命周期**:
registerOnChange的执行时机与回调风险
Angular会在组件初始化早期(早于ngOnInit)调用registerOnChange,此时你直接将父表单控件传入的回调fn订阅到ctrl.valueChanges上。这个fn内部通常会触发父组件的变更检测或视图操作,而组件的视图销毁时机可能早于destroyRef发出的实例销毁信号。当组件销毁过程中,如果ctrl.valueChanges仍有值发出(比如writeValue在销毁阶段被调用),fn会尝试操作已销毁的视图,从而抛出View has already been destroyed错误。第二种写法的安全逻辑
在ngOnInit中订阅valueChanges并通过本地onChange变量转发值,解决了两个关键问题:
- 订阅的关联时机延迟到组件初始化完成后,此时视图状态更稳定;
- 当组件销毁时,
takeUntilDestroyed会及时终止valueChanges的订阅,不会再触发onChange调用;同时父表单的回调fn通过变量引用关联,当视图销毁后,即使有残留逻辑,也不会直接触发已销毁视图的操作。
简单来说,第一种写法中父表单的回调直接绑定到了组件内部的可观察流,订阅清理时机和视图销毁时机不匹配,导致回调在视图销毁后仍被调用;第二种写法通过中间变量隔离了订阅和父回调的直接绑定,规避了这个时序问题。
内容的提问来源于stack exchange,提问作者Alexandre SIRKO

