[译]关于Angular脏值检查你应该知道的最新指南

原文javascript

Angular脏值检查

image

本文提供了您须要了解的有关变动检测的全部必要信息。经过使用本文构建的演示项目来解释angular 的变动检测机制。

Angular的变动检测是该框架的核心机制,但(至少以个人经验)很难理解。更不幸的是,官方网站上没有关于此主题的官方指南。html

What Is Change Detection

Angular的两个主要目标是可预测和高效。框架须要经过组合状态和模板来在UI上复制应用程序的状态:前端

image

若是状态发生任何更改,也必须更新DOM。将HTML与咱们的数据同步的机制称为“更改检测”。每一个前端框架都使用其实现,例如React使用虚拟DOM,Angular使用更改检测等等。我能够推荐文章“ JavaScript框架中的更改及其检测”,该文章很好地概述了此主题。java

更改检测:数据更改后更新DOM的过程

做为开发人员,大多数时候咱们不须要关心变动检测,除非咱们须要优化应用程序的性能。若是处理不当,更改检测会下降大型应用程序的性能。npm

How Change Detection Works 变动检测是如何工做的

变动检测周期能够分为两个部分:bootstrap

  • 开发人员更新应用程序模型
  • Angular经过从新渲染来同步DOM中的更新模型

让咱们更详细地看一下这个过程:后端

  1. 开发人员更新数据模型,例如经过更新组件绑定
  2. angular 检测变化
  3. 变动检测从上到下检查组件树中的每一个组件,以查看相应的模型是否已更改
  4. 若是有新值,它将更新组件的视图(DOM)

    如下GIF以简化的方式演示了此过程:数组

    image

该图显示了Angular组件树及其在应用程序引导过程当中为每一个组件建立的更改检测器(CD)。该检测器将当前值与属性的先前值进行比较。若是该值已更改,它将==isChanged==设置为==true==。检查框架代码中的实现,这只是与NaN的特殊处理进行的===比较。promise

Zone.js

通常状况下,zone能够跟踪并拦截任何异步任务。浏览器

Zone 一般具备如下阶段:

  • 开始稳定
  • 若是任务在区域中运行,它将变得不稳定,
  • 若是任务完成,它将再次变得稳定

Angular在启动时修补了几个低级浏览器API,以便可以检测到应用程序中的更改。这是使用zone.js完成的,该区域修补了EventEmitter,DOM事件侦听器,XMLHttpRequest,Node.js中的fs API等API。

简而言之,若是发生如下事件之一,则框架将触发更改检测

  • 任何浏览器事件(单击,键入等)
  • setInterval() and setTimeout()
  • HTTP 请求

Angular使用其称为NgZone的区域。仅存在一个NgZone,而且仅针对此区域中触发的异步操做触发更改检测。

Performance 性能

默认状况下,若是模板值已更改,则“Angular Change Detection ”将从上至下检查全部组件。

Angular对每一个组件执行更改检测的速度很是快,由于它可使用内联缓存在毫秒内执行数千次检查,内联缓存可生成VM优化代码。

若是您想对此主题有更深刻的说明,建议您观看Victor Savkin关于“重塑变化检测”的演讲。

尽管Angular在后台进行了大量优化,可是在大型应用程序上性能仍然会降低。在下一章中,您将学习如何经过使用不一样的变动检测策略来主动提升Angular性能。

Change Detection Strategies 变动检测策略

Angular提供了两种策略来运行更改检测:

  • Default
  • OnPush

让咱们看一下每种变化检测策略。

Default Change Detection Strategy

默认状况下,Angular使用ChangeDetectionStrategy.Default更改检测策略。每当事件触发更改检测(例如用户事件,计时器,XHR,promise等)时,此默认策略都会从上到下检查组件树中的每一个组件。这种不对组件的依赖项作任何假设的保守检查方法称为脏检查。它可能会对包含许多组件的大型应用程序的性能产生负面影响。

image

OnPush Change Detection Strategy

经过将changeDetection属性添加到组件装饰器元数据中,咱们能够切换到ChangeDetectionStrategy.OnPush更改检测策略:

@Component({
    selector: 'hero-card',
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: ...
})
export class HeroCard {
    ...
}

这种更改检测策略能够跳过对此组件及其全部子组件的没必要要检查。
下一个GIF演示了使用OnPush更改检测策略跳过组件树的各个部分:
image

使用此策略,Angular知道仅在如下状况下才须要更新组件:

  • 输入属性已更改, 标记为@Input() 的属性;
  • 该组件或其子组件之一触发事件处理程序
  • 手动触发变化检测
  • 经过异步管道连接到模板的可观察对象发出新值, 如 data | async

    让咱们仔细看看这些事件类型。

    Input Reference Changes

    在默认的更改检测策略中,每当@Input()数据被更改或修改时,Angular将运行更改检测器。使用OnPush策略,仅当新引用做为@Input()值传递时,才会触发更改检测器。

    JavaScript中的全部内容都是按引用传递的,可是全部基元都是不可变的,而且它们的文字表示均指向相同的基元实例/引用。修改对象属性或数组条目不会建立新引用,所以不会触发OnPush组件上的更改检测。要触发变动检测器,您须要传递一个新的对象或数组引用。

您可使用简单DEMO测试此行为:

  1. 使用ChangeDetectionStrategy.Default修改HeroCardComponent的 age
  2. 验证带有ChangeDetectionStrategy.OnPush的HeroCardOnPushComponent不能反映更改的age(经过组件周围的红色边框显示)
  3. 在“修改英雄”面板中单击“建立新对象引用”
  4. 验证是否经过更改检测检查了具备ChangeDetectionStrategy.OnPush的HeroCardOnPushComponent

image

为防止更改检测错误,在全部地方仅使用不可变的对象和列表使用OnPush更改检测来构建应用程序可能会颇有用。不可变对象只能经过建立新的对象引用来修改,所以咱们能够保证:

  • 每次更改都会触发OnPush更改检测
  • 咱们不要忘了建立一个新的对象引用,不然可能致使错误;

Immutable.js是一个不错的选择,该库为对象(地图)和列表(列表)提供了持久不变的数据结构。经过npm安装库提供了类型定义,以便咱们能够在IDE中利用类型泛型,错误检测和自动完成功能。

Event Handler Is Triggered

若是OnPush组件或其子组件之一触发事件处理程序(例如单击按钮),则将触发更改检测(针对组件树中的全部组件)。

请注意,如下操做不会触发使用OnPush更改检测策略的更改检测:

  • setTimeOut
  • setInterval
  • Promise.resolve().then(), (of course, the same for Promise.reject().then())
  • this.http.get('...').subscribe() (in general, any RxJS observable subscription)

    You can test this behavior using the simple demo:

    1. Click on "Change Age" button in HeroCardOnPushComponent which uses ChangeDetectionStrategy.OnPush
    2. 验证触发了变动检测并检查全部组件

image

Trigger Change Detection Manually 手动触发变动检测

存在三种手动触发更改检测的方法:

  • ChangeDetectorRef的detectChanges()经过牢记更改检测策略在此视图及其子级上运行更改检测。它能够与detach()结合使用以实现本地更改检测检查。
  • ApplicationRef.tick()经过遵照组件的更改检测策略来触发整个应用程序的更改检测
  • ChangeDetectorRef上的markForCheck()不会触发更改检测,但会将全部OnPush祖先标记为要检查一次,做为当前或下一个更改检测周期的一部分。即便已标记的组件使用OnPush策略,它也将运行更改检测。
手动运行变动检测不是黑客,但您只能在合理的状况下使用它,

下图以可视表示形式显示了不一样的ChangeDetectorRef方法:

image

您能够在DEMO中使用“ DC”(detectChanges())和“ MFC”(markForCheck())按钮来测试其中一些操做。

### Async Pipe

内置的AsyncPipe订阅一个observable并返回它发出的最新值。

每次发出新值时,AsyncPipe内部都会调用markForCheck,请参见其源代码:

private _updateLatestValue(async: any, value: Object): void {
  if (async === this._obj) {
    this._latestValue = value;
    this._ref.markForCheck();
  }
}

如图所示,AsyncPipe使用OnPush更改检测策略自动运行。所以,建议尽量使用它,以便之后执行从默认更改检测策略到OnPush的切换。

您能够在异步演示中看到这种行为。

image

第一个组件经过AsyncPipe将可观察对象直接绑定到模板

<mat-card-title>{{ (hero$ | async).name }}</mat-card-title>
hero$: Observable<Hero>;

  ngOnInit(): void {
    this.hero$ = interval(1000).pipe(
        startWith(createHero()),
        map(() => createHero())
      );
  }

而第二个组件订阅可观察对象并更新数据绑定值:

<mat-card-title>{{ hero.name }}</mat-card-title>
hero: Hero = createHero();

  ngOnInit(): void {
    interval(1000)
      .pipe(map(() => createHero()))
        .subscribe(() => {
          this.hero = createHero();
          console.log(
            'HeroCardAsyncPipeComponent new hero without AsyncPipe: ',
            this.hero
          );
        });
  }

如您所见,没有AsyncPipe的实现不会触发更改检测,所以咱们须要为可观察对象发出的每一个新事件手动调用detectChanges()

避免变化检测循环和ExpressionChangedAfterCheckedError

Angular包括一种检测变化检测循环的机制。在开发模式下,框架运行两次更改检测,以检查自第一次运行以来该值是否已更改。在生产模式下,更改检测仅运行一次便可得到更好的性能。

我在ExpressionChangedAfterCheckedError演示中强加了该错误,若是打开浏览器控制台,则能够看到它:

image

在此演示中,我经过更新ngAfterViewInit生命周期挂钩中的hero属性来强制执行错误:

ngAfterViewInit(): void {
    this.hero.name = 'Another name which triggers ExpressionChangedAfterItHasBeenCheckedError';
  }

要了解为何这会致使错误,咱们须要查看更改检测运行期间的不一样步骤:

image

如咱们所见,在呈现了当前视图的DOM更新以后,将调用AfterViewInit生命周期挂钩。若是咱们更改此挂钩中的值,则它将在第二次更改检测运行中具备不一样的值(如上所述,这是在开发模式下自动触发的),所以Angular将抛出ExpressionChangedAfterCheckedError。

我能够强烈推荐Max Koretskyi撰写的有关Angular中的更改检测所需的全部知识,它详细探讨了著名的ExpressionChangedAfterCheckedError的基础实现和用例

没有更改检测的运行代码

能够在NgZone外部运行某些代码块,以便它不会触发更改检测。

constructor(private ngZone: NgZone) {}

  runWithoutChangeDetection() {
    this.ngZone.runOutsideAngular(() => {
      // the following setTimeout will not trigger change detection
      setTimeout(() => doStuff(), 1000);
    });
  }

这个简单的演示提供了一个按钮来触发Angular区域以外的动做:

image

您应该看到该操做已记录在控制台中,可是HeroCard组件未选中,这意味着它们的边框不会变成红色。

此机制对于由量角器运行的E2E测试颇有用,特别是若是您在测试中使用browser.waitForAngular。将每一个命令发送到浏览器后,量角器将等待,直到区域变得稳定为止。若是使用setInterval,则区域将永远不会变得稳定,而且测试可能会超时。

RxJS可观察对象可能发生相同的问题,可是您须要按照Zone.js对非标准API的支持中所述,将修补版本添加到polyfill.ts中:

import 'zone.js/dist/zone';  // Included with Angular CLI.
import 'zone.js/dist/zone-patch-rxjs'; // Import RxJS patch to make sure RxJS runs in the correct zone

若是没有此修补程序,则能够在ngZone.runOutsideAngular内部运行可观察的代码,但仍能够做为任务在NgZone内部运行

停用变动检测

在特殊的使用状况下,有必要停用更改检测。例如,若是您使用WebSocket将大量数据从后端推送到前端,则相应的前端组件仅应每10秒更新一次。在这种状况下,咱们能够经过调用detach()来停用更改检测,并使用detectChanges()手动触发它:

constructor(private ref: ChangeDetectorRef) {
    ref.detach(); // deactivate change detection
    setInterval(() => {
      this.ref.detectChanges(); // manually trigger change detection
    }, 10 * 1000);
  }

在Angular应用程序的引导过程当中,也能够彻底停用Zone.js。这意味着自动更改检测功能已彻底停用,咱们须要手动触发用户界面更改,例如经过调用ChangeDetectorRef.detectChanges()。

首先,咱们须要注释掉从polyfills.ts导入的Zone.js:

import 'zone.js/dist/zone';  // Included with Angular CLI.

接下来,咱们须要在main.ts中传递noop区域:

platformBrowserDynamic().bootstrapModule(AppModule, {
      ngZone: 'noop';
}).catch(err => console.log(err));

有关停用Zone.js的更多详细信息,请参见文章没有Zone.Js的Angular Elements

Ivy

从Angular 9开始,Angular默认使用Ivy,它是Angular的下一代编译和渲染管道。

Ivy仍然以正确的顺序处理全部框架生命周期挂钩,以便更改检测像之前同样工做。所以,您仍将在应用程序中看到相同的ExpressionChangedAfterCheckedError。

Max Koretskyi在文章中写道

如您所见,全部熟悉的操做仍在这里。可是操做顺序彷佛已经改变。例如,如今看来Angular首先检查子组件,而后才检查嵌入式视图。因为目前没有编译器能够生成适合于检验个人假设的输出,所以我不肯定。

您能够在此博文末尾的“推荐文章”部分中找到另外两个与Ivy相关的有趣文章。

最后

Angular Change Detection是一种强大的框架机制,可确保咱们的UI以可预测和高效的方式表示咱们的数据。能够确定地说,更改检测仅适用于大多数应用程序,尤为是当它们不包含50多个组件时。

做为开发人员,您一般须要深刻探讨此主题,缘由有两个:

  • 您收到一个ExpressionChangedAfterCheckedError并须要解决它
  • 您须要提升应用程序性能

我但愿本文能够帮助您更好地了解Angular的变动检测。随意使用个人演示项目来试用不一样的变动检测策略。

推荐文章