深入理解Angular的变更检测

概述

这是一篇偏理论的文章,Angular的变更检测是这个框架的灵魂,如果我们深入理解这里边的内容,对于优化程序,解决性能问题,以及对Angular的深入理解都有很好的帮助,本文涉及到的知识点主要有: ,, 。

哪些行为会引起Angular的变更检测?

  • DOM事件: click、submit...
  • XHR: 从服务端获取数据
  • Timers: setTimeout、setInterval

如上的所示,我们可以发现,我们的程序状态可能发生变化,这就是Angular视图需要更新,需要进行变更检测的时候。

谁来通知Angular进行变更检测呢?

Angular有自己的zone,就是我们常见的ngZone,Angular 源码中有一个东西叫做 ApplicationRef,它监听 NgZone 的 onTurnDone 事件。只要这个事件发生,它就执行 函数,这个函数执行变更检查,DOM就会更新,看下图示例,其实zone.js就是。

屏幕快照 2021-08-03 下午1.22.21.png

变更检测如何执行呢?

在 Angular 中,每个组件都有自己的变更检测器(change detector)。因为我们可以单独控制每个组件的变更检查何时发生以及如何执行,既然每个组件都有自己的 ,并且一个 Angular 应用包含着一个组件树,那么逻辑上我们也有一个(change detector tree)。这棵树也可以被看成是一个,该有向图的数据总是从顶端流向底端(单向数据流)。
其实每个视图(组件)都有一个状态,也是非常重要的角色,因为根据它的值(FirstCheck、 ChecksEnabled、Errored、Destoryed),Angular决定是否对视图及其所有子视图运行或跳过脏值检测。如果ChecksEnabled是false或者视图是Errored或者Destroyed的状态,变更检测将会跳过这个视图以及它的子视图。

关于Angular的变更检测,是有两种策略,一种是,另一个是策略,接下来我们具体阐述一下~

Angular的Default策略

此默认策略,就是我们常提到的脏检查,它是只要有变化,就从根组件所有子组件进行检查(深度优先遍历)

屏幕快照 2021-08-03 下午2.08.18.png

Angular的onPush策略

如上默认的变更检测,Angular必须每次都检测所有组件,但是如果我们可以让Angular仅对应用中发生改变的状态进行变更检测,那样会更高效,一般我们会从下边↓两个方向上着手,实现更加高效且聪明的变更检测。

  • 使用不可变对象(减少对象属性的对比)

不可变对象保证了这个对象是不可变的,这意味着如果我们使用者不可变对象,同时试图改变这个对象,那我们总是会得到一个新的引用,如果我们的需求就是只要地址更改才进行变更检测,这样我们就不需要对比对象中的属性了

  • 减少检测组件个数

如果我们在组件中使用了onPush策略,变更检测会在第一次检查后被禁用,那么只有当输入属性(@Input)没有发生改变的时候,Angular 会跳过整棵子树的变更检查。此变更检测,只会在当前组件以及其子组件中生效,其相邻兄弟组件不会受影响。

屏幕快照 2021-08-02 下午7.41.09.png

使用方法如下:

@Component({
  selector: 'app-test',
  templateUrl: './test.component.html',
  styleUrls: ['./test.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush, // 使用Angular的onPush策略
})

在onPush策略下,那些情况可以引起组件的变更检测?

  1. 只有当发生变化组件才进行变化检测。
  2. 有,组件才进行变化检测,其他异步事件更换数据都不会触发变更检测。(而且即使这个事件是个空的函数或者操作的不是@Input属性,也仍然会导致变更检测的发生)
  3. 变更检测

如何手动触发变更检测呢?

通过引用,可以手动去操作变化检测。我们可以在组件中的通过依赖注入的方式来获取该对象:

constructor(
   private changeRef: ChangeDetectorRef
){}

此变更检测对象提供了如下所示的方法

abstract class ChangeDetectorRef {

abstract markForCheck() : void 

abstract detectChanges() : void

abstract detach() : void

abstract reattach() : void 

abstract checkNoChanges() : void

}
markForCheck()

,那么变化检测不会再次执行,除非手动调用该方法, 在程序中使用this.changeRef.markForCheck()变化检测,如要想要执行多次多次,则需要多次的运行这句话。具体的执行流程如下:

屏幕快照 2021-08-02 下午7.40.02.png
detectChanges()

首先他的使用与是否使用了onPush无关,他是只在当前视图和它的子视图,应用场景:明确知道有数据的更新,需要Angular执行变更检测的时候使用。

屏幕快照 2021-08-03 上午11.21.25.png
detach()

首先他的使用与是否使用了onPush无关,他是从变化检测树中,该组件的变化检测器将不再执行变化检测,同时其子组件也不会执行检测,除非手动调用 reattach() 方法

reattach()

首先他的使用与是否使用了onPush无关,他是,使得该组件及其子组件都能执行变化检测,但是如果当前的组件的父组件(脏检查)的话,它将。

使用async pipe

当父组件的输入属性是用observable,那么除了使用this.changeRef.markForCheck()来进行变更检测,我们还可以在子组件中使用async pipe, 发出一个新值时,异步管道会将组件标记为要检查更改(其实也是调用了 this.changeRef.markForCheck())

{{addCount | async}}

ChangeDetectorRef中易混方法对比

markForCheck()与detectChanges()的对比?
  • markForCheck():

不会真正触发变成检测,而是将根组件标记为需要检查
② 基本上只在Angular的检测策略被设置成OnPush的时候会被使用到
③ 他的范围是从当前组件向上扩展到根组件

  • detectChanges():

会真正触发Angular的change detection
② 当数据更新,但是视图没有更新的时候,会用到这个方法,是用于手动触发变更检测的方法之一,当然微任务的定时器啥的也会触发变更检测
③ 更改检测将针对当前组件以及所有子组件
④ 可以与detach使用,完成局部的更新

reattach()与detectChanges()的对比?
  • detectChanges()

只会让变成检测手动执行一次

  • reattach()

是将视图加回去,变更检测会根据业务中数据的变化一直进行变化,相当于完全还原最原本的变更检测

通过一个示例更好的理解这两个方法的区别:


屏幕快照 2021-08-03 下午1.29.16.png

示例解析:

当num是5的时候,因为使用了detach(),则此视图不在进行变更检测,当我们调用RecoveryDetection(),执行reattach()的时候,页面的数据恢复递增显示,如果我们在这个方法中执行的是detectChanges(),页面的数据只会增加一次,就不在更新变化了~

总结

如果大家想真正了解Angular的变更检测,一定要动手写一写相关的demo去验证自己的猜想,否则就像之前的我一样,看了很多理论,还是不能够了解,而且还很容易弄混ChangeDetectorRef的属性~ 希望大家看到这篇文章可以更加清晰点吧~

你可能感兴趣的:(深入理解Angular的变更检测)