Angular 入门基础(第十三篇) 变更检测和运行时优化

206 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第13天。点击查看活动详情

变更检测是 Angular 检查你的应用程序状态是否已更改以及是否需要更新任何 DOM 的过程。大体而言,Angular 会从上到下遍历你的组件,寻找更改。Angular 会定期运行其变更检测机制,以便对数据模型的更改反映在应用程序的视图中。变更检测可以手动触发,也可以通过异步事件(比如用户交互或 XHR 自动完成)来触发。

变更检测具有一种高度优化的性能,但如果应用程序过于频繁的运行它,它仍然会导致变慢。

解决区域(Zone)污染

Zone.js是一种信号机制,Angular 用它来检测应用程序状态何时可能已更改。它捕获异步操作,比如 setTimeout、网络请求和事件侦听器。Angular 会根据来自 Zone.js 的信号安排变更检测

在某些情况下,某些已安排的任务或微任务不会对数据模型进行任何更改,这使得运行变更检测变得不必要。常见的例子是:

  • requestAnimationFrame 、 setTimeout 或 setInterval

  • 第三方库的任务或微任务调度

识别不必要的变更检测调用

你可以用 Angular DevTools 检测不必要的变更检测调用。它们通常在分析器的时间线中显示为连续的条形,其源为 setTimeout、setInterval、requestAnimationFrame 或事件处理程序。当你在应用程序中对这些 API 的调用有限时,变更检测调用通常是由第三方库引起的。

有一系列由与元素关联的事件处理程序触发的变更检测调用。这是使用第三方非原生 Angular 组件时的常见挑战,这些组件不会更改 NgZone 的默认行为。

在 NgZone 之外运行任务

在这种情况下,我们可以指示 Angular 避免使用NgZone为给定代码段调度的任务调用变更检测。

import { Component, NgZone, OnInit } from '@angular/core';
@Component(...)
class AppComponent implements OnInit {
  constructor(private ngZone: NgZone) {}
  ngOnInit() {
    this.ngZone.runOutsideAngular(() => setInterval(pollForUpdates), 500);
  }
}

上面的代码段告诉 Angular,它应该在 Angular Zone 之外执行 setInterval 调用,并在 pollForUpdates 运行之后跳过运行变更检测。

第三方库通常会触发不必要的变更检测周期,因为它们在创作时并没有考虑到 Zone.js。通过调用 Angular 区域外的库 API 来避免这些额外的周期:

import { Component, NgZone, OnInit } from '@angular/core';
import * as Plotly from 'plotly.js-dist-min';

@Component(...)
class AppComponent implements OnInit {
  constructor(private ngZone: NgZone) {}
  ngOnInit() {
    this.zone.runOutsideAngular(() => {
      Plotly.newPlot('chart', data);
    });
  }
}

在 runOutsideAngular 中运行 Plotly.newPlot('chart', data); 会告诉框架它不应该在执行此初始化逻辑安排的这些任务之后执行变更检测。

比如,如果 Plotly.newPlot('chart', data) 将事件侦听器添加到 DOM 元素,则 Angular 将不会在执行其处理程序之后执行变更检测。

慢速计算

在每个变更检测周期上,Angular 都会同步进行:

  • 除非另有指定,否则会根据每个组件的检测策略估算所有组件中的所有模板表达式

  • 执行 ngDoCheck 、 ngAfterContentChecked 、 ngAfterViewChecked 和 ngOnChanges 生命周期钩子。模板中的单个慢速计算或生命周期钩子可能会减慢整个变更检测过程,因为 Angular 会按顺序运行计算。

优化慢速计算

有几种技术可以消除慢速计算:

  • 优化底层算法。这是推荐的方法;如果你可以加快导致问题的算法的速度,则可以加快整个变更检测机制。

  • 使用纯管道进行缓存。你可以将繁重的计算移动到纯管道中。与 Angular 上一次调用它时相比,只有在检测到其输入发生更改时,Angular 才会重新估算纯管道。

  • 使用记忆化(memoization)。记忆化是一种与纯管道类似的技术,不同之处在于纯管道仅保留计算中的最后一个结果,而记忆化可以存储多个结果。

  • 避免在生命周期钩子中触发重绘/回流。某些操作会导致浏览器同步重新计算页面布局或重新渲染它。由于回流和重绘通常很慢,因此我们希望避免在每个变更检测周期中都执行它们。

纯管道和记忆化有不同的权衡。与记忆化相比,纯管道是 Angular 的内置概念,记忆化是一种用于缓存函数结果的通用软件工程实践。如果你使用不同的参数频繁调用繁重的计算,则记忆化的内存开销可能会很大。

跳过组件子树

默认情况下,JavaScript 会使用你可以从多个不同组件引用的可变数据结构。Angular 会在你的整个组件树上运行变更检测,以确保数据结构的最新状态反映在 DOM 中。

对于大多数应用程序,变更检测都足够快。但是,当应用程序有特别大的组件树时,在整个应用程序中运行变更检测可能会导致性能问题。你可以通过将变更检测配置为仅在组件树的子集上运行来解决这个问题。

如果你确信应用程序的一部分不受状态更改的影响,可以用 OnPush 跳过整个组件子树中的变更检测。

使用 OnPush

OnPush 变更检测会指示 Angular 仅在以下情况下为组件子树自动运行变更检测:

子树的根组件接收到作为模板绑定的结果的新输入。Angular 将输入的当前值和过去值使用 == 进行比较

Angular 处理使用了 OnPush 变更检测策略的组件中的事件时

你可以在 @Component 装饰器中将组件的变更检测策略设置为 OnPush :

import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent {}

边缘情况

修改 TypeScript 代码中的输入属性。当你使用 @ViewChild 或 @ContentChild 等 API 来获取对 TypeScript 中组件的引用并手动修改 @Input 属性时,Angular 将不会自动为 OnPush 组件运行变更检测。如果你需要 Angular 运行变更检测,你可以在你的组件中注入 ChangeDetectorRef 并调用 changeDetectorRef.markForCheck() 来告诉 Angular 为其安排一次变更检测。

修改对象引用。如果输入接收到可变对象作为值,并且你修改了对象内容但引用没变,则 Angular 将不会调用变更检测。这是预期的行为,因为输入的前一个值和当前值都指向了同一个引用。