浅谈 Angular 变化检测机制

Photo by Roman Kraft on Unsplash

声明

变化检测(Change Detection)也叫脏值检测(Dirty Detection),都是指

Angular 中检测组件中数据变化的机制。由于变化检测与 Angular 若干生命周

期有很强的关联,本文会结合两者做出说明。

本文例子

首先我们构造一棵组件树,然后利用组件的生命周期钩子函数来得知框架在何

时做了变化检测,并对组件的样式做出对应的改变。在线例子可以看这里。下

文的实验全是基于这个示例。

变化检测

在 Angular 应用中,数据的变化能够通过绑定、插值等形式直接反映到页面

上。而数据变化是由 Angular 中的 NgZone( NgZone 是对 Zone.js的封装 )

对浏览器自带的事件及异步函数进行拦截,在其中注入变化检测操作实现的。

变化检测根据一定策略,通过深度优先遍历的算法,对组件树中绑定数据进行

更新,实现随着组件控制层数据与页面数据联动变化。

变化检测器

变化检测的最小单元称为变化检测器(Change Detector),一个变化检测器

单独检测一个组件的状态。各个变化检测器依据组件的结合方式,对应形成一

棵变化检测树。在默认策略下,变化检测树中的任意一个节点检测到一个组件

的状态可能会发生变更,整颗变化检测树都会重新进行检测。

在源码中可以看到 Angular 通过 checkAndUpdateBinding 这个函数来判断

绑定的数据是否发生了变化。其利用了数据结构 view 中的 oldValues 来存储

变化前的数据,在变化检测时,将此值与当前数据进行对比,发现不一致就会

触发页面结点( 具体来说是 DOM )的变更。

Default 策略

在默认策略下,若某一组件上触发了一个事件(如鼠标点击),那么整棵变化

检测树都会进行更新。如下所示,我调节了一个子组件中的值,整棵组件树都

进行了变更检测,这也使得各个组件的值同步发生了变化。一般来说变化检测

非常快,不会明显影响页面性能,但是假如在某些复杂的页面或者需要优化的

场景下,我们仅需对确定发生变化的组件进行检测,这才引出了 OnPush 策

略。

OnPush 策略

在 OnPush策略下,只有父组件改变了子组件的输入属性,才会触发子组件变

化检测。这里需要注意的是假如输入属性是一个对象,若父组件改变了对象的

属性而没有改变对象的引用,子组件仍然不会触发变化检测。我在例子中用

byRef 这个输入变量做了如下这个实验。

这边 byRef 是一个引用,初始化时 {id: 1},每次点击我都会修改这个引用的

id。可以看到,在改变 id 值,没有改变引用的情况下,子组件都不会进行变更

检测。

生命周期钩子

变化检测过程中,会触发对应组件的三个 check 钩子,分别是 ngDoCheck、

ngAfterContentChecked、ngAfterViewChecked。但是调用了这三个钩子不

意味着它对此组件真正意义上做了变化检测。

我们知道在 OnPush 策略下,若没有改变子组件的输入属性值,子组件的变化

检测不会被触发。但我们仍然能够调用子组件的 ngDoCheck 钩子。这是因为

在某些条件下,我们希望能够手动地触发变更检测。比如我们在父组件中修改

了某个输入型属性的值(非引用),但是子组件由于采用 onPush 策略,无法

接受到这个输入属性引用的变化,子组件不会进行变化检测。那么我们可以在

ngDoCheck 方法中显式地使用 markForCheck 将此组件进行标注,通知变化

检测器在下次变化让变化检测阶段更新此组件中输入属性的值。这篇文章详细

的说明了这一点。

参考链接

A gentle introduction into change detection in Angular

Angular OnPush Change Detection and Component Design – Avoid Common Pitfalls

Angular Change Detection – How Does It Really Work?

Angular Change Detection Strategy: An introduction

发表评论

电子邮件地址不会被公开。 必填项已用*标注