概述
这是一篇偏理论的文章,Angular的变更检测是这个框架的灵魂,如果我们深入理解这里边的内容,对于优化程序,解决性能问题,以及对Angular的深入理解都有很好的帮助,本文涉及到的知识点主要有: ,, 。
哪些行为会引起Angular的变更检测?
- DOM事件: click、submit...
- XHR: 从服务端获取数据
- Timers: setTimeout、setInterval
如上的所示,我们可以发现,我们的程序状态可能发生变化,这就是Angular视图需要更新,需要进行变更检测的时候。
谁来通知Angular进行变更检测呢?
Angular有自己的zone,就是我们常见的ngZone,Angular 源码中有一个东西叫做 ApplicationRef,它监听 NgZone 的 onTurnDone 事件。只要这个事件发生,它就执行 函数,这个函数执行变更检查,DOM就会更新,看下图示例,其实zone.js就是。
变更检测如何执行呢?
在 Angular 中,每个组件都有自己的变更检测器(change detector)。因为我们可以单独控制每个组件的变更检查何时发生以及如何执行,既然每个组件都有自己的 ,并且一个 Angular 应用包含着一个组件树,那么逻辑上我们也有一个(change detector tree)。这棵树也可以被看成是一个,该有向图的数据总是从顶端流向底端(单向数据流)。
其实每个视图(组件)都有一个状态,也是非常重要的角色,因为根据它的值(FirstCheck、 ChecksEnabled、Errored、Destoryed),Angular决定是否对视图及其所有子视图运行或跳过脏值检测。如果ChecksEnabled是false或者视图是Errored或者Destroyed的状态,变更检测将会跳过这个视图以及它的子视图。
关于Angular的变更检测,是有两种策略,一种是,另一个是策略,接下来我们具体阐述一下~
Angular的Default策略
此默认策略,就是我们常提到的脏检查,它是只要有变化,就从根组件
到所有子组件
进行检查(深度优先遍历)
Angular的onPush策略
如上默认的变更检测,Angular必须每次都检测所有组件,但是如果我们可以让Angular仅对应用中发生改变的状态进行变更检测,那样会更高效,一般我们会从下边↓两个方向上着手,实现更加高效且聪明的变更检测。
- 使用不可变对象(减少对象属性的对比)
不可变对象保证了这个对象是不可变的,这意味着如果我们使用者不可变对象,同时试图改变这个对象,那我们总是会得到一个新的引用,如果我们的需求就是只要地址更改才进行变更检测,这样我们就不需要对比对象中的属性了
- 减少检测组件个数
如果我们在组件中使用了onPush策略,
变更检测会在第一次检查后被禁用
,那么只有当输入属性(@Input)没有发生改变的时候,Angular 会跳过整棵子树的变更检查。此变更检测,只会在当前组件
以及其子组件
中生效,其相邻兄弟组件不会受影响。
使用方法如下:
@Component({
selector: 'app-test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, // 使用Angular的onPush策略
})
在onPush策略下,那些情况可以引起组件的变更检测?
- 只有当发生变化组件才进行变化检测。
- 有,组件才进行变化检测,其他异步事件更换数据都不会触发变更检测。(而且即使这个事件是个空的函数或者操作的不是@Input属性,也仍然会导致变更检测的发生)
- 变更检测
如何手动触发变更检测呢?
通过引用,可以手动去操作变化检测。我们可以在组件中的通过依赖注入的方式来获取该对象:
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()变化检测,如要想要执行多次多次,则需要多次的运行这句话。具体的执行流程如下:
detectChanges()
首先他的使用与是否使用了onPush无关,他是只在当前视图和它的子视图,应用场景:明确知道有数据的更新,需要Angular执行变更检测的时候使用。
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()
是将视图加回去,变更检测会根据业务中
数据的变化一直进行变化
,相当于完全还原最原本的变更检测
通过一个示例更好的理解这两个方法的区别:
示例解析:
当num是5的时候,因为使用了detach(),则此视图不在进行变更检测,当我们调用RecoveryDetection(),执行reattach()的时候,页面的数据恢复递增显示,如果我们在这个方法中执行的是detectChanges(),页面的数据只会增加一次,就不在更新变化了~
总结
如果大家想真正了解Angular的变更检测,一定要动手写一写相关的demo去验证自己的猜想,否则就像之前的我一样,看了很多理论,还是不能够了解,而且还很容易弄混ChangeDetectorRef的属性~ 希望大家看到这篇文章可以更加清晰点吧~