生命周期钩子是Angular组件和指令生命周期中的关键点,允许开发者在特定时刻执行自定义逻辑。
点击各个阶段查看详细信息
┌─────────────────────────────────────────────────────┐ │ 组件生命周期执行顺序 │ ├─────────────────────────────────────────────────────┤ │ 1. Constructor() │ │ 2. ngOnChanges() (首次在ngOnInit之前调用) │ │ 3. ngOnInit() │ │ 4. ngDoCheck() │ │ 5. ngAfterContentInit() │ │ 6. ngAfterContentChecked() │ │ 7. ngAfterViewInit() │ │ 8. ngAfterViewChecked() │ │ │ │ ──────────────────────────────────────────────── │ │ │ │ 9. ngOnDestroy() (组件销毁时) │ └─────────────────────────────────────────────────────┘
执行时机:组件实例化时,在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
}
}
执行时机:输入属性(@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}`);
}
}
}
执行时机:在第一次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('刷新数据');
}
}
执行时机:每次变更检测周期中调用,在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 {
// 检测对象深度变化
// 这里可以实现自定义的变更检测逻辑
}
}
执行时机:在组件内容(通过ng-content投影的内容)初始化后调用一次
主要用途:访问投影内容、操作投影DOM
执行时机:每次检查组件投影内容后调用
主要用途:响应投影内容变化
// 内容投影生命周期示例
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;
// 可以比较之前的值,检测变化
}
}
}
执行时机:在组件视图及其子视图初始化后调用一次
主要用途:访问DOM元素、初始化第三方库
执行时机:每次检查组件视图和子视图后调用
主要用途:响应视图变化、更新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('更新图表数据');
}
}
}
ExpressionChangedAfterItHasBeenCheckedError错误。如果需要修改,请使用setTimeout或ChangeDetectorRef.detectChanges()。
执行时机:组件销毁前调用
主要用途:清理资源、取消订阅、移除事件监听器
重要性:防止内存泄漏
// 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();
}
}
// 完整的生命周期组件示例
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);
}
}
这个错误通常发生在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 | 每次变更检测时 |
// 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();
}
// 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);
});
});