Skip to content
 

Angular 变更检测(Change Detection)

更新: 8/13/2026字数: 0 字 时长: 0 分钟

变更检测是 Angular 保证「数据变化 → 视图更新」的核心机制。理解它对于编写高性能的 Angular 应用至关重要。

一、什么是变更检测

变更检测负责检查组件的数据模型是否发生了变化,如果变了就把视图更新到与数据一致。

Angular 默认通过 Zone.js 实现自动变更检测:Zone.js 会 patch 所有异步 API(setTimeoutPromiseaddEventListener、HTTP 请求等),在这些异步操作完成后自动触发变更检测。

二、变更检测策略

1. Default(默认策略)

默认策略下,任何异步事件(点击、定时器、HTTP 响应等)都会触发整棵组件树的变更检测。实现简单,但可能产生不必要的性能开销。

typescript
import { Component, ChangeDetectionStrategy } from "@angular/core";

@Component({
  selector: "app-default",
  changeDetection: ChangeDetectionStrategy.Default // 默认值,可省略
})
export class DefaultComponent {}

2. OnPush(按需检测)

OnPush 策略下,组件只在以下情况才进行变更检测:

  1. @Input 的引用发生变化(注意:是引用变化,不是对象内部属性变化)
  2. 组件自身模板中的事件处理器被触发
  3. Observable(配合 async 管道)发出新值
  4. 手动调用 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)」和 NgRxSignal 搭配如此自然。

四、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) 模式铺路
  • 可预测、易于推理

六、性能优化实践

  1. 优先使用 OnPush:配合不可变数据和 async 管道,减少不必要的检测
  2. 使用 Signal:获得精细粒度的响应式更新
  3. trackBy:配合 *ngFor 避免重复创建列表项 DOM
  4. 避免在模板中调用函数:模板中的方法调用在每次检测都会执行,复杂计算应放到 computed 或组件字段
  5. 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:detectChangesmarkForCheck 有什么区别?

  • detectChanges:立即同步执行一次检测
  • markForCheck:只做标记,等到下一次检测周期再处理(更推荐,避免重复检测)

Q4:什么时候需要手动 detach()

  • 对于变化频繁但与显示无关的组件(如高频动画、实时数据流),可以 detach 后按需手动检测,避免影响性能。