在本文中我将会深入讨论Angular 2 中的变更检测系统。
高层次概览
一个Angular 2 应用就是一颗组件树。
Angular 2 应用是一个反馈系统,变更检测是它的核心。
每一个组件都有一个变更检测器(change detector ),负责检测模板中所定义的数据绑定。绑定示例:{{todo.text}} 和[todo]=”t”。变更检测器会传播绑定,以深度优先的顺序从根节点向叶子节点传播。(换句话说,数据会从根节点流向叶子节点---译者注。)
Angular 2 里面并没有设计一种通用的机制来实现双向数据绑定(但是,你仍然可以实现双向绑定行为以及ng-model特性。更多内容请点这里。)。这就是为什么变更检测路径是有向树而且不可以带有闭环的原因。这种结构让检测系统极其高效。更重要的是,它可以保证系统具备更强的可预测性,并且更加方便debug。
有多快?
默认情况下,变更检测会遍历组件树中的每一个节点,看看是不是发生了变化,而且对于浏览器发出的每一个事件都会进行一轮检测。这种做法乍一看非常低效,而实际上Angular 2 变更检测系统可以在几个毫秒内(具体数值和平台有关)进行成百上千次这样的简单检测。至于我们是怎么达成如此感人的效率的,那是另一篇文章的话题了。
Angular必须采用保守的策略,每一次都检查所有节点,因为JavaScript语言并没有在对象变更方面给我们提供任何保证(这里的意思是说,当一个普通的JavaScript对象里面的某个属性发生了修改的时候,我们没办法精确地知道到底是哪个属性被改了---译者注。)。但是,如果我们使用不可变对象(immutable object)或者可观察(observable object)对象,我们就可以知道对象中的某个特定的属性发生了变化。以前Angular无法利用这一点,而现在可以了。
Immutable(不可变)对象
如果一个组件只依赖于它的那些输入属性,而这些属性是不可变类型,那么只有当其中一个输入属性发生变化的时候这个组件才会发生改变。这样一来,我们就可以在变更检测树里面跳过这个组件的子树,直到它的某个输入属性触发变更事件的时候再去检测。当发生变更事件的时候,我们对组件所在的子树进行一次检测,然后立即禁用变更检测器直到发生下一次变化为止(下图中灰色的方块表示被禁用的变更检测器)。
我们采用比较激进的方式使用不可变对象,那么在大多数时间里面,变更检测树里面大块地方都会处于禁用状态。
这一机制是如何实现的并不重要。你只要把变更检测策略设置为OnPush就可以了。
请注意,组件仍然可以拥有可变的状态,只要这个状态只会因为输入属性发生改变而改变,或者因为组件模板内部触发的事件而改变即可。OnPush策略唯一禁止的事情是依赖于共享的可变状态。更多细节请点这里。
Observable(可观察) 对象
如果组件只依赖于它的那些输入属性,并且这些属性是可观察的,那么只有这些属性之一触发事件的时候组件才会发生改变。这样一来,我们就可以在变更检测树里面跳过这个组件的子树,直到发生这样的事件为止。在触发事件之后,我们可以对这颗子树进行单次检测,然后立即禁用直到发生下次变更。
虽然这里的处理方式看起来和不可变对象那一小节很类似,但是实际上是完全不同的。如果你的组件树是由不可变对象绑定构成的,发生一次变化就必须从根组件开始遍历所有组件。而处理可观察(observable)对象的方法却不是这样的。
我来草拟一个很小的例子示范一下这个问题。
ObservableTodosCmp的模板:
ObservableTodoCmp最终的样子:
如你所见,这里Todos组件只有一个todos数组的引用,这个数组是可观察的(observable)。所以,组件无法感知到数组里面每一个todo的变化。
处理这个问题的方法是,当其中一个可观察的todo触发事件的时候,从根组件开始一路检测到真正发生了变化的Todo组件为止。变更检测系统会保证这一过程。
假设我们的应用只使用可观察对象。出现以上情况的时候,Angular就会检查所有对象。
所以,第一趟检查完成之后的状态看起来就像这样:
比方说,这时候第一个可观察的todo触发了一个事件。那么,系统将会切换到以下状态:
在App_ChangeDetector、Todos_ChangeDetector,以及第一个Todo_ChangeDetector检查完成之后,系统又会回到以下状态:
假设发生变化的次数非常少,并且组件构成的是一颗平衡树,那么使用可观察对象会把复杂度从O(N)降低到O(logN),其中N是系统中数据绑定的总数量。
此功能并没有绑定到任何一个特定的库上面。把Angular切换到其它任何observable library都只需要修改几行代码而已。
可观察对象会导致级联更新吗?
可观察对象名声比较差,因为它们可能会导致级联更新。有使用过基于可观察模型的框架来构建大型应用经验的人都知道我在说什么。一个可观察对象发生更新可能会导致一大堆可观察对象触发更新,然后就这样一直级联下去。最后,在检测过程中的某个不确定的地方,视图会被更新。这种系统非常难以debug。
如上面的例子所示,在Angular 2 里面使用可观察对象不会出现这种问题。当可观察对象触发事件的时候,只是标记出一条路径,从组件一直延伸到根,在下次检测的过程中会沿着这条路径进行。然后,普通的变更检测过程开始介入,以深度优先顺序开始遍历组件树中的节点。所以,无论你是否使用可观察对象,更新的顺序都不会发生改变。这一点非常重要。使用可观察对象变成了一种非常简单的优化手段,而且并不会改变你理解系统的方式。
为了这些好处我必须在每个地方都使用observable/immutable对象吗?
不,你没有必要这样做。你可以只在应用里面的某个局部使用可观察对象(例如,在某个巨大的table里面),然后那个部分就可以获得巨大的性能提升。你甚至可以构建基于两种数据类型的组件,从而可以同时获得它们所带来的好处。例如,一个 “observable component”可以包含一个 “immutable component”,然后这个组件内部可以再包含一个“observable component”。即使在这种情况下,在传播变更的时候,变更检测系统一样能够最小化必要检测的次数。
小结
● Angular 2 应用是一个反馈式系统。
● 变更检测系统会按照从根到叶子的顺序传播数据绑定。
● 与Angular 1.x不同,Angular 2中的变更检测路径是一颗有向树。结果就是,整个系统性能更高并且可预测性更好。
● 默认情况下,变更检测系统会遍历整棵组件树。但是,如果你使用不可变对象或者可观察对象,你就可以享受到它们带来的优势,只要检测组件树里面“真正发生变化”的部分即可。
● 这些优化手段可以成为变更检测系统的组成部分,但是又不会破坏变更检测系统所提供的功能。