CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / ANGULAR / P-170 · Angular 高级教程

Angular 22+ 教程 13:组件生命周期钩子

生命周期钩子(Lifecycle Hooks)是组件从创建到销毁过程中的固定回调点。理解每个钩子的触发时机、执行顺序,以及信号时代哪些钩子可以”退休”,是从会写 Angular 到写好 Angular 的必经之路。本文基于 Angular v22:钩子 API 全部保留,但推荐用法已大幅向信号 API 迁移。

生命周期全景

钩子接口调用时机信号时代还需要吗
ngOnChangesOnChanges首次及每次 @Input 装饰器输入变化后(先于 ngOnInit优先用 input() + computed / effect 替代
ngOnInitOnInit首次 ngOnChanges 之后调用一次仍常用:一次性初始化
ngDoCheckDoCheck每次变更检测周期极少;配合 differ 做细粒度 diff
ngAfterContentInitAfterContentInit内容投影首次渲染后contentChild() / contentChildren() 信号后通常不需要
ngAfterContentCheckedAfterContentChecked每次投影内容检查后少用
ngAfterViewInitAfterViewInit组件自身及子视图首次渲染后viewChild() 信号查询后通常不需要
ngAfterViewCheckedAfterViewChecked每次视图检查后少用,常被误当作”CD 钩子”
ngOnDestroyOnDestroy实例销毁前仍核心;DestroyRef / takeUntilDestroyed 是等价形态

注意:ngOnChanges 只响应 @Input 装饰器输入。函数式 input() 的变化不会触发它——这是迁移老代码时最容易踩的坑。

执行顺序

单次创建流程(忽略子组件交错的细节):

sequenceDiagram
    participant P as 父组件
    participant C as 子组件
    P->>C: 实例化(构造器执行)
    C->>C: ngOnChanges(首次 @Input)
    C->>C: ngOnInit
    C->>C: ngDoCheck
    C->>C: ngAfterContentInit
    C->>C: ngAfterContentChecked
    C->>C: ngAfterViewInit
    C->>C: ngAfterViewChecked
    Note over C: 交互与后续变更检测周期
    P->>C: ngOnDestroy(销毁前)

两个容易忽略的细节:

  • 父子组件的钩子是交错的:子组件的 ngAfterViewInit 先于父组件的 ngAfterViewInit(深度优先,子视图渲染完毕父视图才算完毕)。
  • 开发模式下 Angular 会对变更检测执行双检查(detect changes twice),因此”在钩子里同步修改模板已读取的状态”会抛出 ExpressionChangedAfterItHasBeenCheckedError——这在生产模式不报错,属于开发期保护。

关键钩子详解

ngOnInit:一次性初始化

构造器里只做轻量工作(依赖注入、字段默认值),需要依赖输入的初始化放 ngOnInit(此时 @Input 已绑定)。不过信号时代有了更细的划分:初始化”组件自身状态”用构造器里的 computed / effect 也可以表达,ngOnInit 更多承担”发起首次数据加载”这类副作用。

ngOnChanges:响应输入变化

历史写法:

export class FilterComponent implements OnChanges {
  @Input() keyword = '';

  ngOnChanges(changes: SimpleChanges) {
    if (changes['keyword']) {
      this.search(changes['keyword'].currentValue);
    }
  }
}

v22 推荐写法:

export class FilterComponent {
  keyword = input.required<string>();

  constructor() {
    effect(() => this.search(this.keyword()));
  }
}

AfterContent 与 AfterView:投影与视图

  • ngAfterContentInit/Checked<ng-content> 投影内容渲染完毕,此时可以安全读取 @ContentChild 查询结果。
  • ngAfterViewInit/Checked:自身模板渲染完毕,此时可以安全读取 @ViewChild 查询结果、执行 DOM 测量。

ngOnDestroy:清理

凡是手工申请的资源——定时器、订阅、WebSocket、浏览器监听器——都要在这里归还。v22 提供了两个更不易漏的形态:

import { DestroyRef, inject, takeUntilDestroyed } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

export class TimerComponent implements OnDestroy {
  private destroyRef = inject(DestroyRef);

  constructor() {
    // 形态一:DestroyRef 注册回调
    const id = setInterval(() => this.tick(), 1000);
    this.destroyRef.onDestroy(() => clearInterval(id));

    // 形态二:RxJS 订阅自动跟随组件销毁
    this.http.get<Stats>('/api/stats')
      .pipe(takeUntilDestroyed())
      .subscribe(stats => this.stats.set(stats));
  }

  ngOnDestroy() {
    // 传统钩子仍有效,三种形态可共存
  }
}

信号时代:谁退休了

传统组合v22 推荐说明
@Input() + ngOnChangesinput() / input.required() + computed / effect声明式响应;信号 input 不触发 ngOnChanges
@ViewChild + ngAfterViewInitviewChild() / viewChild.required()声明即用,模板与 effect 中读取自动追踪
@ContentChild + ngAfterContentInitcontentChild() / contentChildren()同上
AfterViewInit 里操作 DOMafterNextRender / afterRenderEffect与变更检测解耦,SSR 安全
ngOnDestroy 手动退订takeUntilDestroyed / DestroyRef.onDestroy样板代码更少、更难漏

afterRenderEffect:渲染后副作用

需要”每次渲染后都执行”的 DOM 操作(滚动到底部、测量尺寸),用 afterRenderEffect

export class ChatComponent {
  private list = viewChild.required<ElementRef<HTMLElement>>('list');
  messages = signal<Message[]>([]);

  constructor() {
    afterRenderEffect(() => {
      const el = this.list().nativeElement;
      el.scrollTop = el.scrollHeight;
    });
  }
}

它支持按阶段注册回调(beforeRender / afterRender),读取 DOM 的动作放在渲染前后这个明确窗口内,避免与变更检测互相干扰。只在下一次渲染执行一次的场景用 afterNextRender

Zoneless 下的注意点

Zoneless 稳定后(v21 起),钩子本身照常执行,区别在于”谁触发刷新”:只有信号写入等显式通知才会调度变更检测。因此:

export class ClockComponent {
  // 反例:普通字段,Zoneless 下模板不会刷新
  nowPlain = new Date();

  // 正确:信号驱动
  now = signal(new Date());

  constructor() {
    const timer = setInterval(() => this.now.set(new Date()), 1000);
    inject(DestroyRef).onDestroy(() => clearInterval(timer));
  }
}
<div>现在时间:{{ now() | date: 'HH:mm:ss' }}</div>

推论:在 ngOnInit 等钩子里修改”普通字段”对模板不可见(历史上依赖 zone.js 补丁的隐式刷新在 Zoneless 下消失)。凡是模板要读的状态,一律用信号表达。

常见坑

  • 构造器里读取信号 input 得到 undefined(非 required 时):首次变更检测之前输入尚未绑定。需要”输入就绪后”的逻辑放到 effectngOnInit@Input 场景)里。required input 在绑定前读取会直接抛运行时错误,这在开发期能更快暴露问题。
  • ExpressionChangedAfterItHasBeenCheckedError:在 ngAfterViewInit 里同步修改父组件已渲染的状态。修复方向:改为 computed 声明派生状态,或用 afterNextRender / effect 延后一拍。
  • 钩子与继承:子类覆写钩子时记得调用 super.ngOnInit(),Angular 不会自动串联。
  • ngAfterViewChecked 里做重活是性能杀手:它每个 CD 周期都跑,且常引发上面的 ExpressionChanged 错误。

系列导航

← 算法 013:堆排序(Heap Sort) 目录 设计模式 013:外观(Facade) →
← 返回文章列表