NG2: 变化检测机制解析

 

What’s change detection anyways?

变化检测的基本任务就是,对程序内部的状态值进行检测,让状态值跟UI的展示效果同步,这个状态值可以是对象、数组或者基本类型的变量,任何合法的JS数据结构都是可以的。

这些程序内部的状态值,在UI上可能是文章段落、表单或者按钮,对于web应用来说就是DOM。基本上说我们就是把这些数据结构作为输出,然后把DOM给用户输出到UI上,我们把这个过程称为渲染过程。

1

但是,如果这个数据的变化发生在程序的runtime,在DOM已经生成以后,情况就会有点儿复杂。我们如何能够得知数据模型发生了怎样的变化呢?访问DOM的成本总是很重,所以我们需要找一个比较简便的方法。

有很多的方法来解决这个问题,其实一个最笨的方法是,对整个DOM进行刷新,比如一旦触发http请求,就去整体的刷新整个页面。另一个方法是,对DOM的新状态值和老状态值进行diff, 然后只对变化的部分进行更新。

在不同的JS框架,有很多的方法来处理这个任务,可以参考这篇文章:

http://teropa.info/blog/2015/03/02/change-and-its-detection-in-javascript-frameworks.html

我们这篇文章主要分析NG2的变化检测机制。

What causes change?

我们已经知道了变化检测是什么了,下一个问题是,到底什么是时候变化检测会发生,需要去更新UI呢?Ok让我们看看下面的代码:

2

希望你对于NG2的组件已经足够的属性了,关于组件的实现不是本文的目的,所以不展开讲了。

这个上面的组件简单的展示两个属性和一个方法去改变属性值,这个方法会在模板中按钮元素被点击的时候触发。 在这个case中,按钮被点击的时刻也是程序中的状态值发生变化的时刻。也就是说我们需要更新UI了。

下面是另外一个情况:

3

上面这个组件在初始化的时候会生成一个联系人列表,起初是空列表,之后发出http请求,当请求返回的时候,列表要进行更新,也就是说状态值发生了变化,我们需要更新UI了。

所以NG2就对这些可能导致属性值发生变化的场景进行了总结,归结起来就是下面这三种情况:

  • 事件绑定: click, submit等等
  • XHR请求: 从server获取异步数据
  • Timer操作:setTimeout(),setInterval()

可以发现这些都是异步操作,所以我们可以得到一个结论,就是无论何时当异步操作发生的时候,应用的状态值就有可能发生了变化。也是需要进行UI更新的时候了。

Who notifies Angular?

Alright, 我们现在知道了导致应用状态发生变化的原因。但是Angular是如何知道的呢?Angular如何知道发生了异步事件呢?

原来angular利用了Zones来做这部分的工作。事实上,Angular在通用Zones的基础上做了一些扩展称为NgZone, 关于这个机制你可以看看下面这篇文章:

https://blog.thoughtram.io/angular/2016/02/01/zones-in-angular-2.html

长话短说的话,在ng2的源码中有一个东西叫做ApplicationRef, 它会对NgZones的onTurnDone事件进行监听,无论何时这个事件被触发,就会执行tick函数,进而执行变化检测。

Change detection

聊完了变化检测的触发机制,我们得具体说说变化检测的机制了。首先,我们需要注意的是,在ng2中每个组件都有它自己的变化检测器。

5

所以对应着组件树,我们其实也能得到一个变化检测器树,这是一个很重要的,它是后面做性能优化的基础,因为它允许我们单独的控制每个组件。

让我们假设组件树中有个时间被触发,比如一个按钮被点击。下一步会发生什么呢?ngzone会捕获这个异步事件,然后触发相应的变化检测。

6

在变化检测器树中,数据流是从上到下进行单向流动的。之所以数据流是单向流动的,是因为变化检测也是从上倒下,从根组件开始往下走的。这个单向的数据流要比ng1中的digest cycle要强的多,因为更具有可预期能力。

另外一个发现是,这种变化检测机制会在一次过滤之后达到稳定的状态。也就是说,如果在某组件触发变化检测之后,又发生了变化,ng2会抛出error.

Performance

Smarter change Detection

在默认情况下,如何有异步事件发生整个应用的变化检测器树都会进行检测。如何能够能够让angular只对发生变化的组件部分进行检测,性能不是能优化很多吗?

我们可以利用两种数据结构immutables和observables来实现这个优化的效果。

Understanding mutability

为了能够理解为什么immutable能够实现优化,我们首先需要了解mutability是什么意思。假设有下面这个组件:
7

这段代码没有太多新鲜的,VCardApp是父组件,<v-card>是子组件,子组件有一个属性绑定vData,通过父组件的vData值进行传递。另外,还有一个changeData方法。

关键的地方是,在changeData方法中,vData的name属性被改变了,但是要注意的是,它的引用地址并没有变。

这是JS对象天然的mutable属性决定的,因为这个原因NG2不得不采用保守的措施,对所有的组件的所有属性值进行深度的遍历。

所以需要immutable对象来发挥效用。

Immutable objects

Immutable对象保证不能操作并修改对象的属性。也就是说,如果我们使用immutable对象,又想修改这个对象的时候,总是得到一个新的引用地址,因为原始的对象是不可变的。

伪代码如下:

8

此处的someAPIForImmutables可以换成真的能够生成不可变对象的方法。

简单的讲就是:如果要改变,那末就生成一个新的引用地址。

Reducing the number of checks

首先来看下<v-card>组件:

9

我们发现这个组件完全依赖@input 输入值,也就是说,我们可以设置一种策略,如果输入属性没有变化的话,就可以跳过相应的变化检测。

而ng2的onpush策略恰恰就是为这个设计的。对于引用类型的变量,比如对象等,它是去check这个变量的引用地址是否发生了变化。对于一般的对象而言,容易出现上面叙述的那种情况,因为对象本身是mutable的。而immutable的价值就在这里,要想改变它就必须新建一个对象,导致引用地址发生变化,onPush策略就不会漏过需要检测的组件了。

onpush的使用还是很方便的,在对应的元数据中添加一个changeDetection属性就行了。

10

这样的话,我们就可以跳过某一部分的子组件树的变化检查,从而提高效率

11

Observable

像上面说的那样,observables这种数据结构也是一种能够准确地发现数据变化的方法,不过跟immutable不同,observable不会在变化发生的时候,给出一个全新的引用值。observable的处理方法是触发一个事件,我们通过subscribe这个事件来进行响应。

在使用observables的case下,我们也想通过OnPush策略去skip某个子组件树的变化检测,从而实现提升性能的目的,但是如果JS对象的引用永远不会发生变化,我们该如何处理呢?。ng2提供了一个非常smart的方法,能够让组件树结构中的某个路径在某些事件触发的时候进行变化检测,这应该恰恰是我们需要的。。

比如下面这个组件:

12

比如说,我们想开发一个电商应用的购物车模块,当用户向购物车中添加新商品的时候,希望在UI上出现一个计数器,这样用户可以看到购物车中的产品数量。

上面的CartBadgeCmp组件就是干这个的,它有一个counter属性,另外还有一个addItemSteam输入属性,可以看出它是一个observable。关于observable的具体细节会在另外的文章中进行更加全面的解析。

https://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html

同样的,我们也将变化检测机制设为OnPush,所以只用当输入属性发生变化时,变化检测才会被触发。

但是如上面所说的那样,addItemStream的引用是不会变的,所以对于这个子组件树来说,变化检测也就不会触发。这样问题就来了,因为可以看到在组件的ngOnInit生命周期钩子中,我们subscribe了这个事件流,并且会把counter进行增值。这就出现了状态值变了,但是view并不会更新。

在这样的情况下,我们的变化检测树的样子应该是这样的:

13

就是没有任何检测实例被触发。

我们如何处理这样的问题呢?不用担心angular提供了相应的方法,像我们上面说的,变化检测是从组件树的顶部到底部的顺序执行的。所以我们做为app的开发者需要告诉angular一条路径,在这条路径上的组件是需要进行变化检测的。

angular给我们提供了ChangeDetectorRef类,通过它的markForCheck()方法,我们可以手动的触发变化检测。

具体的实现像下面这样:

14

15

利用observables,变化检测树的触发情况如下,正是我们需要的效果

16

原文链接:https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html

Leave a Reply

Your email address will not be published. Required fields are marked *