浅谈 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 将此组件进行标注,通知变化检测器在下次变化让变化检测阶段更新此组件中输入属性的值。这篇文章详细的说明了这一点。

很多组件的设计都采用了 OnPush 策略,比如 NG-ZORRO,因此我们必须改变值的引用才能让组件收到变化检测的消息。

相关阅读

变化检测是 Angular 实现 MVVM 中 VM 的一种方法,与之具有同样地位的是 Angular 中隐藏的概念视图。只所以称之为隐藏,只是因为这个概念在官方文档中丝毫未被提及,却在源码中能寻觅到它的影子。我在理解 Angular 视图中讨论了 Angular 中视图的作用以及它与 MVVM 模式的关系。

参考链接

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

发表评论

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