Zone.js 是 Angular 用來偵測應用程式狀態可能已變更的一種訊號機制。它會擷取非同步操作,例如 setTimeout、網路請求與事件監聽器。Angular 會根據來自 Zone.js 的訊號,排程變更偵測。
在某些情況下,排程的工作或微工作並沒有對資料模型做任何變更,這時執行變更偵測就是不必要的。常見的例子有:
requestAnimationFrame、setTimeout或setInterval- 第三方程式庫排程的工作或微工作
本節涵蓋如何找出這種情況,以及如何讓程式碼在 Angular Zone 之外執行,以避免不必要的變更偵測呼叫。
找出不必要的變更偵測呼叫
你可以用 Angular DevTools 偵測不必要的變更偵測呼叫。它們在分析工具的時間軸中,經常以連續的長條出現,來源是 setTimeout、setInterval、requestAnimationFrame 或事件處理常式。當你的應用程式中呼叫這些 API 的地方有限時,變更偵測的叫用通常是由第三方程式庫造成的。

在上圖中,有一連串由與某個元素相關聯的事件處理常式觸發的變更偵測呼叫。在使用第三方的、非原生 Angular 元件時,這是常見的挑戰,因為它們不會改變 NgZone 的預設行為。
在 NgZone 之外執行工作
在這種情況下,你可以用 NgZone,指示 Angular 避免為某段程式碼所排程的工作呼叫變更偵測。
在 Zone 之外執行
1import { Component, NgZone, OnInit, inject } from '@angular/core';
2
3@Component(...)
4class App implements OnInit {
5 private ngZone = inject(NgZone);
6
7 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, inject } from '@angular/core';
2import * as Plotly from 'plotly.js-dist-min';
3
4@Component(...)
5class App implements OnInit {
6 private ngZone = inject(NgZone);
7
8 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 之外完成的,那些事件監聽器也會在 Angular Zone 之外執行:
檢查處理常式是否在 Zone 之外呼叫
1import { Component, NgZone, OnInit, output, inject } from '@angular/core';
2import * as Plotly from 'plotly.js-dist-min';
3
4@Component(...)
5class App implements OnInit {
6 private ngZone = inject(NgZone);
7
8 plotlyClick = output<Plotly.PlotMouseEvent>();
9
10 ngOnInit() {
11 this.ngZone.runOutsideAngular(() => {
12 this.createPlotly();
13 });
14 }
15
16 private async createPlotly() {
17 const plotly = await Plotly.newPlot('chart', data);
18
19 plotly.on('plotly_click', (event: Plotly.PlotMouseEvent) => {
20 // This handler will be called outside of the Angular zone because
21 // the initialization logic is also called outside of the zone. To check
22 // 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, inject } from '@angular/core';
2import * as Plotly from 'plotly.js-dist-min';
3
4@Component(...)
5class App implements OnInit {
6 private ngZone = inject(NgZone);
7
8 plotlyClick = output<Plotly.PlotMouseEvent>();
9
10 ngOnInit() {
11 this.ngZone.runOutsideAngular(() => {
12 this.createPlotly();
13 });
14 }
15
16 private async createPlotly() {
17 const plotly = await Plotly.newPlot('chart', data);
18
19 plotly.on('plotly_click', (event: Plotly.PlotMouseEvent) => {
20 this.ngZone.run(() => {
21 this.plotlyClick.emit(event);
22 });
23 });
24 }
25}
在 Angular Zone 之外派送事件的情境也可能發生。重要的是要記住,觸發變更偵測(例如手動觸發)可能會導致在 Angular Zone 之外建立/更新檢視。