Angular 变更检测(Change Detection)
更新: 8/13/2026字数: 0 字 时长: 0 分钟
变更检测是 Angular 保证「数据变化 → 视图更新」的核心机制。理解它对于编写高性能的 Angular 应用至关重要。
一、什么是变更检测
变更检测负责检查组件的数据模型是否发生了变化,如果变了就把视图更新到与数据一致。
Angular 默认通过 Zone.js 实现自动变更检测:Zone.js 会 patch 所有异步 API(setTimeout、Promise、addEventListener、HTTP 请求等),在这些异步操作完成后自动触发变更检测。
二、变更检测策略
1. Default(默认策略)
默认策略下,任何异步事件(点击、定时器、HTTP 响应等)都会触发整棵组件树的变更检测。实现简单,但可能产生不必要的性能开销。
typescript
import { Component, ChangeDetectionStrategy } from "@angular/core";
@Component({
selector: "app-default",
changeDetection: ChangeDetectionStrategy.Default // 默认值,可省略
})
export class DefaultComponent {}2. OnPush(按需检测)
OnPush 策略下,组件只在以下情况才进行变更检测:
@Input的引用发生变化(注意:是引用变化,不是对象内部属性变化)- 组件自身模板中的事件处理器被触发
- Observable(配合
async管道)发出新值 - 手动调用
ChangeDetectorRef的检测方法
typescript
@Component({
selector: "app-onpush",
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {
@Input() user!: User;
}三、OnPush 的「坑」:不可变数据
OnPush 比较的是引用。如果直接修改对象内部属性,引用不变,视图不会更新:
typescript
// ❌ 错误:修改内部属性,引用没变,OnPush 组件不会更新
this.user.name = "新名字";
// ✅ 正确:返回新对象,引用变化,触发更新
this.user = { ...this.user, name: "新名字" };这就是为什么 OnPush 与「不可变数据(immutable data)」和 NgRx、Signal 搭配如此自然。
四、ChangeDetectorRef 手动控制
ChangeDetectorRef 提供了手动控制变更检测的能力:
typescript
import { Component, ChangeDetectorRef, ChangeDetectionStrategy } from "@angular/core";
@Component({
selector: "app-manual",
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ManualComponent {
data: any;
constructor(private cdr: ChangeDetectorRef) {}
loadData() {
// 某些脱离 Zone.js 的场景(如第三方库回调),需要手动触发
this.data = fetchData();
this.cdr.detectChanges(); // 立即执行一次变更检测
}
}常用方法
| 方法 | 作用 |
|---|---|
detectChanges() | 立即对当前组件(含其子组件)执行一次变更检测 |
markForCheck() | 标记当前组件及其祖先需要检测(配合 OnPush 常用) |
detach() | 将组件从变更检测树中分离,不再自动检测 |
reattach() | 重新加入变更检测树 |
五、Signal 与新的变更检测
Angular 16+ 的 Signals 带来了更精细的变更检测:Signal 的变化会精确地只更新依赖它的视图部分,检测粒度更细。而到了 Angular 18+,配合 zoneless(无 Zone) 模式,可以彻底摆脱 Zone.js 的全量检测,进一步减小包体积、提升性能。
typescript
import { Component, signal } from "@angular/core";
@Component({
selector: "app-counter",
template: `<p>{{ count() }}</p><button (click)="add()">+1</button>`
})
export class CounterComponent {
count = signal(0);
add() {
this.count.update((v) => v + 1); // 只更新依赖 count 的视图
}
}Signal 的优势:
- 更精细的更新粒度,性能更好
- 不再强依赖
Zone.js,为未来的 zoneless(无 Zone) 模式铺路 - 可预测、易于推理
六、性能优化实践
- 优先使用
OnPush:配合不可变数据和async管道,减少不必要的检测 - 使用
Signal:获得精细粒度的响应式更新 trackBy:配合*ngFor避免重复创建列表项 DOM- 避免在模板中调用函数:模板中的方法调用在每次检测都会执行,复杂计算应放到
computed或组件字段 - 用
async管道代替手动订阅:自动管理订阅和检测
typescript
// ❌ 模板中调用函数:每次变更检测都执行
<div>{{ expensiveCalculation() }}</div>
// ✅ 提前计算好,或用 computed
readonly result = computed(() => this.expensiveCalculation());
<div>{{ result() }}</div>七、常见问题解答
Q1:为什么我的 OnPush 组件不更新?
- 最常见原因:直接修改了对象的内部属性而没有更换引用。改用不可变数据(返回新对象)或手动
markForCheck()。
Q2:Zone.js 一定要用吗?
- 不是。
Angular 18+支持 zoneless 变更检测(配合 Signal),可减少包体积并提升性能。老项目仍默认使用 Zone.js。
Q3:detectChanges 和 markForCheck 有什么区别?
detectChanges:立即同步执行一次检测markForCheck:只做标记,等到下一次检测周期再处理(更推荐,避免重复检测)
Q4:什么时候需要手动 detach()?
- 对于变化频繁但与显示无关的组件(如高频动画、实时数据流),可以 detach 后按需手动检测,避免影响性能。