解決 Zone 汙染

edit

Zone.js 是一種信令機制,Angular 使用它來檢測應用狀態可能何時已更改。它會捕獲非同步操作,如 setTimeout、網路請求和事件監聽器。Angular 基於來自 Zone.js 的訊號來排程變更檢測。

在某些情況下,排程的 任務微任務 不會對資料模型進行任何更改,這使得執行變更檢測變得不必要。常見的例子有:

本節介紹如何識別此類別情況,以及如何在 Angular Zone 之外執行程式碼以避免不必要的變更檢測呼叫。

識別不必要的變更檢測呼叫

你可以使用 Angular DevTools 檢測不必要的變更檢測呼叫。它們通常在效能分析器的時序圖中顯示為連續的條形,其來源為 setTimeoutsetIntervalrequestAnimationFrame 或事件處理程式。當你的應用中對這些 API 的呼叫有限時,變更檢測呼叫通常是由第三方函式庫引起的。

Angular DevTools profiler preview showing Zone pollution

在上圖中,有一系列由與元素關聯的事件處理程式觸發的變更檢測呼叫。這是使用第三方非原生 Angular 元件時的一個常見挑戰,這些元件不會改變 NgZone 的預設行為。

NgZone 外部執行任務

在這種情況下,你可以指示 Angular 避免為使用 NgZone 的給定程式碼片段排程的任務呼叫變更檢測。

在 Zone 外部執行

1import { Component, NgZone, OnInit } from '@angular/core';23@Component(...)4class AppComponent implements OnInit {5  private ngZone = inject(NgZone);67  ngOnInit() {8    this.ngZone.runOutsideAngular(() => setInterval(pollForUpdates), 500);9  }10}

上面的程式碼片段指示 Angular 在 Angular Zone 之外呼叫 setInterval,並在 pollForUpdates 執行後跳過執行變更檢測。

當第三方函式庫的 API 在 Angular Zone 內被呼叫時,它們通常會觸發不必要的變更檢測週期。這種現象尤其會影響那些設定事件監聽器或啟動其他任務(如定時器、XHR 請求等)的函式庫。透過在 Angular Zone 之外呼叫函式庫 API 來避免這些額外的週期:

將繪圖初始化移到 Zone 外部

1import { Component, NgZone, OnInit } from '@angular/core';2import * as Plotly from 'plotly.js-dist-min';34@Component(...)5class AppComponent implements OnInit {6  private ngZone = inject(NgZone);78  ngOnInit() {9    this.ngZone.runOutsideAngular(() => {10      Plotly.newPlot('chart', data);11    });12  }13}

runOutsideAngular 中執行 Plotly.newPlot('chart', data); 指示框架在執行由初始化邏輯排程的任務後不應執行變更檢測。

例如,如果 Plotly.newPlot('chart', data) 向 DOM 元素新增事件監聽器,則 Angular 在執行其處理程式後不會執行變更檢測。

但有時,你可能需要監聽由第三方 API 排程的事件。在這種情況下,重要的是要記住,如果初始化邏輯在那裡完成,這些事件監聽器也將在 Angular Zone 之外執行:

檢查處理程式是否在 Zone 外部被呼叫

1import { Component, NgZone, OnInit, output } from '@angular/core';2import * as Plotly from 'plotly.js-dist-min';34@Component(...)5class AppComponent implements OnInit {6  private ngZone = inject(NgZone);78  plotlyClick = output<Plotly.PlotMouseEvent>();910  ngOnInit() {11    this.ngZone.runOutsideAngular(() => {12      this.createPlotly();13    });14  }1516  private async createPlotly() {17    const plotly = await Plotly.newPlot('chart', data);1819    plotly.on('plotly_click', (event: Plotly.PlotMouseEvent) => {20      // This handler will be called outside of the Angular zone because21      // the initialization logic is also called outside of the zone. To check22      // whether we're in the Angular zone, we can call the following:23      console.log(NgZone.isInAngularZone());24      this.plotlyClick.emit(event);25    });26  }27}

如果你需要向父元件排程事件並執行特定的檢視更新邏輯,你應該考慮重新進入 Angular Zone 以指示框架執行變更檢測或手動執行變更檢測:

當排程事件時,重新進入 Angular Zone

1import { Component, NgZone, OnInit, output } from '@angular/core';2import * as Plotly from 'plotly.js-dist-min';34@Component(...)5class AppComponent implements OnInit {6  private ngZone = inject(NgZone);78  plotlyClick = output<Plotly.PlotMouseEvent>();910  ngOnInit() {11    this.ngZone.runOutsideAngular(() => {12      this.createPlotly();13    });14  }1516  private async createPlotly() {17    const plotly = await Plotly.newPlot('chart', data);1819    plotly.on('plotly_click', (event: Plotly.PlotMouseEvent) => {20      this.ngZone.run(() => {21        this.plotlyClick.emit(event);22      });23    });24  }25}

也可能出現將事件分派到 Angular 區域之外的情況。重要的是要記住,觸發變更檢測(例如,手動觸發)可能會導致在 Angular 區域之外建立/更新檢視。