以前我读到的大部分文章都将Zone(zone.js)以及NgZone同Angular中的变化检测紧密联系在一块儿。虽然说它们确实有关联,可是从技术角度来看,它们属于两个不一样的部分。毋庸置疑,Zone和NgZone被用来在异步操做以后自动触发框架内的变化检测,可是自动监测机制实际是能够抛开它们独立运行的。所以,在本文的第一章,我将展现在不使用zone.js的状况下如何使用Angular。而第二章将详细介绍Angular如何经过NgZone与zone.js打交道。在本文末尾,我会分析为什么有时候在使用像gapi这样的第三方库时,自动变化检测会失效。git
我以前写了不少深刻分析Angular中变化检测原理的文章,而本文是这一系列的最后一篇。若是你想全面了解变化检测的工做原理,我推荐你从这一篇 These 5 articles will make you an Angular Change Detection expert 开始去读一下这一系列文章。另外,须要说明的是,本文并不会详细介绍Zones(zone.js),而是旨在说明Angular经过构造NgZone去使用Zones的原理以及它们与变化检测机制之间的联系。若是想了解更多关于Zones的内容,能够阅读这篇文章:I reverse-engineered Zones (zone.js) and here is what I’ve found。github
最开始的时候,我想伪造一个空的zone对象来方便展现Angular能够在不使用Zone的时候正常工做。不过自动Angular v5发布以后,官方提供了一种更加简单的方式:经过配置noop Zone来中止Zone的工做。json
首先,咱们须要移除对zone.js的依赖。后面我将会用stackblitz进行演示。由于该网站上是经过Angular-CLI来创建项目的,因此我会先从polyfils.ts
(译者注:此处应该是polyfills.ts,估计是做者笔误)中将下面这段代码删掉bootstrap
* Zone JS is required by Angular itself. */
import 'zone.js/dist/zone'; // Included with Angular CLI.
复制代码
而后,按照下面的代码配置Angular去使用noop Zoneapi
platformBrowserDynamic()
.bootstrapModule(AppModule, {
ngZone: 'noop'
});
复制代码
此时若是你运行这个演示应用,能够看到变化检测是全面运行了的,而且组件的name
属性会成功地被渲染到DOM中。bash
接下来,咱们经过setTimeout
来对这个属性进行更新网络
export class AppComponent {
name = 'Angular 4';
constructor() {
setTimeout(() => {
this.name = 'updated';
}, 1000);
}
复制代码
能够看到,属性的变化并无更新到页面上。这一点是能够理解的,由于NgZone被停掉了,因此变化检测不会自动被触发。不过,咱们能够手动去触发检测来让它正常工做。具体的作法是,经过注入ApplicationRef服务而且调用它的tick
方法来开启变化检测:app
export class AppComponent {
name = 'Angular 4';
constructor(app: ApplicationRef) {
setTimeout(()=>{
this.name = 'updated';
app.tick();
}, 1000);
}
复制代码
如今这个演示应用中属性变化就会成功地更新到页面中了。框架
小结一下,上面这个演示是想说明,zone.js
还有特别是NgZone
,它们并不是变化检测实现逻辑的组成部分。只是相比于在特定时刻手动执行app.tick()
,经过自动调用的方式能够很方便地实现自动变化检测。下文立刻就将解释它们是怎么实现的。异步
在个人前一篇有关Zone(zone.js)的文章中曾深刻解释了Zone所提供的API及其内部运行机制。在该文章中,我解释了关于fork一个zone以及在特定zone中运行任务的核心概念。后面我将会用到这些概念。
另外,在那篇文章中,我还演示了Zone提供的两项功能——上下文传递以及待办异步任务追踪。而Angular所实现的NgZone服务则在很大程度上依赖于这个任务追踪机制。
NgZone本质上就是围绕着fork出的子zone进行的封装:
function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
zone._inner = zone._inner.fork({
name: 'angular',
...
复制代码
这个被fork出来的zone记录在_inner
属性中而且一般被叫作Angular Zone。当你使用NgZone.run()
方法时,实际也是经过这个zone来运行回调函数的:
run(fn, applyThis, applyArgs) {
return this._inner.run(fn, applyThis, applyArgs);
}
复制代码
而当前这个运行forking操做的zone则被记录在_outer
属性中。当你运行NgZone.runOutsideAngular()
时,实际使用的就是它:
runOutsideAngular(fn) {
return this._outer.run(fn);
}
复制代码
咱们一般使用这个方法在Angular Zone外执行一些性能消耗很大的操做,从而避免频繁地触发变化检测。
NgZone经过一个isStable
属性来代表当前是否还有待办的微任务与宏任务。此外,NgZone中还定义了下面四种不一样的事件:
+------------------+-----------------------------------------------+
| Event | Description |
+------------------+-----------------------------------------------+
| onUnstable | Notifies when code enters Angular Zone. |
| | This gets fired first on VM Turn. |
| | |
| onMicrotaskEmpty | Notifies when there is no more microtasks |
| | enqueued in the current VM Turn. |
| | This is a hint for Angular to do change |
| | detection which may enqueue more microtasks. |
| | For this reason this event can fire multiple |
| | times per VM Turn. |
| | |
| onStable | Notifies when the last `onMicrotaskEmpty` has |
| | run and there are no more microtasks, which |
| | implies we are about to relinquish VM turn. |
| | This event gets called just once. |
| | |
| onError | Notifies that an error has been delivered. |
+------------------+-----------------------------------------------+
复制代码
另外一方面,Angular在ApplicationRef中经过监听onMicrotaskEmpty
事件来触发变化检测:
this._zone.onMicrotaskEmpty.subscribe(
{next: () => { this._zone.run(() => { this.tick(); }); }});
复制代码
还记得咱们在前文中正是经过这个tick()
方法来实现应用的变化检测的。
如今咱们来看一下NgZone如何执行onMicrotaskEmpty
事件。这个事件在checkStable中会被触发:
function checkStable(zone: NgZonePrivate) {
if (zone._nesting == 0 && !zone.hasPendingMicrotasks && !zone.isStable) {
try {
zone._nesting++;
zone.onMicrotaskEmpty.emit(null); <-------------------
复制代码
而这个checkStable
方法又会被三个钩子触发:
在以前那篇介绍Zones的文章中咱们曾经分析过,后两个钩子被触发时,说明多是微任务队列中发生了变化(译者注:这里指的是单个微任务的变化)。所以,Angular须要在这个时候去运行stable
状态检测。另外,onHasTask
钩子则会用于在追踪到整个任务队列发生变化时(译者注:这里指的是整个任务队列全空或者有新任务进入空队列时,是一种更加宏观的监测)去运行状态检测。
在stackoverflow上有一个关于变化检测最多见的问题是为何有时在使用第三方库时组件中的数据变化并无实时展现出来。好比说这里就有一个关于gapi的例子。对这类问题一般的解决办法是,像以下代码同样,将回调函数放在Angular Zone中来运行:
gapi.load('auth2', () => {
zone.run(() => {
...
复制代码
不过,这个问题的有趣之处在于,为什么Angular Zone并无将这个请求登记在册,即为何gapi的请求操做没有触发上述任何一个钩子?正是由于没有收到消息,因此NgZone
才没能自动触发变化检测。
为了分析这个问题,我深刻研究了一下gapi
压缩后的源码,而后发现它是使用JSONP来进行网络请求的。这种方式并无使用常规的AJAX接口,像XMLHttpRequest或者Fetch API等,而Zones对后面这些API是打了补丁而且进行追踪了的。相反地,这种方式定义了一个script
的标签并为其指定源路径,而后定义了一个全局的回调函数。当被请求的script带着数据从服务端返回时会触发这个回调函数。Zones无法给这种方式打补丁或者进行检测,所以在Angular框架中只能对使用这种技术进行的网络请求漠不关心了。
下面这段就是gapi压缩版本中相关的代码,感兴趣的能够了解一下:
Ja = function(a) {
var b = L.createElement(Z);
b.setAttribute(“src”, a);
a = Ia();
null !== a && b.setAttribute(“nonce”, a);
b.async = “true”;
(a = L.getElementsByTagName(Z)[0]) ?
a.parentNode.insertBefore(b, a) :
(L.head || L.body || L.documentElement).appendChild(b)
}
复制代码
这里的变量Z
其实就是"script"
,而变量a
则记录了请求的地址:
https://apis.google.com/_.../cb=gapi.loaded_0
复制代码
该地址中的最后一段gapi.loaded_0
就是定义的全局回调函数了:
typeof gapi.loaded_0
“function” 复制代码
文章内容到此结束。感谢您的耐心阅读!