FORMA

变更检测

Angular 在数据变化后更新 DOM 的过程称为 变更检测(Change Detection)。理解默认策略有助于性能调优。运行时技巧见 runtime

核心概念

变更检测要解决的问题:「数据变了,如何知道该更新哪些 DOM?」Angular 采用组件树自顶向下遍历的策略——每次检测周期,从根组件开始,按树的顺序检查每个组件的模板绑定表达式是否变化,若变化则更新对应 DOM。触发一次遍历的时机由 Zone.js(默认)或显式调用(ChangeDetectorRef.detectChanges())决定。

默认机制与 Zone.js

典型应用在 polyfills 中引入 Zone.js:浏览器异步任务(事件、定时器、XHRPromise 等)结束后触发 Angular 检查组件树。Zone.js 通过打补丁(monkey-patch) 全局的 setTimeoutaddEventListenerfetch 等 API,在任务完成时自动调用 ApplicationRef.tick()

  • 开发时可用 NgZone.runOutsideAngular 执行与 UI 无关的高频任务,必要时再 run() 回区。
  • Zoneless(无 Zone):Angular 18/19 曾提供实验 API provideExperimentalZonelessChangeDetection();Angular 20 起重命名为 provideZonelessChangeDetection()(Developer Preview),约 20.2 起可按稳定 API 用于生产Angular 21+ 的 CLI 新项目默认即 zoneless(通常无需再手动添加该 provider)。开启后依赖 Signal / markForCheck 等显式通知触发检测,也可省去 zone.js。具体以当前 Zoneless 指南 为准。
ts
// main.ts:在 Angular 20.x 显式开启 zoneless(v21+ 新项目一般已默认)
bootstrapApplication(AppComponent, {
  providers: [provideZonelessChangeDetection()],
});

变更检测策略

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

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {}
策略行为
Default每次变更检测周期检查该组件(子树按规则参与)
OnPush仅在 输入引用变化事件来自本模板async 管道发出新值Signal 更新 等时检查

列表与展示组件宜使用 OnPush,并配合不可变数据或 Signal。

OnPush 触发条件详解

OnPush 组件被跳过检测,除非发生以下任一情况:

  1. @Input() 引用变化:父组件传入了新的对象/数组引用(浅比较,obj.prop = x 不会触发,必须 obj = { ...obj, prop: x })。
  2. 组件(或其子组件)内触发的 DOM 事件:如 (click)="onClick()",会标记该组件及祖先链需要检测。
  3. async 管道收到新值user$ | async 在 Observable 发出新值时自动标记。
  4. 依赖的 Signal 变化:读取的 signal() 更新会精确标记使用它的视图。
  5. 手动调用 ChangeDetectorRef.markForCheck()detectChanges()
ts
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<p>{{ user().name }}</p>`,
})
export class UserCardComponent {
  user = input.required<User>();

  // 手动触发场景:定时器等不经过模板事件/Input/Signal 的更新
  private cdr = inject(ChangeDetectorRef);

  constructor() {
    setInterval(() => {
      this.lastPing = Date.now();
      this.cdr.markForCheck(); // OnPush 下需要显式标记
    }, 5000);
  }
}

Signals 与检测

Signal 更新会标记相关视图节点,有利于缩小检测范围(与 OnPush 协同)。Angular 的新一代渲染管线("signal-based reactivity")逐步做到只更新读取了该 Signal 的具体绑定,而不必重新检查整个组件模板,理论上比传统脏检查更精细。详见 Signal 基础

最佳实践

  • 新组件默认写 OnPush,仅在确有需要(如深层双向绑定、遗留代码迁移期)才退回 Default
  • 传给 OnPush 子组件的数据保持不可变:更新数组/对象时用扩展运算符或 signal.update() 产生新引用,而不是原地修改。
  • 定时器、WebSocket 回调等脱离 Zone 感知路径的更新,搭配 markForCheck() 或迁移到 signal.set()
  • 优先用 async 管道消费 Observable,而不是手动 subscribe 后再 markForCheck,减少遗漏标记的风险。
  • 若逐步迁移到 Signal,可先在叶子组件启用 OnPush + Signal 输入,验证收益后再扩大范围。

常见坑

现象常见原因处理
OnPush 组件数据变了但视图没更新原地修改了对象/数组,引用未变使用不可变更新或 Signal
定时器里更新的数据不显示定时器在 Zone 外或 markForCheck 未调用显式 markForCheck(),或用 Signal
迁移到 zoneless 后部分组件不更新依赖 Zone 自动触发的旧代码路径未迁移到 Signal逐步替换为 Signal/markForCheck
大表单频繁全树检测导致卡顿顶层组件未 OnPush,任意输入都触发全树检查拆分子组件并逐层 OnPush

与 Vue / React 的变更检测机制对照

Angular(默认)VueReact
触发方式Zone.js 拦截异步 API依赖追踪(Proxy)显式 setState/dispatch
检测范围整棵组件树遍历(Zone 模式)精确到依赖该数据的组件触发更新的组件及其子树
精细化方案Signal(细粒度)天生细粒度(响应式系统)memo + 状态下沉

Angular 默认的 Zone 模式偏向「保守但简单」:不需要开发者手动声明依赖,但检测范围较大;Signal 的引入让 Angular 逐步向 Vue 式的「精确依赖追踪」靠近。

延伸阅读

参考文献

以下链接在编写时均可正常访问:

资料说明
变更检测官方性能建议
OnPushAPI
Zone.js仓库说明
Zoneless 指南开发者预览(v21+ 新项目默认启用)
ChangeDetectorRefAPI

Series

performance

1 / 2