Angular 之 zone.js 介紹

Zuckjet發表於2022-02-07

或許你聽說過 Angular 使用了zone.js, 但 Angular 為什麼要使用zone.js, 它能夠提供哪些功能呢?今天我們單獨寫一篇文章聊聊zone.js,關於它在 Angular 框架中發揮的作用將在下一篇文章講述。

什麼是 Zone ? 官方文件是這麼解釋的:Zone 是一個跨多個非同步任務的執行上下文。一句話總結來說,Zone 在攔截或追蹤非同步任務方面有著特別強大的能力。下面我們將通過一個示例來展示它的能力,並簡單剖析一下背後的工作原理。

<button id="b1">Bind Error</button>
<button id="b2">Cause Error</button>
<script>
  function main() {
    b1.addEventListener('click', bindSecondButton);
  }
  function bindSecondButton() {
    b2.addEventListener('click', throwError);
  }
  function throwError() {
    throw new Error('aw shucks');
  }
  main();
</script>

這是一個簡單的 HTML 頁面。頁面載入時會給第一個按鈕新增點選事件,其點選事件函式的功能是給第二個按鈕新增點選事件,而第二個按鈕的點選事件函式功能是丟擲一個異常。我們依次點選第一個按鈕和第二個按鈕,控制檯顯示如下:

(索引):26 Uncaught Error: aw shucks
    at HTMLButtonElement.throwError ((索引):26:13)

但是如果我們通過zone.js啟動執行程式碼,控制檯輸出會有什麼不同呢,我們先調整啟動程式碼:

 Zone.current.fork(
      {
        name: 'error',
        onHandleError: function (parentZoneDelegate, currentZone, targetZone, error) {
          console.log(error.stack);
        }
      }
    ).fork(Zone.longStackTraceZoneSpec).run(main);

此時控制檯輸出如下:

Error: aw shucks
    at HTMLButtonElement.throwError ((索引):26:13)
    at ZoneDelegate.invokeTask (zone.js:406:31)
    at Zone.runTask (zone.js:178:47)
    at ZoneTask.invokeTask [as invoke] (zone.js:487:34)
    at invokeTask (zone.js:1600:14)
    at HTMLButtonElement.globalZoneAwareCallback (zone.js:1626:17)
    at ____________________Elapsed_571_ms__At__Mon_Jan_31_2022_20_09_09_GMT_0800_________ (localhost)
    at Object.onScheduleTask (long-stack-trace-zone.js:105:22)
    at ZoneDelegate.scheduleTask (zone.js:386:51)
    at Zone.scheduleTask (zone.js:221:43)
    at Zone.scheduleEventTask (zone.js:247:25)
    at HTMLButtonElement.addEventListener (zone.js:1907:35)
    at HTMLButtonElement.bindSecondButton ((索引):23:10)
    at ZoneDelegate.invokeTask (zone.js:406:31)
    at Zone.runTask (zone.js:178:47)
    at ____________________Elapsed_2508_ms__At__Mon_Jan_31_2022_20_09_06_GMT_0800_________ (localhost)
    at Object.onScheduleTask (long-stack-trace-zone.js:105:22)
    at ZoneDelegate.scheduleTask (zone.js:386:51)
    at Zone.scheduleTask (zone.js:221:43)
    at Zone.scheduleEventTask (zone.js:247:25)
    at HTMLButtonElement.addEventListener (zone.js:1907:35)
    at main ((索引):20:10)
    at ZoneDelegate.invoke (zone.js:372:26)
    at Zone.run (zone.js:134:43)

通過對比我們知道:不引入zone.js時,我們通過錯誤呼叫棧僅僅能夠知道,異常是由按鈕2的點選函式丟擲。而引入了zone.js後,我們不僅知道異常是由按鈕2的點選函式丟擲,還知道它的點選函式是由按鈕1的點選函式繫結的,甚至能夠知道最開始的應用啟動是main函式觸發。這種能夠持續追蹤多個非同步任務的能力在大型複雜專案中異常重要,現在我們來看zone.js是如何做到的吧。

zone.js接管了瀏覽器提供的非同步 API,比如點選事件、計時器等等。也正是因為這樣,它才能夠對非同步操作有更強的控制介入能力,提供更多的能力。現在我們拿點選事件舉例,看看它是如何做到的吧。

proto[ADD_EVENT_LISTENER] = makeAddListener(nativeAddEventListener,..)

上述程式碼中,proto便指的是EventTarget.prototype,也就是說這行程式碼重新定義了addEventListener函式。我們繼續看看makeAddListener函式做了什麼。

function makeAddListener() {
  ......
  // 關鍵程式碼1
  nativeListener.apply(this, arguments);
  ......
  // 關鍵程式碼2
  const task = zone.scheduleEventTask(source, ...)
  ......
}

該函式主要做了兩件事,一是在自定義函式中執行瀏覽器本身提供的addEventListener函式,另外一個就是為每個點選函式安排了一個事件任務,這也是zone.js對非同步 API 有強大介入能力的重要因素。

現在我們再回到本文開頭的示例中,看看控制檯為什麼能夠輸出完整的完整的函式呼叫棧。剛剛我們分析過了makeAddListener函式,其中提到它為每個點選函式安排了一個事件任務,也就是zone.scheduleEventTask函式的執行。這個安排事件任務函式最終其實執行的是onScheduleTask:

onScheduleTask: function (..., task) {
  const currentTask = Zone.currentTask;
  let trace = currentTask && currentTask.data && currentTask.data[creationTrace] || [];
  trace = [new LongStackTrace()].concat(trace);
  task.data[creationTrace] = trace;
}

文章開頭控制檯輸出的完整的函式呼叫棧,儲存在currentTask.data[creationTrace]裡面,它是一個由LongStackTrace例項組成的陣列。每次有非同步任務發生時,onScheduleTask函式便把當前函式呼叫棧儲存記錄下來,我們看看類LongStackTrace的構造器就知道了:

class LongStackTrace {
    constructor() {
        this.error = getStacktrace();
        this.timestamp = new Date();
    }
}
function getStacktraceWithUncaughtError() {
    return new Error(ERROR_TAG);
}

this.error儲存的便是函式呼叫棧,getStacktrace函式通常呼叫的是getStacktraceWithUncaughtError函式,我們看到 new Error大概就能夠知道整個呼叫棧是如何得來的了。

本文分析的只是zone.js能力的一個示例,如果你希望瞭解更多功能可以參閱官方文件。通過這個示例,希望讀者能對zone.js有一個大概的認識,因為它也是 Angular 變更檢測不可或缺的基石。這方面的內容我將在下一篇文章中講解。歡迎關注我的個人微信公眾號【朱玉潔的部落格】,後續將帶來更多前端知識分享。

相關文章