3.1变更检测 (Change Detection) 3.1 变更检测 (Change Detection) 在构建现代Web应用时,响应式和高效的用户界面至关重要。Angular 框架的核心机制之一就是变更检测 (Change Detection),它负责自动检测应用状态的变化,并将这些变化反映到用户界面上,确保视图与数据模型保持同步。理解 Angular 的变更检测机制对于构建高性能、可维护的 Angular 应用至关重要。 3.1.1 变更检测的核心概念 变更检测是 Angular 框架用来确定何时需要更新组件视图的过程。当应用状态发生变化时,Angular 需要知道哪些组件受到影响,并更新它们的模板以反映最新的数据。
Default Strategy: {{ message }}
Update Message ` }) export class DefaultStrategyComponent { message = 'Initial Message'; updateMessage() { this.message = 'Message updated at ' + new Date().toLocaleTimeString(); } } ``` 在这个例子中,`DefaultStrategyComponent` 使用默认的 `ChangeDetectionStrategy.Default` 策略。当点击 "Update Message" 按钮时,`updateMessage()` 方法会修改 `message` 属性的值。由于浏览器事件(点击事件)会触发变更检测,Angular 会检测到 `message` 属性的变化,并更新视图中 `{{ message }}` 的显示内容。 * **`ChangeDetectionStrategy.OnPush` (OnPush 策略):** 这是一种**按需检测 (On-Demand Detection)** 策略,旨在提升应用性能,尤其是在组件树庞大且复杂的应用中。`OnPush` 策略告诉 Angular,只有在以下两种情况下才需要检查组件及其子组件的变化: 1. **输入属性 (`@Input`) 引用发生变化:** 当父组件传递给子组件的输入属性的**引用**发生变化时。这意味着你需要使用**不可变数据 (Immutable Data)** 模式,确保每次数据变化都创建一个新的对象引用。 2. **组件或其子组件触发了事件处理函数:** 例如组件模板中的事件绑定函数被调用时。 **`OnPush` 策略的工作原理:** 与 `Default` 策略不同,`OnPush` 策略不会在每次浏览器事件或异步操作后都遍历整个组件树。它只会在上述两种特定情况下才进行变更检测。这可以显著减少不必要的变更检测操作,从而提升性能。 **代码实践 (OnPush 策略):** ```typescript import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; interface User { name: string; age: number; } @Component({ selector: 'app-on-push-strategy', template: `OnPush Strategy: {{ user.name }}, Age: {{ user.age }}
Update Age (Immutable) Update Name (Mutable - Not Recommended for OnPush) `, changeDetection: ChangeDetectionStrategy.OnPush // 设置为 OnPush 策略 }) export class OnPushStrategyComponent { @Input() user: User = { name: 'Initial User', age: 30 }; updateAge() { // 使用不可变数据更新 age,创建新的 user 对象 this.user = { ...this.user, age: this.user.age + 1 }; } updateNameMutably() { // 错误示范:直接修改 user 对象的 name 属性 (可变数据) this.user.name = 'Name Mutated'; // 视图可能不会更新! } } ``` ```typescript // 父组件 (例如 AppComponent) import { Component } from '@angular/core'; interface User { name: string; age: number; } @Component({ selector: 'app-root', template: ` Update User Object (Immutable) ` }) export class AppComponent { currentUser: User = { name: 'Parent User', age: 25 }; updateUserObject() { // 更新 user 对象引用,触发 OnPush 组件的变更检测 this.currentUser = { ...this.currentUser, age: this.currentUser.age + 5 }; } } ``` 在这个例子中,`OnPushStrategyComponent` 使用 `ChangeDetectionStrategy.OnPush` 策略。 * **`updateAge()` 方法:** 通过展开运算符 `...` 创建一个新的 `user` 对象,并修改 `age` 属性。由于 `user` 对象的引用发生了变化,`OnPushStrategyComponent` 会检测到变化并更新视图。 * **`updateNameMutably()` 方法:** 直接修改 `user` 对象的 `name` 属性,**没有改变对象引用**。在这种情况下,`OnPushStrategyComponent` **不会** 检测到变化,视图可能不会更新 (除非其他事件触发了变更检测)。 * **父组件 `AppComponent` 的 `updateUserObject()` 方法:** 通过创建新的 `currentUser` 对象,并将其传递给 `OnPushStrategyComponent` 的 `[user]` 输入属性,触发 `OnPushStrategyComponent` 的变更检测。 **mermaid 图表:Default vs OnPush 策略** ```mermaid graph TD subgraph Default Strategy A[浏览器事件/异步操作/输入属性变化] --> B[触发变更检测周期]; B --> C[自顶向下遍历组件树]; C --> D{每个组件: 脏值检查}; D -- 数据变化 --> E[更新视图]; D -- 数据无变化 --> C; end subgraph OnPush Strategy F[输入属性引用变化 OR 组件/子组件事件处理函数] --> G[触发变更检测周期 (仅针对受影响组件及其子组件)]; G --> H[自顶向下遍历 **受影响组件子树**]; H --> I{每个组件: 脏值检查}; I -- 数据变化 --> J[更新视图]; I -- 数据无变化 --> H; end style Default Strategy fill:#f9f,stroke:#333,stroke-width:2px style OnPush Strategy fill:#ccf,stroke:#333,stroke-width:2px ``` **选择策略的原则:** * **`Default` 策略:** 适用于大多数应用场景,易于理解和使用。对于小型到中型应用,或者数据变化频繁的应用,`Default` 策略通常足够高效。 * **`OnPush` 策略:** 适用于大型、复杂的应用,尤其是组件树庞大且数据变化相对静态的应用。`OnPush` 策略可以显著提升性能,减少不必要的变更检测操作。但使用 `OnPush` 策略需要遵循**不可变数据模式**,并仔细管理组件的输入属性和事件处理。 ### 3.1.3 手动触发变更检测 在某些高级场景下,你可能需要手动控制变更检测的触发时机。Angular 提供了 `ChangeDetectorRef` 服务,允许你手动触发变更检测或进行更细粒度的控制。 **`ChangeDetectorRef` 服务:** 每个组件实例都有一个关联的 `ChangeDetectorRef` 实例,可以通过依赖注入获取: ```typescript import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; @Component({ selector: 'app-manual-change-detection', template: `Manual Change Detection: {{ message }}
Update Message (Manual DetectChanges) Update Message (No DetectChanges) Mark For Check Example (OnPush) `, changeDetection: ChangeDetectionStrategy.OnPush // 示例中使用 OnPush 策略 }) export class ManualChangeDetectionComponent { message = 'Initial Message'; constructor(private changeDetectorRef: ChangeDetectorRef) {} updateMessage() { this.message = 'Message updated manually at ' + new Date().toLocaleTimeString(); this.changeDetectorRef.detectChanges(); // 手动触发变更检测 } updateMessageWithoutDetectChanges() { this.message = 'Message updated without detectChanges at ' + new Date().toLocaleTimeString(); // 注意:由于 OnPush 策略,视图可能不会立即更新 } markForCheckExample() { // 在 OnPush 策略下,如果输入属性没有变化,但组件内部数据需要更新 // 可以使用 markForCheck() 标记组件及其祖先组件需要检查 this.message = 'Message updated with markForCheck at ' + new Date().toLocaleTimeString(); this.changeDetectorRef.markForCheck(); } } ``` **`ChangeDetectorRef` 的常用方法:** * **`detectChanges()`:** 手动触发当前组件及其子组件的变更检测周期。通常在需要强制更新视图,但默认变更检测机制没有触发的情况下使用。 * **`markForCheck()`:** 标记当前组件及其所有祖先组件为 "需要检查"。在 `OnPush` 策略下,当组件的输入属性没有变化,但组件内部数据需要更新时,可以使用 `markForCheck()` 确保组件在下一次变更检测周期中被检查到。 * **`detach()`:** 从变更检测树中分离当前组件。被分离的组件及其子组件将不再参与自动变更检测。适用于需要手动完全控制变更检测的极端情况,需要谨慎使用。 * **`reattach()`:** 将之前分离的组件重新连接到变更检测树中,恢复自动变更检测。 * **`checkNoChanges()`:** 手动执行变更检测,并检查是否发生了任何变化。如果在变更检测期间检测到任何变化,则会抛出错误。用于开发模式下的提前错误检测。 **使用 `ChangeDetectorRef` 的注意事项:** * **谨慎使用 `detectChanges()`:** 过度使用 `detectChanges()` 可能会降低应用性能,因为它会强制 Angular 进行额外的变更检测操作。尽量依赖 Angular 的自动变更检测机制。 * **`markForCheck()` 在 `OnPush` 策略下非常有用:** 用于优化 `OnPush` 组件的更新,确保在需要时进行检测,避免不必要的检测。 * **`detach()` 和 `reattach()` 属于高级用法:** 通常只有在非常特定的性能优化场景下才需要使用,例如在大型数据网格或图表组件中。 ### 3.1.4 优化变更检测性能 高效的变更检测是构建高性能 Angular 应用的关键。以下是一些优化变更检测性能的常用技巧: 1. **使用 `OnPush` 策略:** 尽可能使用 `OnPush` 策略,尤其是在性能敏感的组件和大型应用中。 2. **不可变数据 (Immutable Data):** 结合 `OnPush` 策略,使用不可变数据模式。避免直接修改对象或数组,而是创建新的对象或数组副本进行修改。这可以更精确地触发 `OnPush` 组件的变更检测。 3. **`trackBy` 函数在 `*ngFor` 中:** 在 `*ngFor` 循环中使用 `trackBy` 函数,可以帮助 Angular 更高效地更新列表。`trackBy` 函数允许 Angular 识别列表中哪些项发生了变化 (添加、删除、移动),从而避免不必要的 DOM 元素的重新渲染。 ```typescript import { Component } from '@angular/core'; interface Item { id: number; name: string; } @Component({ selector: 'app-track-by-example', template: `