目前几种主流的前端框架都已经实现双向绑定特性,但实现的方法各有不一样:javascript
下面咱们就来了解一下 ng2.x+ 的版本中的脏检查机制是如何运行的。html
变化检测(脏检查)的基本任务是获取程序内部状态的变化,并使其在用户界面上以某种方式可见,这种状态的变化能够来自于 JavaScript 的任何数据结构,最终呈现为用户界面中的段落、表单、连接或者按钮等 DOM 对象。咱们把输入数据结构并生成 DOM 结构显示给用户的过程叫做 渲染。然而在程序运行时发生变化状况比较复杂,咱们须要肯定模型中发生什么变化,以及什么地方须要更新 DOM 节点。操做 DOM 树十分昂贵,因此咱们不只须要找出待更新的地方,还须要保持操做数尽量小。这可能经过许多不一样的方式来解决:好比咱们能够简单的发起 http 请求并从新渲染整个页面,或者能够区分 DOM 树的新旧状态并只从新渲染两者不一样的部分(ReactJS 虚拟 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 请求,一旦这个请求返回列表就会被更新,此时咱们的应用程序状态发生改变,并须要更新视图。基本上应用程序状态的改变能够由三类活动引发:vue
上述活动都是异步的,所以咱们能够得出结论:每当执行一些异步操做时,咱们的应用程序状态可能发生改变,这时则须要 Angular 更新视图。Angular 在启动时会重写(经过 Zone.js)部分底层浏览器 APIs 好比 addEventListener:java
// this is the new version of addEventListener function addEventListener(eventName, callback) { // call the real addEventListener callRealAddEventListener(eventName, function() { // first call the original callback callback(...); // and then run Angular-specific functionality var changed = angular2.runChangeDetection(); if (changed) { angular2.reRenderUIPart(); } }); }
Zone 负责通知 Angular 进行视图更新,Angular 封装有 NgZone,简单来讲,经过 Angular 的部分源码咱们能够知道有一个叫做 ApplicationRef 的东西负责监听 NgZone 中的 onTurnDone 事件,每当该事件触发时,它就执行 trick 方法进行变化检测的基本工做。json
// 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 中每一个组件都有本身的变化检测器,这使得咱们能够对每一个组件分别控制如何以及什么时候进行变化检测。
因为每一个组件都有其本身的变化检测器,即一个 Angular 应用程序由一个组件树组成,因此逻辑结果就是咱们也有一个变化检测器树,这棵树也能够看做是一个有向图,数据老是从上到下流动。数据从上到下的缘由是由于变化检测也老是从上到下对每个单独的组件进行,每一次从根组件开始,单向数据流比循环脏检查更可预测,咱们老是能够知道视图中使用的数据来自哪里。浏览器
咱们假设在组件树的某个地方触发一个事件,好比一个按钮被点击,zones 会进行事件的处理并通知 Angular,而后变化检测依次向下传递。
一种方法是基于组件的生命周期钩子:
ngAfterViewChecked() { if (this.callback && this.clicked) { console.log("changing status ..."); this.callback(Math.random()); } }
在开发模式下运行 Angular 会在控制台中获得一条错误日志,生产模式下则不会抛出。
EXCEPTION: Expression '{{message}} in App@3:20' has changed after it was checked
另外一种方法是手动控制变化检测的打开/关闭,并手动触发:
constructor(private ref: ChangeDetectorRef) { ref.detach(); setInterval(() => { this.ref.detectChanges(); }, 5000); }
Angular 2.x+ 的数据流是自顶向下,从父组件向子组件的的单向流动,变化监测树与之相呼应,单项数据量保证变化监测的高效性和可预测性。检查父组件后,子组件可能会改变父组件中的数据使得父组件须要被再次检查,这是不被推荐的数据处理方式,而且在开发模式下这种状况会抛出异常 ExpressionChangedAfterItHasBeenCheckedError,在生产模式下不会报错可是脏检查仅会执行一次。相比之下 1.x 的版本采用双向数据流,为了使得数据最终趋向稳定不得很少次检查错综复杂的数据流,性能提高就此可见一斑。前端框架
默认状况下,即便每次发生事件都须要检查每一个组件,Angular 速度仍然很是快,它能够在几毫秒内执行数十万次检查,这主要是因为 Angular 能够生成 VM 友好的代码。
Angular 每次都要检查每一个组件,由于事件发生的缘由也许是应用程序状态已经改变,可是若是咱们可以告诉 Angular 只对那些改变状态的应用程序部分运行变化检测,那不是很好吗?事实证实,有些数据结构能够给咱们何时发生变化的一些保证 - Immutables 和 Observables。服务器
好比咱们拥有一个组件 VCardApp 使用 v-card 做为子组件,其具备一个输入属性 vData,而且咱们可使用 changeData 方法改变 vData 对象的 name 属性(并不会改变该对象的引用)。
@Component({ template: '<v-card [vData]="vData"></v-card>' }) class VCardApp { constructor() { this.vData = { name: 'Christoph Burgdorf', email: 'christoph@thoughtram.io' } } changeData() { this.vData.name = 'Pascal Precht'; } }
当某些事件致使 changeData 执行时, vData.name 发生改变并传递至 v-card 中, v-card 组件的变化检测器检查给定的数据新 vData 是否与之前同样,在数据引用未变可是其参数改变的状况下,Angular 也须要对该数据进行变化监测。这就是 immutable 数据结构发挥做用的地方。angular2
[
How I optimized Minesweeper using Angular 2 and Immutable.js to make it insanely fast](https://www.jvandemo.com/how-...
Immutable 为咱们提供不可变的对象:这意味着若是咱们使用不可变的对象,而且想要对这样的对象进行更改,咱们会获得一个新的引用(保证原始对象不变)。
var vData = someAPIForImmutables.create({ name: 'Pascal Precht' }); var vData2 = vData.set('name', 'Christoph Burgdorf'); vData === vData2 // false
上述伪代码即演示不可变对象的含义,其中 someAPIForImmutables 能够是咱们想要用于不可变数据结构的任何 API。
当输入属性不变时,Angular能够跳过整个变动检测子树。若是咱们在 Angular 应用程序中使用不可变对象,咱们所须要作的就是告诉 Angular 组件能够跳过变化检测,若是它的输入没有改变的话。
@Component({ template: ` <h2>{{vData.name}}</h2> <span>{{vData.email}}</span> ` }) class VCardCmp { @Input() vData; }
正如咱们所看到的, VCardCmp 只依赖于它的输入属性,咱们能够告诉 Angular 跳过这个组件的子树的变化检测,若是它的输入没有改变,经过设置变化检测策略 OnPush 是这样的:
@Component({ template: ` <h2>{{vData.name}}</h2> <span>{{vData.email}}</span> `, changeDetection: ChangeDetectionStrategy.OnPush }) class VCardCmp { @Input() vData; }
Angular OnPush Change Detection and Component Design - Avoid Common Pitfalls
与不可变的对象不一样,当进行更改时 Observables 不会给咱们提供新的引用,而是发射咱们能够订阅的事件来对他们作出反应。
@Component({ template: '{{counter}}', changeDetection: ChangeDetectionStrategy.OnPush }) class CartBadgeCmp { @Input() addItemStream:Observable<any>; counter = 0; ngOnInit() { this.addItemStream.subscribe(() => { this.counter++; // application state changed }) } }
好比咱们用购物车创建一个电子商务应用程序:每当用户将产品放入购物车时,咱们须要在用户界面中显示一个小计数器,以便用户能够看到购物车中的产品数量。该组件有一个 counter 属性和一个输入属性 addItemStream,当产品被添加到购物车时,这是一个被触发的事件流。另外,咱们设置了变化检测策略 OnPush,只有当组件的输入属性发生变化时,变化检测才会执行。
如前所述,引用 addItemStream 永远不会改变,因此组件的子树从不执行变动检测。
当整个树被设置成 OnPush 后,咱们如何通知 Angular 须要对这个组件进行变化检测呢?正如咱们所知,变化检测老是从上到下执行的,因此咱们须要的是一种能够检测树的整个路径到发生变化的组件的变化的方法。咱们能够经过依赖注入访问组件的 ChangeDetectorRef,这个注入来自一个叫作 markForCheck 的 API,它标记从组件到根的路径,以便下次更改检测的运行。
constructor(private cd: ChangeDetectorRef) {}
ngOnInit() { this.addItemStream.subscribe(() => { this.counter++; // application state changed this.cd.markForCheck(); // marks path }) } }
下面是在可观察事件被触发后,变化检测开始前。
如今当执行更改检测时,它将从上到下进行。
而且一旦更改检测运行结束,它将恢复 OnPush 整个树的状态。
TAKING ADVANTAGE OF OBSERVABLES IN ANGULAR
ANGULAR CHANGE DETECTION EXPLAINED
How does Angular Change Detection Really Work ?
Change And Its Detection In JavaScript Frameworks