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

引言

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

Angular Change Detection Explained by thoughtram

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

    • 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的企业咨询公司。java

变化检测是有向的

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

数据传播到view的绑定:程序员

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

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

<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和子组件上。api

OnPush

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

值得注意的是,做者指出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.app

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源码。

相关文章
相关标签/搜索