什么是变化检测?
变化检测的基本功能就是获取应用程序的内部状态(state),并且是将这种状态对用户界面保持可见.状态可以是javascript中的任何的数据结构,比如对象,数组,(数字,布尔,字符串等基础数据类型).这种状态最终可能成为用户界面中的段落,表单,链接或按钮,在web浏览器中我们们称之为文档对象模型(dom).将数据结构作为输入生成dom作为输出并展示给用户,我们称这个过程为渲染.
但是,在运行时发生更改时会变得更加棘手。一段时间后,DOM已经被渲染。我们如何弄清楚模型中发生了哪些变化,以及我们需要在哪里更新DOM?访问DOM树总是很昂贵,因此我们不仅需要找出需要更新的位置,而且还希望尽可能小地保持访问权限.这可以通过许多不同的方式解决。例如,一种方法是简单地发出http请求并重新呈现整个页面。另一种方法是将新状态的DOM与先前状态进行区分并仅渲染差异的概念,这就是React使用Virtual DOM进行的操作.
因此,变更检测的目标始终是预测数据及其变化。
什么导致变化发生?
现在我们知道变化检测的全部内容,我们可能想知道,何时会发生这样的变化? Angular什么时候知道它必须更新视图?好吧,我们来看看下面的代码
@Component({ template: ` <h1>{{firstname}} {{lastname}}</h1> <button (click)="changeName()">Change name</button> ` }) class MyApp { firstname:string = 'Pascal'; lastname:string = 'Precht'; changeName() { this.firstname = 'Brad'; this.lastname = 'Green'; } }
上面的组件只显示两个属性,并提供了一种方法,可以在单击模板中的按钮时更改它们。单击此特定按钮的那一刻是应用程序状态发生更改的时刻,因为它会更改组件的属性。那是我们想要更新视图的那一刻。
又比如:
@Component() class ContactsApp implements OnInit{ contacts:Contact[] = []; constructor(private http: Http) {} ngOnInit() { this.http.get('/contacts') .map(res => res.json()) .subscribe(contacts => this.contacts = contacts); } }
该组件包含联系人列表,在初始化时,它会执行http请求。一旦此请求返回,列表就会更新。同样,此时,我们的应用程序状态已更改,因此我们将要更新视图。
应用程序状态改变一般由以下3个方面引起:
- 事件: click,input,submit...
- XHR: 从远程服务器获取数据
- 定时器:setTimeout(),setInterval()
事实证明,这三件事有一些共同之处。你能说出来吗? ......正确!它们都是异步的.
为什么你认为这很重要?嗯......因为事实证明,当Angular真正对更新视图感兴趣时,这些是唯一的情况。假设我们有一个Angular组件,当单击一个按钮时它会执行一个处理程序:
@Component({ selector: 'my-component', template: ` <h3>We love {{name}}</h3> <button (click)="changeName()">Change name</button> ` }) class MyComponent { name:string = 'thoughtram'; changeName() { this.name = 'Angular'; } }
单击组件的按钮时,将执行changeName(),这将更改组件的name属性,由于我们希望此更改也反映在DOM中,因此Angular将相应地更新视图绑定{{name}}。很好,这似乎神奇地工作.
另一个例子是使用setTimeout()更新name属性。请注意,我们删除了该按钮。我们不是必须去做一些特殊的事情来通知框架状态 发生了变化
@Component({ selector: 'my-component', template: ` <h3>We love {{name}}</h3> ` }) class MyComponent implements OnInit { name:string = 'thoughtram'; ngOnInit() { setTimeout(() => { this.name = 'Angular'; }, 1000); } }
谁通知Angular进行变化检测?
Angular允许我们直接使用本机API。我们不需要调用拦截器方法,因此Angular会通知更新DOM。这是纯粹的魔法吗? 背后的秘密就是Angular利用了Zones库.Zones猴子补丁全局异步操作,如setTimeout()和addEventListener(),这就是Angular可以轻松找到的原因,何时更新DOM. 简短的版本是,在Angular的源代码中,有一个名为ApplicationRef的东西,它监听NgZones onTurnDone事件。每当触发此事件时,它都会执行tick()函数,该函数基本上执行更改检测。
// very simplified version of actual source class ApplicationRef { changeDetectorRefs:ChangeDetectorRef[] = []; constructor(private zone: NgZone) { this.zone.onTurnDone .subscribe(() => this.zone.run(() => this.tick()); } tick() { this.changeDetectorRefs .forEach((ref) => ref.detectChanges()); } }
变化检测执行机制
一个重要的事实是:我们可以为每个组件单独控制如何以及何时执行更改检测
由于每个组件都有自己的更改检测器,而Angular应用程序由组件树组成,因此逻辑结果是我们也有一个更改检测器树。此树也可以视为有向图,其中数据始终从顶部流向底部.数据从上到下流动的原因是因为每个单独的组件,从根组件开始,每个组件也始终从上到下执行更改检测。这很棒,因为单向数据流比循环更容易预测。相比之下,AngularJS采用的是双向数据流,错综复杂的数据流使得它不得不多次检查,使得数据最终趋向稳定。理论上,数据可能永远不稳定。AngularJS给出的策略是,脏检查超过10次,就认为程序有问题,不再进行检查。我们总是知道我们在视图中使用的数据来自何处,因为它只能来自其组件。在Angular 2+中,另一个有趣的观察是一次通过后变化检测变得稳定。这意味着,如果我们的某个组件在更改检测期间第一次运行后导致任何其他副作用,Angular将抛出错误。在开发模式下,Angular会进行二次检查,如果出现上述情况,二次检查就会报错:Expression Changed After It Has Been Checked Error。而在生产环境中,脏检查只会执行一次。
变化检测性能
默认情况下,即使我们每次都要检查事件发生时每个组件,Angular都非常快。它可以在几毫秒内执行数十万次检查。这主要是因为Angular生成了VM友好代码。那是什么意思?好吧,当我们说每个组件都有自己的变化检测器时,它不像Angular中的这个通用的东西,它负责每个组件的变化检测。原因是它必须以动态方式编写,因此无论模型结构如何,它都可以检查每个组件。虚拟机不喜欢这种动态代码,因为它们无法对其进行优化。它被认为是多态的,因为物体的形状并不总是相同的。
Angular在运行时为每个组件创建变化检测器类,这些组件是单态的,因为它们确切地知道组件模型的形状。 VM可以完美地优化此代码,从而使其执行起来非常快。好消息是我们不必过多关心它,因为Angular会自动完成它。
本篇简单介绍下angular 2+变化检测的基础,下一篇重点讲一下变化检测策略.