Zone.js到底是如何工作的?
如果你閱讀過關於Angular 2變化檢測的資料,那么你很可能聽說過zone。Zone是一個從Dart中引入的特性並被Angular 2內部用來判斷是否應該觸發變化檢測。
如果你去到zone.js的GitHub頁面,你會發現它對Zone是這么定義的:
Zone是一個在異步任務間保持一致的執行環境。你可以把它理解成是JavaScript VM的線程本地存儲。
第一次讀到這句話你可能會像我一樣摸不着頭腦。為了更好的理解它的含義,我推薦你觀看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文件中的第一個示例(這里是該示例的plnkr。
// 加載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。
這么解釋應該清楚一些了吧!
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
。
原文鏈接 https://www.zcfy.cc/article/how-the-hell-does-zone-js-really-work