Angular ZoneJS 原理
Zone.js到底是如何工作的?
如果你閱讀過關於Angular 2變化檢測的資料,那麼你很可能聽說過zone。Zone是一個從Dart中引入的特性並被Angular 2內部用來判斷是否應該觸發變化檢測。如果你去到zone.js的GitHub頁面,你會發現它對Zone是這麼定義的:
Zone是一個在非同步任務間保持一致的執行環境。你可以把它理解成是JavaScriptVM的執行緒本地儲存。
第一次讀到這句話你可能會像我一樣摸不著頭腦。為了更好的理解它的含義,我推薦你觀看Brian Ford在ngConf 2014上的這個演講並閱讀thoughtram上的這篇理解zones。
然而,即使是在觀看了演講並閱讀了部落格文章以後,我還是對它實際的工作原理很好奇。Zone.js是如何給瀏覽器事件打上猴子補丁,那些github頁面上的例子又到底是如何工作的呢。本文旨在把我在調查過程中學到的知識分享出來。
瀏覽器事件是如何被打上猴子補丁的,這又意味著什麼呢?
為了瞭解瀏覽器事件是如何被打上猴子補丁的,我決定深入原始碼。以下是Zone.js啟動時執行邏輯的抽象程式碼片段。
function zoneAwareAddEventListener() {...}
function zoneAwareRemoveEventListener() {...}
function zoneAwarePromise() {...}
function patchTimeout() {...}
window.prototype.addEventListener = zoneAwareAddEventListener;
window.prototype.removeEventListener = zoneAwareRemoveEventListener;
window.prototype.promise = zoneAwarePromise;
window.prototype.setTimeout = patchTimeout;
注意:zone.js實際上給更多的事件打了補丁,由於原理相同在此處不一一列出。
原來zone.js覆寫了一些window原型上的函式,換之以一些代理函式。這意味著在載入zone.js指令碼之後出發的任何事件或是建立的任何promise都是被代理函式封裝過的。這個概念就叫做猴子補丁。
讓我們看一個例項
讓我們看看zone.js GitHub倉庫裡README檔案中的第一個示例
// 載入zone.js
Zone.current.fork({}).run(function () {
Zone.current.inTheZone = true;
setTimeout(function () {
console.log(‘in the zone: ‘ + !!Zone.current.inTheZone);
}, 0);
});
console.log(‘in the zone: ‘ + !!Zone.current.inTheZone);
如果執行這段程式碼,你會得到以下的結果:
‘in the zone: false‘
‘in the zone: true‘
你可能期望兩次輸出的結果都是true,因為我們在兩處輸出了同一個屬性。
為了理解這是如何工作的,我們需要把焦點聚集到這個程式碼片段的某些部分上。
在一個Zone中建立並執行程式碼
Zone.current.fork({}).run( .... );
當zone.js被載入時,它會建立一個可以用於訪問根Zone的全域性屬性。在這個例子中,我們通過fork根Zone Zone.current來建立一個Zone。我們在新建立的物件上執行run函式來在這個Zone內部執行某些程式碼。
在Zone中執行的函式
接下來讓我們看看這個在Zone中執行的函式:
....
Zone.current.inTheZone = true;
setTimeout(function () {
console.log(‘in the zone: ‘ + !!Zone.current.inTheZone);
}, 0);
....
這段程式碼首先在Zone.current屬性上增加了一個布林值。然後設定了一個定時器用來在呼叫棧被清空之後(如果你不太清楚我在說什麼,我推薦你看看這個分享)輸出這個新建立的屬性。
Zone之外的log語句
最後,同樣的log語句也在zone之外被執行了一次。
....
console.log(‘in the zone: ‘ + !!Zone.current.inTheZone);
我們同樣訪問了相同的Zone.current屬性。如果我們在兩條log語句中訪問了同一個屬性,為何輸出的結果會不一樣呢?
Zone的初始化和收尾程式碼
每次在Zone內部執行程式碼或是一個被打過猴子補丁的事件型別被觸發時,Zone或是代理函式都會在執行函式或回撥之前初始化Zone。代理函式之所以能初始化Zone是因為它保留了一個指向它被建立時所屬Zone的引用。
在初始化的過程中,與這個特定Zone相關的狀態都會被恢復,因此即使是定時器,事件監聽器這樣的非同步程式碼執行起來也像同步的程式碼一樣。你可以把Zone理解為一個在非同步任務之間保持一致的執行環境,就像定義裡說的那樣。
為了進一步澄清,請看看下面這個程式碼片段。我把程式碼按照它執行的順序重新整理並增加了初始化和收尾的時間點。註釋中有更多詳細資訊。
//載入Zone.js 這會給所有的瀏覽器時間打上補丁
Zone.current.fork({}).run(function () {
// 初始化Zone
// 觸發器: run函式被呼叫了。首先會初始化zone然後才會執行後續邏輯
// 動作:
// - Zone.current被設定為函式被執行時所屬的Zone。
// 在這裡,它就是我們fork根Zone生成的那個。
// 我們就叫它exampleZone吧。
// - Zone的生命週期裡的鉤子函式會被觸發(我們稍後會繼續討論)
// Zone.current上會多一個布林值屬性。在經歷了zone的初始化過程之後
// 此時的Zone.current指向的是exampleZone
Zone.current.inTheZone = true;
// 這裡註冊了一個定時器。由於被打過了猴子補丁,這裡呼叫的並不是
// 瀏覽器"預設"的timeout方法。因此,這裡實際上是在配置代理。這裡
// 要重點指出的是這個代理會保留一個指向建立時所屬Zone(這裡就是
// ‘exampleZone‘)的引用,稍後會用到這個引用。
setTimeout(
...., 0);
// 銷燬Zone
// 觸發器: 要在Zone中執行的函式已經執行完成
// 動作:
// - Zone.current屬性被重置為根Zone
// - Zone的生命週期裡的鉤子函式會被觸發
});
// log語句。Zone.current屬性目前指向的根Zone。
// 由於它並不知曉‘inTheZone‘屬性,因此會輸出false
console.log(‘in the zone: ‘ + !!Zone.current.inTheZone);
// 任務棧被清空了然後定時器的回撥函式開始執行
// 初始化Zone
// 觸發器: 被打過猴子補丁的事件被觸發了。proxy的包裝器會觸發一次
// Zone的初始化。要記得proxy包裝器保留了一個指向其被建立時所屬
// Zone的引用。
// 行為:
// - Zone.current屬性被設定為exampleZone
// - Zone的生命週期裡的鉤子函式會被觸發
function () {
// exampleZone包含‘inTheZone‘屬性,因此會輸出true
console.log(‘in the zone: ‘ + !!Zone.current.inTheZone);
}
// 銷燬Zone
// 觸發器: 定時器回撥函式執行完畢,proxy要執行一次Zone的銷燬流程
// 行為:
// - Zone.current屬性會被重置為根Zone
// - Zone的生命週期裡的鉤子函式會被觸發
多虧了針對事件的猴子補丁使得Zone.js可以在執行定時器回撥函式時初始化並銷燬Zone。
這麼解釋應該清楚一些了吧!
資源搜尋網站大全 https://www.renrenfan.com.cn 廣州VI設計公司https://www.houdianzi.com
Angular 2是如何利用Zone的?
為了瞭解Angular 2是如何利用Zone的,我檢視以下它的原始碼。請看下面這個程式碼片段:
....
new NgZoneImpl({
trace: enableLongStackTrace,
onEnter: () => {
// console.log(‘ZONE.enter‘, this._nesting, this._isStable);
this._nesting++;
if (this._isStable) {
this._isStable = false;
this._onUnstable.emit(null);
}
},
onLeave: () => {
this._nesting--;
// console.log(‘ZONE.leave‘, this._nesting, this._isStable);
this._checkStable();
},
setMicrotask: (hasMicrotasks: boolean) => {
this._hasPendingMicrotasks = hasMicrotasks;
this._checkStable();
},
setMacrotask: (hasMacrotasks: boolean) => { this._hasPendingMacrotasks = hasMacrotasks; },
onError: (error: NgZoneError) => this._onErrorEvents.emit(error)
});
....
這段程式碼來自NgZone.ts檔案。Zone.js暴露了一個Zone生命週期各階段的鉤子函式。這裡列出了Angular 2所監聽的事件。由於Angular 2中所有的程式碼都在同一個Zone中執行,也就是ngZOne, 因此Angular 2可以利用它的這些回撥函式來判斷何時該執行一次變更檢測迴圈。這避免了像Angular 1中那樣手動呼叫$digest。