角度变化检测和 OnPush 策略

已发表: 2022-03-11

你已经开始在你喜欢的所有项目中使用 Angular。 您知道 Angular 必须提供什么,以及如何利用它来构建出色的 Web 应用程序。 但是,有一些关于 Angular 的东西,了解它们可以让你更好地在项目中使用 Angular。

数据流是 Angular 几乎所有事物的核心,变更检测是值得了解的,因为它可以帮助您更轻松地跟踪错误,并让您有机会在处理复杂数据集时进一步优化您的应用程序。

角度变化检测和 OnPush 策略

在本文中,您将了解 Angular 如何检测其数据结构中的更改,以及如何使它们不可变以充分利用 Angular 的更改检测策略。

Angular中的变化检测

当您更改任何模型时,Angular 会检测到更改并立即更新视图。 这是 Angular 中的变更检测。 这种机制的目的是确保底层视图始终与其对应的模型同步。 Angular 的这一核心特性是框架的成功之处,也是 Angular 成为开发现代 Web 应用程序的绝佳选择的部分原因。

Angular 中的模型可能会因以下任何一种情况而发生变化:

  • DOM 事件(单击、悬停等)

  • AJAX 请求

  • 计时器 (setTimer(), setInterval())

变化检测器

所有 Angular 应用程序都由一个分层的组件树组成。 在运行时,Angular 为树中的每个组件创建一个单独的更改检测器类,然后最终形成一个类似于组件层次结构树的更改检测器层次结构。

每当触发变更检测时,Angular 就会沿着这棵变更检测器树向下走,以确定它们中是否有任何报告了变更。

每次检测到的变化都会执行一次变化检测周期,并从根变化检测器开始,并以顺序方式一直向下。 这种顺序设计选择很好,因为它以可预测的方式更新模型,因为我们知道组件数据只能来自其父级。

更改检测器层次结构

更改检测器提供了一种跟踪组件先前和当前状态及其结构的方法,以便向 Angular 报告更改。

如果 Angular 从变更检测器获取报告,它会指示相应的组件重新渲染并相应地更新 DOM。

变更检测策略

值与引用类型

为了理解什么是变更检测策略以及它为什么起作用,我们必须首先了解 JavaScript 中值类型和引用类型之间的区别。 如果您已经熟悉它的工作原理,则可以跳过本节。

首先,让我们回顾一下值类型和引用类型及其分类。

值类型

  • 布尔值

  • 空值

  • 不明确的

  • 数字

  • 细绳

为简单起见,可以想象这些类型只是将它们的值存储在堆栈内存中(这在技术上是不正确的,但对于本文来说已经足够了)。 例如,请参见下图中的堆栈内存及其值。

堆栈内存

参考类型

  • 数组

  • 对象

  • 职能

这些类型有点复杂,因为它们在堆栈内存上存储一个引用,该引用指向它们在堆内存上的实际值。 您可以在下面的示例图像中看到堆栈内存和堆内存如何协同工作。 我们看到栈内存引用了堆内存中引用类型的实际值。

堆栈内存和堆内存

值类型和引用类型之间的重要区别在于,为了读取值类型的值,我们只需要查询堆栈内存,但是为了读取引用类型的值,我们需要先查询堆栈内存以获取引用,然后使用该引用查询堆内存以定位引用类型的值。

默认策略

正如我们之前所说,Angular 会监控模型的变化,以确保它捕捉到所有的变化。 它将检查整个应用程序模型的先前状态和当前状态之间的任何差异。

Angular 在默认变更检测策略中提出的问题是:模型中的任何值是否发生了变化? 但是对于引用类型,我们可以实施策略,以便我们可以提出更好的问题。 这就是 OnPush 变更检测策略的用武之地。

OnPush 策略

OnPush 策略背后的主要思想体现在以下认识中:如果我们将引用类型视为不可变对象,我们可以更快地检测到值是否发生了变化。 当引用类型是不可变的时,这意味着每次更新时,堆栈内存上的引用都必须更改。 现在我们可以简单地检查:引用类型的引用(在堆栈中)是否改变了? 如果是,则仅检查所有值(在堆上)。 如果这令人困惑,请参考前面的堆栈图。

OnPush 策略基本上问两个问题而不是一个问题。 引用类型的引用是否改变了? 如果是,那么堆内存中的值是否已更改?

例如,假设我们有一个包含 30 个元素的不可变数组,并且我们想知道是否有任何更改。 我们知道,为了对不可变数组进行任何更新,它的引用(在堆栈上)必须改变。 这意味着我们可以首先检查对数组的引用是否有任何不同,这可能会使我们免于再进行 30 次检查(在堆中)以确定哪个元素不同。 这称为 OnPush 策略。

那么,您可能会问,将引用类型视为不可变意味着什么? 这意味着我们从不设置引用类型的属性,而是一起重新分配值。 见下文:

将对象视为可变对象:

 static mutable() { var before = {foo: "bar"}; var current = before; current.foo = "hello"; console.log(before === current); // => true }

将对象视为不可变的:

 static mutable() { var before = {foo: "bar"}; var current = before; current = {foo "hello"}; console.log(before === current); // => false }

请注意,在上面的示例中,我们按照惯例将引用类型“视为”不可变,因此最终我们仍在使用可变对象,但只是“假装”它们是不可变的。

那么如何为组件实现 OnPush 策略呢? 您需要做的就是在 @Component 注释中添加changeDetection参数。

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

不可变的.js

如果决定在 Angular 组件上使用 OnPush 策略,那么强制执行不变性是一个好主意。 这就是 Immutable.js 的用武之地。

Immutable.js 是 Facebook 创建的用于 JavaScript 不变性的库。 它们有许多不可变的数据结构,例如 List、Map 和 Stack。 出于本文的目的,将对列表和地图进行说明。 如需更多参考,请在此处查看官方文档。

为了将 Immutable.js 添加到您的项目中,请确保进入您的终端并运行:

 $ npm install immutable --save

还要确保在您使用它的组件中从 Immutable.js 导入您正在使用的数据结构。

 import {Map, List} from 'immutable';

这就是 Immutable.js Map 的使用方式:

 var foobar = {foo: "bar"}; var immutableFoobar = Map(foobar); console.log(immutableFooter.get("foo")); // => bar

并且,可以使用数组:

 var helloWorld = ["Hello", "World!"]; var immutableHelloWorld = List(helloWorld); console.log(immutableHelloWorld.first()); // => Hello console.log(immutableHelloWorld.last()); // => World! helloWorld.push("Hello Mars!"); console.log(immutableHelloWorld.last()); // => Hello Mars!

使用 Immutable.js 的缺点

使用 Immutable.js 有几个主要的可争论的缺点。

您可能已经注意到,使用它的 API 有点麻烦,传统的 JavaScript 开发人员可能不喜欢这样。 一个更严重的问题与无法为您的数据模型实现接口有关,因为 Immutable.js 不支持接口。

包起来

你可能会问为什么 OnPush 策略不是 Angular 的默认策略。 我认为这是因为 Angular 不想强迫 JavaScript 开发人员使用不可变对象。 但是,这并不意味着您被禁止使用它。

如果这是您想在下一个 Web 项目中利用的东西,那么您现在知道 Angular 可以轻松地切换到不同的变更检测策略。