深入理解 Angular 变化检测(change detection)

csRyan

引言

本文分享一些讲解Angular Change Detection的文章,并指出其中有意思的内容,以及自己的一些总结和引申。

Angular Change Detection Explained by thoughtram

  • change detection的基本任务:用进程内的状态(Component中的数据)来更新view(DOM)的显示。
  • Angular Change Detection发生的时机:基本上所有的异步事件发生(并且回调函数已经执行完毕)以后,都需要触发change detection(因为此时进程的状态可能已经发生改变):

    • Events - click, submit, …
    • XHR - Fetching data from a remote server
    • Timers - setTimeout(), setInterval()
  • 单向数据流:沿着组件树进行变化检测,检查完父组件以后再检查子组件,在检查父组件的时候可能会更新子组件中的绑定,但是在检查子组件的时候(此时父组件已经检查完毕)不会更新父组件的数据。也就是说,在变化检测的过程中,数据可以从父组件流进子组件,但不会从子组件流进父组件。这是Angular与AngularJS之间的重大区别。Angular的这个特点能够保证:只需要执行一次Change Detection,就能使得view与组件中的数据一致("change detection gets stable after a single pass")。而在AngularJS中,由于在检查一个组件的时候可能会改动另一个组件中的数据,因此需要多次检查,直到数据“稳定”下来。
    从别的文章偷来一张图(很多文章有这张图,已经不知道来源):
  • 专用change detector:Angular的变化检测出了名的快,这是其中一个很重要的原因。每个组件都有一个自己的change detector(Angular compiler为每个component编译生成专门检测它的view的代码),这使得每个change detector的检测代码非常地简单高效(VM friendly)。而在AngularJS中,所有component输入同一个算法来进行变化检测,虽然代码的一般性(generic)、通用性很强,但是这种代码执行的效率相对较慢,因为动态性(dynamic)强意味着执行引擎难以做假设、做实时优化。
多态(polymorphic):通用性往往意味着多态的存在。多态的含义是,一段代码,多次执行,但是每次执行所操作的对象都不是同一个对象,甚至这些对象的“形状”(shape)相差巨大(属性名不一样,或者属性被添加的顺序不一样)。这种代码难以被优化。大量使用单态(monomorphic)是Angular速度爆炸的重要原因!除了Change Detection方面,Angular在其他地方也使用了单态的优化思想。
How JavaScript works: inside the V8 engine + 5 tips on how to write optimized codev8 Design Elements 讲解了v8是如何优化代码的。
  • 这篇文章的后面部分讲的是如何通过changeDetection: ChangeDetectionStrategy.OnPush来对变化检测树进行“剪枝”,进一步降低变化检测的时间开销。使用到Immutable ObjectsChangeDetectorRef.markForCheck

Change Detection in Angular

作者Victor Savkin以前是Angular核心团队的成员,现在似乎自己创建了一个Angular的企业咨询公司。

变化检测是有向的

“Change detectors propagate bindings from the root to leaves in the depth first order.”
“传播”(propagate)这个词比较生动地体现了变化检测的特点。Angular程序员通过绑定来定义哪些数据可以传播到view或者子组件中。数据从父组件传播到子组件,反之不行。
并且,对组件树的变化检测是深度优先的。

数据传播到view的绑定:

<span>todo: {{todo.text}}</span>

数据传播到子组件的绑定:

<todo-cmp [model]="myTodo"></todo-cmp>

变化检测将用本组件的myTodo属性来更新子组件的@Input() model属性。

变化检测默认检测所有组件的原因

"Angular has to be conservative and run all the checks every single time because the JavaScript language does not give us object mutation guarantees."
原因在Angular Change Detection Explained by thoughtram也介绍过了。即使@Input()对象的引用没有变,其中的属性可能已经发生变化(JavaScript对象的动态性),变化检测需要将这种变化也反映在view和子组件上。

OnPush

接下来就是介绍changeDetection:ChangeDetectionStrategy.OnPush了。这里我不再做过多解释。引用作者在另一篇文章的话:
The framework will check OnPush components only when their inputs change or components’ templates emit events.

值得注意的是,作者指出OnPush并不是所有属性都必须是immutable的,只要@Input是immutable的,并且其他mutable的属性能保证仅在【@Input更新】或【组件的template中有事件触发】时才更新:
It is worth noting that a component can still have private mutable state as long as it changes only due to inputs being updated or an event being fired from within the component’s template. The only thing the OnPush strategy disallows is depending on shared mutable state. Read more about it here.

Two Phases of Angular Applications

文章开头概括得非常精辟:

Angular 2 separates updating the application model(可以理解为更新Component的属性值) and reflecting the state of the model in the view into two distinct phases. The developer is responsible for updating the application model. Angular, by means of change detection, is responsible for reflecting the state of the model in the view. The framework does it automatically on every VM turn.

Event bindings, which can be added using the () syntax, can be used to capture a browser event execute some function on a component. So they trigger the first phase.

Property bindings, which can be added using the [] syntax, should be used only for reflecting the state of the model in the view.

Angular应用的变化(Component属性的变化和DOM的变化)分成2个阶段(按发生先后顺序排序):

  1. 异步事件发生(比如用户点击或者AJAX请求完成),注册的回调函数被执行(比如<button (click)="handler($event)"></button>),这些回调函数可以改变Component中的属性。
  2. 回调函数执行完毕以后(本轮事件循环的末尾),Angular此时才会执行变化检测。变化检测根据我们定义好的数据绑定([model]='prop'),将Component中的新数据“推”到view中(包括更新DOM和更新子组件的@Input属性)。对组件树执行变化检测的顺序:从根到叶,深度优先。

第一个阶段结束以后才会进入第二个阶段。
我们只能控制第一阶段,因为回调函数是我们定义的,我们可以随意在其中更新父组件属性、子组件属性、本组件属性……Angular完全不会插手。
第二个阶段由Angular来完成。这阶段发生的就是变化检测(change detection)。在变化检测的过程中,这些变化会在组件树上传播:从父组件到子组件单向传播,以及从组件传播到它的DOM。哪些数据传播给子组件、更新子组件的那些属性、更新DOM的哪些属性……这些是由数据绑定来决定的(因此从某种意义上来说,我们也能稍微控制第二个阶段,毕竟数据绑定也是我们来写的)。

可见,事件绑定和数据绑定的语法虽然看起来很相似((event)=[bindProp]=),但是它们是在不同的阶段产生作用的。

这样划分阶段的意义

在AngularJS时代,脏检查的执行过程中不仅会更新DOM,而且可能会更新其他application model,但application model被更新以后,可能有别的DOM又因此需要被更新(因为DOM展示的内容依赖于application model),因此AngularJS不得不做多次脏检查,直到application model不再更新。这会影响应用的性能,而且不利于Debug(因为你不知道application model是什么时候被谁更新的,是事件回调更新的?还是在某次脏检查执行过程中被更新的?)。
再看一次这张图:

正因为这个原因,Angular才如此划分阶段。在Angular中,application model的更新只能有2个原因:

  • 在第一个阶段,被事件回调更新。
  • 在第二个阶段,组件的@Input属性被数据绑定更新(父组件将新数据"推"进本组件)。

开发者不需要像AngularJS时代那样考虑脏检查的杂乱更新过程,现在只要稍微分析一下就能知道数据是如何流动的。这让应用的逻辑更加清晰,更容易调试和重构。

view的更新也更加简单高效了,因为只需要执行一轮变化检测(一轮变化检测执行完以后数据就会稳定下来,不再变化)。并且数据的流动方向也非常清晰,始终是从父组件流入子组件(单向数据流)。

另外,Angular开发者也不需要像AngularJS开发者那样害怕数据环路了(看本小节第一段的例子),因为这不再会发生。在Angular中,在第一阶段,我们可以更新任何父组件、子组件的数据,在第二阶段也不会造成数据环路(因为在第二阶段,数据的传播是单向的)。

更多相关文章

弄懂了这几篇文章以后,很多相关文章的内容其实大同小异。我整理了一张change detection文章列表,里面的文章都是讲得比较好的,不过只有一篇是中文。。。如果感觉还不是太懂的话可以在里面多找几篇阅读。
其中angularindepth的文章一般会深入到源码,想要更深入理解的话可以阅读其中文章,乃至自己研究Angular源码。

阅读 6.3k

csRyan的学习专栏
分享对于计算机科学的学习和思考,只发布有价值的文章: 对于那些网上已经有完整资料,且相关资料已经整...

So you're passionate? How passionate? What actions does your passion lead you to do? If the heart...

1.1k 声望
170 粉丝
0 条评论

So you're passionate? How passionate? What actions does your passion lead you to do? If the heart...

1.1k 声望
170 粉丝
文章目录
宣传栏