ZoneJS 的原理與應用
目錄
- 序言
- Zone 是什麼
- ZoneJS 的原理
- ZoneJS 的應用場景
- 參考
1. 序言
ZoneJS 是 Angular 團隊受到 Dart 的 Zone 的啟發,為 Angular v2 及其以上版本設計的核心模組。Angular 通過引入 ZoneJS 使得其變更檢測機制更加簡單與可靠。
2. Zone 是什麼
在 ZoneJS 中有一個核心概念:Zone(域)。一個 Zone 表示一個 JavaScript 執行過程的上下文,其可以在非同步任務之間進行永續性傳遞。
先看一個示例:
import 'zone.js' const rootZone = Zone.current; const zoneA = rootZone.fork({name: 'A'}); expect(Zone.current).toBe(rootZone); setTimeout(function timeoutCb1() { // 此回撥在 rootZone 中執行 expect(Zone.current).toEqual(rootZone); }, 0); // 執行 run 方法,將切換 Zone.current 所儲存的 Zone zoneA.run(function run1() { expect(Zone.current).toEqual(zoneA); // setTimeout 在 zoneA 中被呼叫 setTimeout(function timeoutCb2() { // 此回撥在 zoneA 中執行 expect(Zone.current).toEqual(zoneA); }, 0); }); // 退出 zoneA.run 後,將切換回之前的 Zone expect(Zone.current).toBe(rootZone);
在上述程式碼中:
Zone.current
是Zone
上的一個靜態屬性,用來儲存全域性此刻正在使用的Zone
。Zone.run()
方法將切換Zone.current
所儲存的Zone
。- Zones 之間的關係:
最初的 rootZone
是 ZoneJS 預設建立的一個 Zone 例項。而通過 Zone.fork()
方法,可以再建立子Zone
(這也是一個 Zone 例項,因此可以繼續呼叫 fork()
方法建立子 Zone
,而其parent
屬性將關聯建立其的父 Zone),這些 Zones
最終可以形成一個樹形結構。
const rootZone = Zone.current; const zoneA = rootZone.fork({name: 'A'}); const zoneB = rootZone.fork({name: 'B'}); const zoneC = zoneA.fork({name: 'C'});
上述程式碼中的 Zones 之間的關係如下圖所示:
從上圖中也可以看出,這些 Zones 形成的樹形結構是一顆有唯一根節點的樹。
3. ZoneJS 的原理
ZoneJS 通過 Monkey patch (猴補丁)的方式,暴力地將瀏覽器或 Node 中的所有非同步 API 進行了封裝替換。
比如瀏覽器中的 setTimeout
:
let originalSetTimeout = window.setTimeout; window.setTimeout = function(callback, delay) { return originalSetTimeout(Zone.current.wrap(callback), delay); } Zone.prototype.wrap = function(callback) { // 獲取當前的 Zone let capturedZone = this; return function() { return capturedZone.runGuarded(callback, this, arguments); }; };
或者 Promise.then
方法:
let originalPromiseThen = Promise.prototype.then;
// NOTE: 這裡做了簡化,實際上 then 可以接受更多引數
Promise.prototype.then = function(callback) {
// 獲取當前的 Zone
let capturedZone = Zone.current;
function wrappedCallback() {
return capturedZone.run(callback, this, arguments);
};
// 觸發原來的回撥在 capturedZone 中
return originalPromiseThen.call(this, [wrappedCallback]);
};
簡單來說,ZoneJS 在載入時,對所有非同步介面進行了封裝,因此所有在 Zone 中執行的非同步方法都會被當做為一個 Task 被其統一監管,並且提供了相應的鉤子函式(hooks),用來在非同步任務執行前後或某個階段做一些額外的操作,因此可以實現:記錄日誌、監控效能、附加資料到非同步執行上下文中等。
而這些鉤子函式(hooks),其實就是通過Zone.fork()
方法來進行設定的,具體可以參考如下配置:
Zone.current.fork(zoneSpec) // zoneSpec 的型別是 ZoneSpec
// 只有 name 是必選項,其他可選
interface ZoneSpec {
name: string; // zone 的名稱,一般用於除錯 Zones 時使用
properties?: { [key: string]: any; } ; // zone 可以附加的一些資料,通過 Zone.get('key') 可以獲取
onFork: Function; // 當 zone 被 forked,觸發該函式
onIntercept?: Function; // 對所有回撥進行攔截
onInvoke?: Function; // 當回撥被呼叫時,觸發該函式
onHandleError?: Function; // 對異常進行統一處理
onScheduleTask?: Function; // 當任務進行排程時,觸發該函式
onInvokeTask?: Function; // 當觸發任務執行時,觸發該函式
onCancelTask?: Function; // 當任務被取消時,觸發該函式
onHasTask?: Function; // 通知任務佇列的狀態改變
}
舉一個onInvoke
的簡單列子:
let logZone = Zone.current.fork({
name: 'logZone',
onInvoke: function(parentZoneDelegate, currentZone, targetZone, delegate, applyThis, applyArgs, source) {
console.log(targetZone.name, 'enter');
parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source)
console.log(targetZone.name, 'leave'); }
});
logZone.run(function myApp() {
console.log(Zone.current.name, 'queue promise');
Promise.resolve('OK').then((value) => {console.log(Zone.current.name, 'Promise', value)
});
});
最終執行結果:
4. ZoneJS 的應用場景
ZoneJS 的應用場景有很多,例如:
- 可以用於開發除錯、錯誤記錄、分析和測試
- 可以讓框架知道什麼時候可以重新渲染(在 Angular 中的應用)
- 可以實現非同步 Task 跟蹤以及自動釋放和清理資源
- 等等
這裡舉幾個比較實用的例子:
1. 在測試中的應用:不允許非同步程式碼
const syncZoneSpec = {
name: 'SyncZone',
onScheduleTask: function() {
throw new Error('No Async work is allowed in test.'); // 如果存在非同步任務排程,將丟擲異常
}
}
function sync(fn) {
return function(...args) {
Zone.current.fork(syncZoneSpec).run(fn, args, this);
}
}
it('should fail when doing async', sync(() => {
Promise.resolve('value');
}));
上述實現可以用來保證測試的程式碼中沒有非同步方法被呼叫。
2. 用於效能分析:監聽非同步方法的執行時間
const executeTimeZoneSpec = {
name: 'executeTimeZone',
onScheduleTask: function (parentZoneDelegate, currentZone, targetZone, task) {
console.time('scheduleTask')
return parentZoneDelegate.scheduleTask(targetZone, task);
},
onInvokeTask: function (parentzone, currentZone, targetZone, task, applyThis, applyArgs) {
console.time('callback')
parentzone.invokeTask(targetZone, task, applyThis, applyArgs);
console.timeEnd('callback')
console.timeEnd('scheduleTask')
}
}
Zone.current.fork(executeTimeZoneSpec).run(() => {
setTimeout(function () {
console.log('start callback...')
for (let i = 0; i < 100; i++) {
console.log(i)
}
}, 1000);
});
// start callback...
// 0
// ...
// 100
// callback: 12.2890625ms
// scheduleTask: 1015.6650390625ms
在 JavaScript 中類似 setTimeout
這種非同步呼叫,其回撥執行的時機很難確定,想要直接監控其執行時間一般來說是比較苦難的,而通過引入 ZoneJS 則可以很容易實現這點。
3. 在框架中的應用:實現自動重新渲染
class VMTurnZoneSpec {
constructor(vmTurnDone) {
this.name = 'VMTurnZone';
this.vmTurnDone = vmTurnDone;
this.hasAsyncTask = false
}
onHasTask(delegate, current, target, hasTaskState) {
const { microTask, macroTask, eventTask } = hasTaskState
this.hasAsyncTask = microTask || macroTask || eventTask;
if (!this.hasAsyncTask) {
this.vmTurnDone();
}
}
onInvokeTask(parent, current, target, task, applyThis, applyArgs) {
try {
return parent.invokeTask(target, task, applyThis, applyArgs);
} finally {
if (!this.hasAsyncTask) {
this.vmTurnDone();
}
}
}
}
上述程式碼中的ZoneSpec
可以用來檢查非同步任務是否執行完畢,然後觸發對應的回撥方法。而像 Angular 這種框架,正是需要知道什麼時候所有的任務執行完畢以此來執行 DOM 更新(變更檢測)。