Angular生命周期钩子

生命周期钩子是Angular组件和指令生命周期中的关键点,允许开发者在特定时刻执行自定义逻辑。

生命周期钩子可视化

点击各个阶段查看详细信息

Constructor
构造函数
OnChanges
输入属性变化
OnInit
组件初始化
DoCheck
变更检测
AfterContentInit
内容投影初始化
AfterViewInit
视图初始化
OnDestroy
组件销毁

1. 生命周期钩子概述

┌─────────────────────────────────────────────────────┐
│               组件生命周期执行顺序                    │
├─────────────────────────────────────────────────────┤
│  1. Constructor()                                   │
│  2. ngOnChanges()   (首次在ngOnInit之前调用)         │
│  3. ngOnInit()                                       │
│  4. ngDoCheck()                                      │
│  5. ngAfterContentInit()                             │
│  6. ngAfterContentChecked()                          │
│  7. ngAfterViewInit()                                │
│  8. ngAfterViewChecked()                             │
│                                                     │
│  ────────────────────────────────────────────────   │
│                                                     │
│  9. ngOnDestroy()   (组件销毁时)                    │
└─────────────────────────────────────────────────────┘
重要概念:生命周期钩子是通过实现特定接口(如OnInit, OnDestroy等)来使用的。每个钩子对应组件生命周期的特定阶段。

2. 各生命周期钩子详解

2.1 Constructor(构造函数)

Constructor()

执行时机:组件实例化时,在Angular调用任何生命周期钩子之前

主要用途:依赖注入、初始化类属性、基本设置

注意事项:此时组件输入属性尚未初始化

// 组件构造函数示例
import { Component, Inject } from '@angular/core';
import { MyService } from './my.service';

@Component({
  selector: 'app-example',
  template: `...`
})
export class ExampleComponent {
  private service: MyService;
  private count: number;

  constructor(
    service: MyService,
    @Inject('API_URL') private apiUrl: string
  ) {
    // 依赖注入
    this.service = service;

    // 初始化属性
    this.count = 0;

    // 基本设置
    console.log('构造函数调用');
    console.log('API URL:', this.apiUrl);

    // 注意:此时不能访问输入属性,因为尚未初始化
    // console.log(this.inputValue); // undefined
  }
}

2.2 ngOnChanges

ngOnChanges(changes: SimpleChanges)

执行时机:输入属性(@Input)的值发生变化时,首次调用在ngOnInit之前

参数:SimpleChanges对象,包含变化的详细信息

主要用途:响应输入属性变化、执行相关逻辑

// ngOnChanges示例
import { Component, Input, OnChanges, SimpleChanges, SimpleChange } from '@angular/core';

@Component({
  selector: 'app-user-profile',
  template: `
    <div>用户名: {{user?.name}}</div>
    <div>年龄: {{user?.age}}</div>
  `
})
export class UserProfileComponent implements OnChanges {
  @Input() user: {name: string, age: number} | null = null;
  @Input() title: string = '';

  // 计算属性
  private previousUser: any = null;

  ngOnChanges(changes: SimpleChanges): void {
    console.log('ngOnChanges触发');

    // 检查user属性是否变化
    if (changes['user']) {
      const userChange: SimpleChange = changes['user'];

      console.log('user变化详情:');
      console.log('当前值:', userChange.currentValue);
      console.log('之前值:', userChange.previousValue);
      console.log('首次变化:', userChange.firstChange);

      // 执行相关逻辑
      if (userChange.currentValue && userChange.previousValue) {
        console.log('用户信息已更新');
      } else if (userChange.currentValue && !userChange.previousValue) {
        console.log('用户信息首次设置');
      }

      this.previousUser = userChange.previousValue;
    }

    // 检查title属性是否变化
    if (changes['title']) {
      console.log('标题变化:', changes['title'].currentValue);
    }

    // 遍历所有变化
    for (const propName in changes) {
      const change = changes[propName];
      console.log(`${propName}: ${change.previousValue} → ${change.currentValue}`);
    }
  }
}

2.3 ngOnInit

ngOnInit()

执行时机:在第一次ngOnChanges之后,组件初始化时调用一次

主要用途:初始化逻辑、数据获取、订阅设置

最佳实践:在此处进行复杂的初始化,而不是在构造函数中

// ngOnInit示例
import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';
import { ProductService } from './product.service';

@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.component.html'
})
export class DashboardComponent implements OnInit {
  user: any = null;
  products: any[] = [];
  loading = true;
  error: string | null = null;

  constructor(
    private userService: UserService,
    private productService: ProductService
  ) {
    // 构造函数中只做基本设置
    console.log('Dashboard组件创建');
  }

  ngOnInit(): void {
    console.log('Dashboard组件初始化');

    // 初始化数据
    this.loadUser();
    this.loadProducts();

    // 设置定时器
    this.startAutoRefresh();

    // 其他初始化逻辑
    this.initializeCharts();
    this.setupEventListeners();
  }

  private loadUser(): void {
    this.userService.getCurrentUser().subscribe({
      next: (user) => {
        this.user = user;
      },
      error: (err) => {
        console.error('加载用户失败:', err);
        this.error = '无法加载用户信息';
      }
    });
  }

  private loadProducts(): void {
    this.productService.getProducts().subscribe({
      next: (products) => {
        this.products = products;
        this.loading = false;
      },
      error: (err) => {
        console.error('加载产品失败:', err);
        this.error = '无法加载产品列表';
        this.loading = false;
      }
    });
  }

  private startAutoRefresh(): void {
    // 定时刷新逻辑
    setInterval(() => {
      this.refreshData();
    }, 30000);
  }

  private initializeCharts(): void {
    // 初始化图表
    console.log('图表初始化');
  }

  private setupEventListeners(): void {
    // 设置事件监听器
    console.log('事件监听器设置完成');
  }

  private refreshData(): void {
    console.log('刷新数据');
  }
}

2.4 ngDoCheck

ngDoCheck()

执行时机:每次变更检测周期中调用,在ngOnChanges之后

主要用途:自定义变更检测逻辑、性能优化

注意事项:频繁调用,避免复杂计算

// ngDoCheck示例
import { Component, Input, DoCheck, KeyValueDiffers } from '@angular/core';

@Component({
  selector: 'app-user-list',
  template: `
    <div *ngFor="let user of users">
      {{user.name}} - {{user.age}}
    </div>
  `
})
export class UserListComponent implements DoCheck {
  @Input() users: any[] = [];

  private differ: any;
  private changeCount = 0;

  constructor(private differs: KeyValueDiffers) {
    // 创建差异检查器
    this.differ = this.differs.find([]).create();
  }

  ngDoCheck(): void {
    // 检查users数组是否变化
    const changes = this.differ.diff(this.users);

    if (changes) {
      this.changeCount++;
      console.log(`第${this.changeCount}次检测到users数组变化`);

      // 遍历变化
      changes.forEachAddedItem((record: any) => {
        console.log('新增用户:', record.currentValue);
      });

      changes.forEachRemovedItem((record: any) => {
        console.log('删除用户:', record.previousValue);
      });

      changes.forEachChangedItem((record: any) => {
        console.log('修改用户:', record.previousValue, '→', record.currentValue);
      });
    }

    // 自定义变更检测逻辑
    this.detectObjectChanges();
  }

  private detectObjectChanges(): void {
    // 检测对象深度变化
    // 这里可以实现自定义的变更检测逻辑
  }
}
性能警告:ngDoCheck会在每次变更检测时调用,频繁执行。在此钩子中应避免复杂计算,否则会导致性能问题。

2.5 ngAfterContentInit 和 ngAfterContentChecked

ngAfterContentInit()

执行时机:在组件内容(通过ng-content投影的内容)初始化后调用一次

主要用途:访问投影内容、操作投影DOM

ngAfterContentChecked()

执行时机:每次检查组件投影内容后调用

主要用途:响应投影内容变化

// 内容投影生命周期示例
import { Component, AfterContentInit, AfterContentChecked, ContentChild, ElementRef } from '@angular/core';

@Component({
  selector: 'app-card',
  template: `
    <div class="card">
      <div class="card-header">
        <ng-content select="[header]"></ng-content>
      </div>
      <div class="card-body">
        <ng-content select="[body]"></ng-content>
      </div>
      <div class="card-footer">
        <ng-content select="[footer]"></ng-content>
      </div>
    </div>
  `
})
export class CardComponent implements AfterContentInit, AfterContentChecked {
  @ContentChild('headerContent') headerContent!: ElementRef;
  @ContentChild('bodyContent') bodyContent!: ElementRef;

  private contentInitCount = 0;
  private contentCheckCount = 0;

  ngAfterContentInit(): void {
    this.contentInitCount++;
    console.log(`ngAfterContentInit调用(第${this.contentInitCount}次)`);

    // 此时可以安全地访问投影内容
    if (this.headerContent) {
      console.log('Header内容:', this.headerContent.nativeElement.textContent);

      // 操作投影内容
      this.headerContent.nativeElement.style.color = 'blue';
    }

    if (this.bodyContent) {
      console.log('Body内容:', this.bodyContent.nativeElement.textContent);
    }
  }

  ngAfterContentChecked(): void {
    this.contentCheckCount++;

    // 只在开发环境记录,避免生产环境过多日志
    if (this.contentCheckCount <= 5) {
      console.log(`ngAfterContentChecked调用(第${this.contentCheckCount}次)`);
    }

    // 检查投影内容是否变化
    if (this.headerContent) {
      const currentText = this.headerContent.nativeElement.textContent;
      // 可以比较之前的值,检测变化
    }
  }
}

2.6 ngAfterViewInit 和 ngAfterViewChecked

ngAfterViewInit()

执行时机:在组件视图及其子视图初始化后调用一次

主要用途:访问DOM元素、初始化第三方库

ngAfterViewChecked()

执行时机:每次检查组件视图和子视图后调用

主要用途:响应视图变化、更新UI

注意事项:避免在此修改组件属性,否则会导致ExpressionChangedAfterItHasBeenCheckedError错误

// 视图生命周期示例
import { Component, AfterViewInit, AfterViewChecked, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';

@Component({
  selector: 'app-chart',
  template: `
    <div #chartContainer class="chart"></div>
    <button (click)="updateData()">更新数据</button>
    <app-chart-tooltip #tooltip></app-chart-tooltip>
  `
})
export class ChartComponent implements AfterViewInit, AfterViewChecked {
  @ViewChild('chartContainer') chartContainer!: ElementRef;
  @ViewChild('tooltip') tooltip: any; // 假设有ChartTooltipComponent

  private chart: any;
  private viewInitCount = 0;
  private viewCheckCount = 0;
  private data: number[] = [10, 20, 30, 40, 50];

  constructor(private cdr: ChangeDetectorRef) {}

  ngAfterViewInit(): void {
    this.viewInitCount++;
    console.log(`ngAfterViewInit调用(第${this.viewInitCount}次)`);

    // 此时可以安全地访问DOM元素
    if (this.chartContainer) {
      console.log('图表容器:', this.chartContainer.nativeElement);

      // 初始化第三方图表库(如Chart.js)
      this.initializeChart();

      // 访问子组件
      if (this.tooltip) {
        console.log('工具提示组件已加载:', this.tooltip);
      }
    }
  }

  ngAfterViewChecked(): void {
    this.viewCheckCount++;

    // 只在开发环境记录
    if (this.viewCheckCount <= 5) {
      console.log(`ngAfterViewChecked调用(第${this.viewCheckCount}次)`);
    }

    // 注意:这里不能修改组件属性,否则会引发错误
    // ❌ this.data.push(60); // 会引发ExpressionChangedAfterItHasBeenCheckedError

    // 如果必须修改,可以使用setTimeout或在下一个周期更新
    if (this.viewCheckCount === 3) {
      setTimeout(() => {
        this.data = [...this.data, 60];
        this.updateChart();
        this.cdr.detectChanges(); // 手动触发变更检测
      });
    }
  }

  private initializeChart(): void {
    // 初始化图表
    console.log('初始化图表');
    // this.chart = new Chart(this.chartContainer.nativeElement, {...});
  }

  updateData(): void {
    this.data = this.data.map(() => Math.random() * 100);
    this.updateChart();
  }

  private updateChart(): void {
    // 更新图表数据
    if (this.chart) {
      console.log('更新图表数据');
    }
  }
}
重要警告:在ngAfterViewChecked中直接修改组件属性会导致ExpressionChangedAfterItHasBeenCheckedError错误。如果需要修改,请使用setTimeoutChangeDetectorRef.detectChanges()

2.7 ngOnDestroy

ngOnDestroy()

执行时机:组件销毁前调用

主要用途:清理资源、取消订阅、移除事件监听器

重要性:防止内存泄漏

// ngOnDestroy示例
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription, interval } from 'rxjs';
import { UserService } from './user.service';
import { DataService } from './data.service';

@Component({
  selector: 'app-user-dashboard',
  templateUrl: './user-dashboard.component.html'
})
export class UserDashboardComponent implements OnInit, OnDestroy {
  user: any = null;
  data: any[] = [];

  // 订阅管理
  private subscriptions: Subscription[] = [];

  // 定时器ID
  private timerId: any;

  // DOM事件监听器引用
  private resizeListener: () => void;

  // WebSocket连接
  private socket: WebSocket | null = null;

  constructor(
    private userService: UserService,
    private dataService: DataService
  ) {
    this.resizeListener = this.handleResize.bind(this);
  }

  ngOnInit(): void {
    this.loadUser();
    this.loadData();
    this.startPolling();
    this.setupWebSocket();
    this.setupEventListeners();
  }

  ngOnDestroy(): void {
    console.log('UserDashboard组件销毁中...');

    // 1. 取消所有RxJS订阅
    this.subscriptions.forEach(sub => {
      if (sub && !sub.closed) {
        sub.unsubscribe();
        console.log('订阅已取消');
      }
    });

    // 2. 清除定时器
    if (this.timerId) {
      clearInterval(this.timerId);
      console.log('定时器已清除');
    }

    // 3. 移除DOM事件监听器
    window.removeEventListener('resize', this.resizeListener);
    console.log('事件监听器已移除');

    // 4. 关闭WebSocket连接
    if (this.socket) {
      this.socket.close();
      console.log('WebSocket连接已关闭');
    }

    // 5. 清理其他资源
    this.cleanupThirdPartyLibraries();

    // 6. 清空数组引用
    this.data = [];

    console.log('UserDashboard组件清理完成');
  }

  private loadUser(): void {
    const userSub = this.userService.getUser().subscribe({
      next: (user) => this.user = user,
      error: (err) => console.error('加载用户失败:', err)
    });
    this.subscriptions.push(userSub);
  }

  private loadData(): void {
    const dataSub = this.dataService.getData().subscribe({
      next: (data) => this.data = data,
      error: (err) => console.error('加载数据失败:', err)
    });
    this.subscriptions.push(dataSub);
  }

  private startPolling(): void {
    // 轮询数据
    const pollSub = interval(5000).subscribe(() => {
      this.refreshData();
    });
    this.subscriptions.push(pollSub);

    // 或使用setInterval
    this.timerId = setInterval(() => {
      this.checkUpdates();
    }, 10000);
  }

  private setupWebSocket(): void {
    try {
      this.socket = new WebSocket('ws://example.com/socket');
      this.socket.onmessage = (event) => {
        this.handleSocketMessage(event.data);
      };
    } catch (error) {
      console.error('WebSocket连接失败:', error);
    }
  }

  private setupEventListeners(): void {
    // 添加窗口大小变化监听器
    window.addEventListener('resize', this.resizeListener);

    // 添加其他事件监听器...
  }

  private handleResize(): void {
    console.log('窗口大小变化');
    // 处理响应式布局
  }

  private handleSocketMessage(data: any): void {
    // 处理WebSocket消息
    console.log('收到WebSocket消息:', data);
  }

  private refreshData(): void {
    // 刷新数据逻辑
  }

  private checkUpdates(): void {
    // 检查更新逻辑
  }

  private cleanupThirdPartyLibraries(): void {
    // 清理第三方库资源
    // 例如:this.chart.destroy();
  }
}

3. 完整的生命周期示例

// 完整的生命周期组件示例
import {
  Component, Input, Output, EventEmitter,
  OnChanges, OnInit, DoCheck,
  AfterContentInit, AfterContentChecked,
  AfterViewInit, AfterViewChecked,
  OnDestroy, SimpleChanges
} from '@angular/core';

@Component({
  selector: 'app-lifecycle-demo',
  template: `
    <div class="demo">
      <h3>生命周期演示组件</h3>
      <p>输入值: {{inputValue}}</p>
      <p>计数: {{count}}</p>
      <button (click)="increment()">增加计数</button>
      <button (click)="emitEvent()">触发事件</button>
      <button (click)="destroy()">销毁组件</button>
    </div>
  `
})
export class LifecycleDemoComponent implements
  OnChanges, OnInit, DoCheck,
  AfterContentInit, AfterContentChecked,
  AfterViewInit, AfterViewChecked,
  OnDestroy {

  @Input() inputValue: string = '';
  @Output() valueChanged = new EventEmitter<string>();

  count = 0;
  private lifecycleLog: string[] = [];
  private intervalId: any;

  constructor() {
    this.log('constructor - 构造函数调用');
  }

  ngOnChanges(changes: SimpleChanges): void {
    this.log('ngOnChanges - 输入属性变化');

    if (changes['inputValue']) {
      this.log(`输入值变化: ${changes['inputValue'].previousValue} → ${changes['inputValue'].currentValue}`);
    }
  }

  ngOnInit(): void {
    this.log('ngOnInit - 组件初始化');

    // 启动定时器
    this.intervalId = setInterval(() => {
      this.count++;
      this.log(`定时器计数: ${this.count}`);
    }, 1000);
  }

  ngDoCheck(): void {
    // 每次变更检测时调用
    // 注意:避免在此处进行复杂操作
  }

  ngAfterContentInit(): void {
    this.log('ngAfterContentInit - 内容投影初始化完成');
  }

  ngAfterContentChecked(): void {
    // 每次内容检查后调用
  }

  ngAfterViewInit(): void {
    this.log('ngAfterViewInit - 视图初始化完成');
  }

  ngAfterViewChecked(): void {
    // 每次视图检查后调用
  }

  ngOnDestroy(): void {
    this.log('ngOnDestroy - 组件销毁');

    // 清理定时器
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.log('定时器已清理');
    }

    // 输出生命周期日志
    console.log('=== 生命周期日志 ===');
    this.lifecycleLog.forEach((log, index) => {
      console.log(`${index + 1}. ${log}`);
    });
  }

  increment(): void {
    this.count++;
    this.log(`手动增加计数: ${this.count}`);
  }

  emitEvent(): void {
    const newValue = `事件触发于 ${new Date().toLocaleTimeString()}`;
    this.valueChanged.emit(newValue);
    this.log(`事件已触发: ${newValue}`);
  }

  destroy(): void {
    this.log('手动触发销毁');
    // 在实际应用中,组件销毁由Angular管理
    // 这里只是演示
  }

  private log(message: string): void {
    const timestamp = new Date().toLocaleTimeString();
    const logMessage = `${timestamp} - ${message}`;
    this.lifecycleLog.push(logMessage);
    console.log(logMessage);
  }
}

4. 生命周期钩子最佳实践

生命周期最佳实践:
  • 构造函数:仅用于依赖注入和基本属性初始化
  • ngOnInit:进行数据获取、订阅设置等初始化逻辑
  • ngOnChanges:响应输入属性变化,执行相关逻辑
  • ngDoCheck:谨慎使用,避免性能问题
  • ngAfterViewInit:访问DOM、初始化第三方库
  • ngOnDestroy:必须清理所有资源,防止内存泄漏
  • 单一职责:每个钩子只做一件事情
  • 错误处理:在适当的位置添加错误处理
Constructor
ngOnChanges
ngOnInit
ngDoCheck
AfterContentInit
AfterViewInit
ngOnDestroy

5. 常见问题和解决方案

ExpressionChangedAfterItHasBeenCheckedError

这个错误通常发生在ngAfterViewChecked中修改了组件属性。

// 解决方案1: 使用setTimeout
ngAfterViewChecked(): void {
  setTimeout(() => {
    this.someProperty = newValue;
  });
}

// 解决方案2: 使用ChangeDetectorRef
constructor(private cdr: ChangeDetectorRef) {}

ngAfterViewChecked(): void {
  this.someProperty = newValue;
  this.cdr.detectChanges(); // 手动触发变更检测
}

// 解决方案3: 重新设计组件逻辑,避免在视图检查后修改属性
何时使用哪个钩子?
场景 使用钩子 原因
获取初始数据 ngOnInit 输入属性已初始化
响应输入变化 ngOnChanges 专门处理属性变化
访问DOM元素 ngAfterViewInit 视图已渲染完成
访问投影内容 ngAfterContentInit 投影内容已初始化
清理资源 ngOnDestroy 组件销毁前
自定义变更检测 ngDoCheck 每次变更检测时

6. 性能优化技巧

// 1. 使用OnPush变更检测策略
@Component({
  selector: 'app-optimized',
  template: '...',
  changeDetection: ChangeDetectionStrategy.OnPush
})

// 2. 避免在ngDoCheck中执行复杂计算
ngDoCheck(): void {
  // ❌ 避免这样做
  // const result = this.computeHeavyOperation();

  // ✅ 使用纯管道或在服务中处理
}

// 3. 使用trackBy优化ngFor
<div *ngFor="let item of items; trackBy: trackById">
  {{item.name}}
</div>

trackById(index: number, item: any): number {
  return item.id;
}

// 4. 取消订阅防止内存泄漏
private destroy$ = new Subject();

ngOnInit(): void {
  this.dataService.getData()
    .pipe(takeUntil(this.destroy$))
    .subscribe(data => this.data = data);
}

ngOnDestroy(): void {
  this.destroy$.next();
  this.destroy$.complete();
}

7. 测试生命周期钩子

// lifecycle-demo.component.spec.ts
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { LifecycleDemoComponent } from './lifecycle-demo.component';

describe('LifecycleDemoComponent', () => {
  let component: LifecycleDemoComponent;
  let fixture: ComponentFixture<LifecycleDemoComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [LifecycleDemoComponent]
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(LifecycleDemoComponent);
    component = fixture.componentInstance;
  });

  it('应该创建组件', () => {
    expect(component).toBeTruthy();
  });

  it('应该在初始化时启动定时器', fakeAsync(() => {
    spyOn(console, 'log');
    fixture.detectChanges(); // 触发ngOnInit

    tick(1000); // 模拟1秒过去
    expect(console.log).toHaveBeenCalledWith(expect.stringContaining('定时器计数'));
  }));

  it('应该在输入变化时调用ngOnChanges', () => {
    spyOn(component, 'ngOnChanges').and.callThrough();

    component.inputValue = '新值';
    fixture.detectChanges();

    expect(component.ngOnChanges).toHaveBeenCalled();
  });

  it('应该在销毁时清理定时器', () => {
    spyOn(window, 'clearInterval');

    fixture.destroy(); // 触发ngOnDestroy

    expect(window.clearInterval).toHaveBeenCalled();
  });

  it('应该正确响应按钮点击', () => {
    const initialCount = component.count;

    component.increment();

    expect(component.count).toBe(initialCount + 1);
  });
});
学习建议:
  • 通过实际编码练习每个生命周期钩子
  • 在开发工具中观察钩子的执行顺序
  • 为关键业务组件添加生命周期日志
  • 定期检查内存泄漏问题
  • 使用Angular DevTools进行性能分析
  • 理解变更检测机制与生命周期钩子的关系