原文链接:IndepthApp
前言概览:注意区分NgZone和zone.js, 更多细节在Angular跟新策略篇,尚未翻译完成。
本文主要解释了Angular是如何基于zone.js实现NgZone。 同时阐述如何在不使用zone.js的情况下,实现手动更新。文章最后部分将描述自动跟新策略何时会失效。
我看过的大多数文章都将Zone(zone.js)和NgZone与Angular中的变更检测紧密关联在一起。尽管它们确实有关系,但从技术上讲,它们并不是一个整体的部分。是的,Zone和NgZone用于自动触发由异步操作引起的变更检测。但由于变更检测是一个单独的机制,它可以在没有Zone和NgZone的情况下成功工作。在第一章中,我将展示如何在没有zone.js的情况下使用Angular。文章的第二部分解释了Angular和zone.js如何通过NgZone进行互动。最后,我还会展示为什么自动变更检测有时无法与诸如Google API客户端库(gapi)等第三方库一起使用。
我已经撰写了许多关于Angular中变更检测的详细文章。如果您想全面了解变更检测的工作原理,我建议从阅读所有这些文章开始,其中包括本文。这5篇文章将使您成为Angular变更检测的专家。请注意,本文不涉及zone.js,而是关于Angular如何在实现NgZone时使用区域以及它与变更检测机制的关系。要了解更多关于区域的信息,请阅读我的Reverse-engineered Zones (zone.js)文章,其中包含了我所发现的内容。(IndepthApp)
为了证明Angular可以在没有Zone的情况下成功工作,我最初计划提供一个模拟的区域对象,它什么也不做。但是,即将发布的Angular 5版本为我简化了这些事情。现在,通过配置方式,它提供了一种使用不执行任何操作的noop Zone的方法。
feat(core): support for bootstrap with custom zone (#17672) · angular/angular@344a5ca · GitHub
为了做到这一点,首先让我们删除对zone.js的依赖。我将使用StackBlitz来演示应用程序,并且由于它使用Angular-CLI,所以我将从polyfills.ts文件中删除以下导入语句:
* Zone JS is required by Angular itself. */
import 'zone.js/dist/zone'; // Included with Angular CLI.
之后,我将像这样配置Angular来使用noop Zone的实现:
platformBrowserDynamic()
.bootstrapModule(AppModule, {
ngZone: 'noop'
});
如果您现在运行该应用程序(Angular (forked) - StackBlitz),您将看到更改检测是完全可操作的,并在DOM中呈现name组件属性。
现在尝试使用 setTimeout() 来更新属性:
export class AppComponent {
name = 'Angular 4';
constructor() {
setTimeout(() => {
this.name = 'updated';
}, 1000);
}
很明显没有触发更新,由于在前置配置中把ngZone设为 'noop'。但是我可以获取组件的引用(ApplicationRef )的 tick( ) 方法来手动触发更新。
export class AppComponent {
name = 'Angular 4';
constructor(app: ApplicationRef) {
setTimeout(()=>{
this.name = 'updated';
app.tick();
}, 1000);
}
好的更新成功。代码案例:Angular (forked) - StackBlitz
总结下来, 上面演示的重点是向你展示zone.js和NgZone并不是变更检测实现的一部分。这是一种非常方便的机制,可以通过调用app.tick()来自动触发变更检测,而不是在某些时机手动执行。我们一会儿会看到这些时机是什么。
此部分将演示了Zone提供的两个功能——上下文传播和出色的异步任务跟踪。Angular实现的NgZone类在很大程度上依赖于任务跟踪机制。
function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
zone._inner = zone._inner.fork({
name: 'angular',
...
zone.js的部分功能被保在 _inner 属性里面,通常被称为Angular zone。 这是你执行NgZone.run()时用来运行的回调部分:
run(fn, applyThis, applyArgs) {
return this._inner.run(fn, applyThis, applyArgs);
}
那么Angular拓展的zone.js的部分被保存在 _outer 属性中,并用于在执行NgZone.runOutsideAngular()时运行回调:
runOutsideAngular(fn) {
return this._outer.run(fn);
}
这种方法通常用于在Angular区域之外运行性能要求很高的操作,以避免不断触发变更检测。
NgZone有一个isStable属性,用来表示是否没有未完成的宏或微任务。它还定义了四个事件:
+------------------+-----------------------------------------------+
| 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(); }); }});
没错就是之前手动触发的方法 : app.tick( )
现在让我们看看NgZone是如何实现onMicrotaskEmpty事件的。事件由checkStable函数触发:
function checkStable(zone: NgZonePrivate) {
if (zone._nesting == 0 && !zone.hasPendingMicrotasks && !zone.isStable) {
try {
zone._nesting++;
zone.onMicrotaskEmpty.emit(null); look this line m???er fuck<-------------------
这个函数经常由三个钩子函数触发:
正如在关于Zone的文章中所解释的,当最后两个钩子被触发时,微任务队列可能会发生变化,所以Angular必须在每次触发钩子时运行稳定检查。onhasask钩子也用于执行检查,因为它跟踪整个队列的变化。
注:此章节的问题我个人暂时尚未遇见
stackoveflow中与变更检测相关的一个最常见的问题是,为什么有时在使用第三方库时,组件中的变更没有应用。这里有一个涉及Google API客户端库(gapi)的例子。解决这些问题的常见方法是在Angular zone中运行一个回调,如下所示:
gapi.load('auth2', () => {
zone.run(() => {
...
然而,一个有趣的问题是为什么Zone不注册请求,这导致在一个钩子中没有通知?如果没有通知,NgZone不会自动触发变更检测。
为了了解这一点,我只是深入研究了gapi的源代码,发现它使用JSONP来发出网络请求。这种方法不使用常见的AJAX API,如XMLHttpRequest或Fetch API,这些API由zone修补和跟踪。相反,它创建一个带有源URL的脚本标记,并定义一个全局回调,当从服务器获取带有数据的请求脚本时将触发该回调。zone无法修补或检测到这一点,因此框架对使用这种技术执行的请求保持不知情。
以下是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保存请求URL:
https://apis.google.com/_.../cb=gapi.loaded_0
URL的最后一段是gapi.loadd_0全局回调:
typeof gapi.loaded_0
“function”