簡析 Angular NgZone

Eric Li
hefemk
Published in
Mar 4, 2023

使用 Angular 或其他當代前端框架/函式庫,不外乎想透過它來簡化開發,而其中「繫結」(binding) 則是要角,使繫結得以運作背後需有一列的努力,這包含變動偵測,以及決定何時偵測。

何時變動偵測

什麼時候偵測變動才是合理的?讓我們先思考有什麼操作該引發變動偵測。在 Angular Repo 當中 zone.md 已經整理需要偵測的時機,摘要如下:

  1. Component 初始化
  2. EventTask 如 element.addEventListener()
  3. HTTP 請求,如呼叫 Web API 進行操作
  4. MacroTasks,如 setTimeout()、setInterval()
  5. MicroTasks,如 Promose.then()
  6. 其他非同步操作,如 WebSocket.onmessage()、Canvas.toBlob()

我們可以觀察到,除了 Component 初始化是明確可預期的,其他多數操作都是非同步的,這正是 NgZone 的舞台,Angular 借其知曉非同步的操作是否完成,將它作為變動偵測的時機點。這些操作都期待某種狀態變化,適合作為通用的變動偵測時機。

當然,有通用就有特例,開發人員仍然可以注入 ChangeDetectorRef,透過呼叫 .detectChanges() 強制進行變動偵測,以滿足更細微的控制。

若有一些操作是我們不希望它參與變動偵測的,則可以注入 NgZone,將想要排除的操作執行在 runOutsideAngular() 當中。

constructor(private ngZone: NgZone) {}

ngOnInit() {
this.ngZone.runOutsideAngular(() => {
setTimeout(() => {
console.log('Hello');
}, 5000);
});
}

從何開始

我們可以在 @angular/core 當中找到 application_ref.ts 原始碼,並觀察到 ApplicationRef 在建構函數訂閱了 NgZone.onMicrotaskEmpty,它會在沒有任何 microtask 時發出通知,進而呼叫了 tick(),而 tick() 裡面輾轉呼叫了 detectChanges()

constructor(
private _zone: NgZone,
private _injector: EnvironmentInjector,
private _exceptionHandler: ErrorHandler,
) {
this._onMicrotaskEmptySubscription = this._zone.onMicrotaskEmpty.subscribe({
next: () => {
this._zone.run(() => {
this.tick();
});
}
});
// ......
}

補充:Zone.js

NgZone 建基在 Zone.js 之上,它這樣介紹 Zone

A Zone is an execution context that persists across async tasks. You can think of it as thread-local storage for JavaScript VMs.

Zone.js 監聽與介入一些非同步操作如 DOM events、setTimeout()、setInterval()、XMLHttpRequest 等 (具體參考 STANDARD-APIS.md),有了這些介入,開發人員將可以在這些事情發生前、發中後執行額外的邏輯,如同 AOP 一般。

--

--