Angular 生命周期 Hook 中首次获取 current billing
结论
第一次点击时得到 undefined,通常不是生命周期 Hook 出了问题,而是发送数据时,billing.component 还没有创建:
- 父组件先通过
Subject.next()发送数据; *ngIf随后才创建billing.component;- 子组件到
ngOnInit才开始订阅,此时第一次数据已经发送; - 普通
Subject不会把之前的值重新发送给新订阅者。
如果数据只在这两个父子组件之间传递,优先使用 @Input()。如果多个组件需要通过 service 共享状态,可以改用 BehaviorSubject 或 ReplaySubject(1),让后创建的订阅者也能收到最新值。
方案一:使用 @Input() 传递数据
如果 billing.component 是 billings.component 模板中的子组件,直接使用 @Input() 即可。
父组件保存当前 billing:
export class BillingsComponent {
currentBilling?: Billing;
setCurrentBilling(billing: Billing): void {
this.currentBilling = billing;
}
}
父组件模板:
<button
*ngFor="let billing of billings"
type="button"
(click)="setCurrentBilling(billing)"
>
{{ billing.name }}
</button>
<app-billing
*ngIf="currentBilling"
[current]="currentBilling"
></app-billing>
子组件通过 @Input() 接收:
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
export class BillingComponent implements OnChanges {
@Input() current?: Billing;
ngOnChanges(changes: SimpleChanges): void {
if (changes['current']?.currentValue) {
console.log('Current billing:', changes['current'].currentValue);
}
}
}
输入值首次设置或之后发生变化时,ngOnChanges 都会执行。对于可能更新的 @Input() 数据,它比只在 ngOnInit 中读取更合适。
如果数据只用于模板展示,不需要实现生命周期 Hook:
<p>{{ current?.name }}</p>
方案二:在 service 中使用 BehaviorSubject
如果多个组件没有直接的父子关系,但都需要访问 current billing,可以由 service 保存最新状态。
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class BillingService {
private readonly currentBillingSubject =
new BehaviorSubject<Billing | null>(null);
readonly currentBilling$ =
this.currentBillingSubject.asObservable();
setCurrentBilling(billing: Billing): void {
this.currentBillingSubject.next(billing);
}
}
在 billings.component 中先设置数据,再显示子组件:
export class BillingsComponent {
showBilling = false;
constructor(private readonly billingService: BillingService) {}
setCurrentBilling(billing: Billing): void {
this.billingService.setCurrentBilling(billing);
this.showBilling = true;
}
}
模板:
<app-billing *ngIf="showBilling"></app-billing>
在 billing.component 中订阅:
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subject, filter, takeUntil } from 'rxjs';
export class BillingComponent implements OnInit, OnDestroy {
current?: Billing;
private readonly destroy$ = new Subject<void>();
constructor(private readonly billingService: BillingService) {}
ngOnInit(): void {
this.billingService.currentBilling$
.pipe(
filter((billing): billing is Billing => billing !== null),
takeUntil(this.destroy$)
)
.subscribe(billing => {
this.current = billing;
console.log('Current billing:', billing);
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
BehaviorSubject 会保存最近一次传入的值。即使 billing.component 在 next() 之后才创建,也能在订阅后立即收到当前 billing。
使用 async pipe 简化订阅
如果 current billing 主要用于模板展示,可以使用 async pipe,省去手动订阅:
export class BillingComponent {
readonly currentBilling$ =
this.billingService.currentBilling$;
constructor(private readonly billingService: BillingService) {}
}
模板:
<ng-container *ngIf="currentBilling$ | async as current">
<h2>{{ current.name }}</h2>
</ng-container>
async pipe 会自动订阅,并在组件销毁时取消订阅,可以减少内存泄漏的风险。
为什么普通 Subject 会漏掉第一次数据
下面这段代码容易出现首次点击收不到数据的问题:
currentBillingSubject.next(billing);
showBilling = true;
第一次点击前,受 *ngIf 控制的 billing.component 还不存在,自然也没有订阅 service。普通 Subject 只会把值发送给当时已经存在的订阅者,不会保存历史值。
第一次点击会创建子组件。等到第二次点击时,订阅已经建立,所以数据可以正常收到。这就是“第一次 undefined,之后正常”的常见原因。
注意事项
ngOnInit只执行一次,不适合用来持续读取会发生变化的数据。@Input()的首次赋值和后续变化都可以在ngOnChanges中处理。- 跨组件共享状态时,可以使用
BehaviorSubject、ReplaySubject(1)或其他明确的状态管理方式。 - 不要直接公开
Subject。service 应只公开Observable,并提供修改状态的方法。 BehaviorSubject的初始值如果是null,消费端需要过滤或处理空值。- 确保各组件使用的是同一个 service 实例。通常应设置
providedIn: 'root',不要再在多个组件的providers中重复声明,否则不同组件可能拿到不同的 service 实例。
备注:内容仅供参考。
