Angular的变更检测
Angular提供了数据绑定的功能。所谓的数据绑定,就是把组件类中的数据与页面的DOM元素关联起来,当数据发生变化时,Angular能够监测到这些变化,并对其绑定的DOM元素进行相应的更新(如何更新这是在模板中的各种情况来决定的,而且更新也只是更新变化的局部,所以重点是Angular如何准确的定位到变化的地方)。
通常,异步事件的发生会导致组件中的数据发生变化,但Angular并不是去捕获对象的变动,而是在适当的时机去检测对象的值是否被改变。这个时机是由NgZone这个服务去掌控的,它会获取整个应用的执行上下文,能够对相关的异步事件的发生、完成、异常等进行捕获,然后驱动Angular的变化检测机制去执行。
我们来整理一下:
- 首先,数据发生变化
- 被NgZone服务捕获
- NgZone服务通知Angular有数据发生改变
- Angular接收到有数据发生了变化的信号,对组件执行变更监测处理
下面我们来大概说明一下:
一、什么触发了数据变化
一般有3中情况触发数据改变:
- 用户动作,比如mousemove,input,click
- ajax,从server获取数据
- 定时操作,比如setTimeout,setInterval
共同特点是都是异步操作。
二、NgZone服务捕获异步事件并通知Angular执行对象比较
Angular需要一种能够捕获异步操作的机制,Angular本身没有这种功能,所以它引入了Zone库。
NgZone服务就是使用Zone实现的,NgZone从Zone中fork了一份实例 ,是Zone派生出来的一个子Zone,这个子Zone拥有Angular执行环境的执行上下文,所以在Angular环境内注册的异步操作都运行在这个子Zone上。
Zone对外提供了一些钩子,能够在异步操作发生、完成、错误时调用这些钩子达到通知外部的功能。
Angular中,就是在NgZone服务中监听了Zone提供的钩子,在异步操作完成后去执行变化监测。
三、Angular如何执行对象比较
Angular应用由组件组成,形成一棵组件树。每一个组件都有自己的变化监测器,所以也组成了一棵变化监测树。
每次变更监测都是从上到下执行的,先从根组件开始,从上往下地监测每一个组件的变化。
组件树中的每一个组件都有自己的变更监测器。这使得每个组件的变化监测相互独立,可以灵活的控制变更监测的执行或者暂停。
下面来看一下变更监测器是如何工作的:
对于每一个组件,Angualr都会创建一个变更监测类的实例,在这个变更监测类的实例中,准确的记录了当前组件的数据模型,当下次变更监测到来时,会被组件当前的数据模型和准确保存下来的数据模型进行比较,(怎么进行比较又是一个大问题)。
默认情况下,任何一个组件模型中的数据变化都会导致整个组件树进行变化监测,但其实很多组件的输入属性没有变化时,数据就不会发生改变,如此没有必要对这样的组件进行变化监测的操作。
那么如何让Angular仅仅监测变化的组件呢?
前面提到过,Angular为每一个组件都创建了一个变化监测类的实例,该实例提供了一些方法可以手动管理变化监测。
当发生变化时,Angular会从根组件到子组件来监测每个组件是否发生了变化。Angular本身并不知道是哪个组件发生了变化,所以才遍历所有组件进行监测。
但是开发者是知道哪个组件发生了变化,所以可以通过变化监测类的实例提供的方法给这个组件做标记,来通知Angular仅仅监测这个组件所在的路径上的组件就可以了。
1 export declare abstract class ChangeDetectorRef { 2 /** 3 * 当视图使用 OnPush(checkOnce)变更检测策略时,把该视图显式标记为已更改,以便它再次进行检查。 4 * 当输入已更改或视图中发生了事件时,组件通常会标记为脏的(需要重新渲染)。调用此方法会确保即使那些触发器没有被触发,也仍然检查该组件。 5 * 把根组件到该组件之间的这条路径标记出来,通知Angular在下次触发变化监测时必须检测这条路径上的组件。 6 */ 7 abstract markForCheck(): void; 8 /** 9 * 从变更检测树中分离开视图。 已分离的视图在重新附加上去之前不会被检查。 与 detectChanges() 结合使用,可以实现局部变更检测。 10 * 从变更监测树上分离出当前的变更监测器,该组件的变化监测器将不会主动的执行变更监测,除非调用redetach方法重新安装上 11 * 即使已分离的视图已标记为脏的,它们在重新附加上去之前也不会被检查。 12 */ 13 abstract detach(): void; 14 /** 15 * 检查该视图及其子视图。与 detach 结合使用可以实现局部变更检测。 16 */ 17 abstract detectChanges(): void; 18 /** 19 * 检查变更检测器及其子检测器,如果检测到任何更改,则抛出异常。 20 * 在开发模式下可用来验证正在运行的变更检测器是否引入了其它变更。 21 */ 22 abstract checkNoChanges(): void; 23 /** 24 * 把以前分离开的视图重新附加到变更检测树上。 视图会被默认附加到这棵树上。 25 * 重新把变更监测器放在变更监测树上 26 */ 27 abstract reattach(): void; 28 }
同时,也可以设置变更检测策略,
当组件实例化之后,Angular 就会创建一个变更检测器,它负责传播组件各个绑定值的变化。 该策略是下列值之一:
-
ChangeDetectionStrategy#OnPush
把策略设置为CheckOnce
(按需)。 -
ChangeDetectionStrategy#Default
把策略设置为CheckAlways
。
2个优化的场景:
为组件设置了 OnPush
变更检测策略(CheckOnce
而不是默认的 CheckAlways
),然后每隔一段时间强制进行第二轮检测。
1 @Component({ 2 selector: 'my-app', 3 template: `Number of ticks: {{numberOfTicks}}`, 4 changeDetection: ChangeDetectionStrategy.OnPush, 5 }) 6 7 class AppComponent { 8 numberOfTicks = 0; 9 10 constructor(private ref: ChangeDetectorRef) { 11 setInterval(() => { 12 this.numberOfTicks++; 13 // require view to be updated 14 this.ref.markForCheck(); 15 }, 1000); 16 } 17 }
分离开变更检测器以限制变更检测的发生频度
定义了一个带有只读数据的大型列表,这些数据预计每秒会变化很多次。 为了提高性能,我们检测和更新列表的频率就应该比实际发生的变化少得多。 要解决这个问题,就要分离开变更检测器,并每隔五秒钟显式执行一次变更检查。
1 constructor(private ref: ChangeDetectorRef, private dataProvider: DataListProvider) { 2 ref.detach(); 3 setInterval(() => { this.ref.detectChanges(); }, 5000); 4 }
准备面试使用,比较粗糙,各位看官见谅。