在系列一中,我们提到Zone.js,Zones是一种执行上下文,它允许我们设置钩子函数在我们的异步任务的开始位置和结束位置,Angular正是利用了这一特性从而实现了变更检测。
Zones.js非常适合Angular
事实证明,Zones解决的问题非常适合Angular在我们的应用程序中执行变更检测所需的内容。你有没有问过自己Angular何时以及为何进行变化检测?什么时候告诉Angular: 兄弟,我的应用程序中可能发生了一个变化。你能检查一下吗?在我们深入研究这些问题之前,让我们首先考虑一下应用程序中实际导致这种变化的原因。或者更确切地说,在我们的应用程序中可以改变状应用程序状态更改由三种情况引起:
- Events - 用户事件比如
click
,change
,input
,submit
, … - XMLHttpRequests - 比如从远程服务器获取数据
- Timers -
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在VM完成任务时执行更改检测的代码就像这样简单:
ObservableWrapper.subscribe(this.zone.onTurnDone, () => { this.zone.run(() => { this.tick(); }); }); tick() { // perform change detection this.changeDetectorRefs.forEach((detector) => { detector.detectChanges(); }); }
每当Angular的区域发出onTurnDone事件时,它就会运行一个任务,对整个应用程序执行更改检测。但是等一下,onTurnDone事件发射器来自哪里?这不是默认Zone.js API的一部分,对吧?事实证明,Angular引入了自己的名为NgZone的区域。
Angular中的NgZone
NgZone基本上是一个分叉区域,它扩展了它的API并为其执行上下文添加了一些额外的功能。它添加到API中的一件事是我们可以订阅的以下一组自定义事件,因为它们是可观察的流:
- onTurnStart() - 在Angular事件开始之前通知订阅者。每个由Angular处理的浏览器任务发出一次事件。
- onTurnDone() - 在Angular的区域完成后处理当前事件队列以及从事件队列的任何微任务后立即通知订阅者。
- onEventDone() - 在结束VM事件之前,在最终的onTurnDone()回调之后立即通知订阅者。用于测试以验证应用程序状态。
Angular添加自己的事件发射器而不是依赖于beforeTask和afterTask回调的主要原因是它必须跟踪定时器和其他微任务。将Observable用作处理这些事件的API也很不错。
在Angular区域外运行代码
因为NgZone实际上是全局区域的一个分支,所以Angular可以完全控制代码什么时候在其区域内进行变更检测,什么时候不能.为什么这有用?好吧,事实证明我们并不总是希望Angular能够神奇地执行变化检测。
正如前面提到的那样,Zones几乎可以通过浏览器修补任何全局异步操作。由于NgZone只是该区域的一个分支,它通知框架在异步操作发生时执行变更检测,因此当mousemove事件触发时它也会触发变更检测。我们可能不希望每次触发mousemove时都执行更改检测,因为它会降低我们的应用程序速度并导致非常糟糕的用户体验。
这就是为什么NgZone带有一个API runOutsideAngular(),它在NgZone的父区域中执行给定任务,该区域不会发出onTurnDone事件,因此不会执行更改检测。为了演示这个有用的功能,我们来看看下面的代码:
@Component({ selector: 'ng-zone-demo', template: ` <p>Progress: {{progress}}%</p> <p *ngIf="progress >= 100">Done processing {{label}} of Angular zone!</p> <button (click)="processWithinAngularZone()">Process within Angular zone</button> <button (click)="processOutsideOfAngularZone()">Process outside of Angular zone</button> ` }) export class NgZoneDemoComponent { progress: number = 0; label: string; constructor(private _ngZone: NgZone) {} // Loop inside the Angular zone // so the UI DOES refresh after each setTimeout cycle processWithinAngularZone() { this.label = 'inside'; this.progress = 0; this._increaseProgress(() => console.log('Inside Done!')); } // Loop outside of the Angular zone // so the UI DOES NOT refresh after each setTimeout cycle processOutsideOfAngularZone() { this.label = 'outside'; this.progress = 0; this._ngZone.runOutsideAngular(() => { this._increaseProgress(() => { // reenter the Angular zone and display done this._ngZone.run(() => { console.log('Outside Done!') }); }); }); } _increaseProgress(doneCallback: () => void) { this.progress += 1; console.log(`Current progress: ${this.progress}%`); if (this.progress < 100) { window.setTimeout(() => { this._increaseProgress(doneCallback); }, 10); } else { doneCallback(); } } }