Angular output 配合 take(1) 后,父组件的事件没有触发
Angular 21.2.12 的 output 监听器在 emit 内同步退订,会移动数组并漏掉后续回调。核对 take(1) 的订阅顺序,采用保留发射期索引的实现。
子组件只发出一次 ready,同一元素上的指令收到了,父组件的 (ready) 却始终不执行,控制台也没有错误。如果指令通过 outputToObservable(...).pipe(take(1)) 消费信号式 output,要检查发射期间的同步退订,而不是先给父组件补一次变更检测。
Angular 21.2.12 的 OutputEmitterRef 会直接遍历监听器数组,退订则立即从同一个数组里删除元素。较早的监听器在回调中退订自己,后面的监听器就可能被跳过。take(1) 恰好会在收到第一个值时同步退订。
顺序决定为什么只有一个监听器漏收
例如子组件用 ready = output<string>(),指令先订阅一次性通知,父组件再通过模板绑定订阅。下面是指令已有注入上下文中的订阅片段,host 指向那个真实子组件:
import { outputToObservable } from '@angular/core/rxjs-interop';
import { take } from 'rxjs';
outputToObservable(host.ready)
.pipe(take(1))
.subscribe(value => console.log('directive:', value));
第一次 emit 时,数组第 0 项的指令收到值并退订。旧实现用 splice 删除第 0 项,父组件的回调随即从第 1 项移动到第 0 项;遍历器接着取第 1 项,于是没有调用它。事件不是被父组件的判断拦住了,而是没有送到这个监听器。
因此,去掉 take(1) 后“恢复正常”很有辨识度,但不能据此把所有 RxJS 一次性订阅都判错。要确认它们订阅的是同一个 output,退订确实发生在 emit 调用栈内,而且还有注册在后面的监听器。组件已经销毁、名字拼错或父模板没有绑定,也会表现为收不到事件,处理方式不同。
使用保留发射期索引的实现
Angular 22.2.3 的正式实现已经区分发射中与平时的退订:发射期间先把对应位置置空,结束后再清理,避免直接移动尚未访问的监听器。它并不是每次复制一份监听器数组。
通过项目既有更新流程采用包含该处理、且与应用兼容的 Angular 组合。核对实际安装的 @angular/core,并保持框架相关包版本匹配;只升级 CLI 或 RxJS 不会替换 OutputEmitterRef。这里确认的是 22.2.3 的正式源码,不据此推断每条旧版本维护线都已经回补。
暂时无法更新时,可以评估让该输出保留原来的 EventEmitter 实现,或调整那条一次性消费路径,使其不在当前 emit 内移除监听器。两者都需要核对现有订阅生命周期。不要简单删除 take(1) 后留下永久订阅,也不要把所有输出改成异步发射来遮住数组变动;异步边界会改变业务顺序。
回归应保留“较早监听器收到一次后退订、后面监听器仍在”的排列。第一次发射两个监听器都应收到;再次发射时,一次性监听器不再收到,仍然有效的父组件绑定继续收到。另查组件销毁后的清理,避免修好漏收又增加重复处理。只在日志里看到指令成功,不足以验收父模板的处理路径。


