以前我讀到的大部分文章都將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」 複製代碼
文章內容到此結束。感謝您的耐心閱讀!