ZoneJS 的原理與應用


目錄

  • 序言
  • Zone 是什么
  • ZoneJS 的原理
  • ZoneJS 的應用場景
  • 參考

1. 序言

ZoneJS 是 Angular 團隊受到 Dart 的 Zone 的啟發,為 Angular v2 及其以上版本設計的核心模塊。Angular 通過引入 ZoneJS 使得其變更檢測機制更加簡單與可靠。


2. Zone 是什么

在 ZoneJS 中有一個核心概念:Zone(域)。一個 Zone 表示一個 JavaScript 執行過程的上下文,其可以在異步任務之間進行持久性傳遞。

Zone 是執行上下文


先看一個示例:

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);

在上述代碼中:

  1. Zone.currentZone 上的一個靜態屬性,用來保存全局此刻正在使用的 Zone
  2. Zone.run() 方法將切換 Zone.current 所保存的 Zone
  3. 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 的樹形結構

從上圖中也可以看出,這些 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)
  });
});

最終執行結果:

onInvoke 示例執行結果


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 更新(變更檢測)。


5. 參考

學習 Zone.js

zone.js - 暴力之美

zone.js and NgZone

ZoneJS in Angular


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM