PAYATHON 2026

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 实例。

备注:内容仅供参考。