Day 2:Angular Signal 是什麼?從 Change Detection 理解它的運作原理
4.3k 字,約 4 分鐘
2026 iThome 鐵人賽:Angular 22 Signal 進化論。 原文發表於 iThome,也可以在下方留言。
文章目錄
Angular 傳統 Change Detection
傳統 Angular 在監測資料變化時,通常會搭配 zone.js。像是 DOM 事件、Promise、Timer、HTTP 請求這些非同步操作發生時,zone.js 會幫忙攔截。
當這些操作執行完成後,Angular 就會知道:「現在資料有可能發生變化了」,接著啟動一輪 Change Detection,檢查畫面是不是需要更新。
變更偵測開始後,Angular 會重新檢查元件 Template 裡的資料綁定,看看目前的值和上一次是否不同。如果真的有變化,就會更新對應的畫面。
Angular 傳統的變更偵測策略主要可以分成:
- Eager:每次 Angular 執行
Change Detection,只要遍歷到這個元件,就會檢查它的 Template binding,確認資料是否發生變化。 - OnPush:Angular 22 起的預設變更偵測策略。它可以減少不必要的檢查,只有在 Angular 認為元件可能需要重新檢查時才會進行處理,例如
Input發生變化、元件內事件被觸發、透過markForCheck()標記,或AsyncPipe收到新的值。
從 Change Detection 到 Signal:Angular 的響應式機制演進
Signal 是 Angular 在響應式機制上的一個很重要的轉折點。對開發者來說,當我們在 Template 中使用一個響應式變數時,通常會很直覺地認為,只要這個值發生改變,畫面就會自動更新。
但實際上,不同框架背後採用的更新策略並不相同。要理解 Angular Signal,可以先拆成兩件事情來看:
- 我們怎麼知道這個
Signal被誰使用,也就是如何建立與追蹤依賴關係 - 當
Signal改變之後,我們怎麼知道哪些 View 可能受到影響
這兩件事情其實是分開處理的。Signal 主要負責精確追蹤資料之間的依賴關係。當 Template 在執行過程中讀取某個 Signal 時,Angular 會記錄這個 View 與 Signal 之間的依賴關係。
當 Signal 的值改變時,它不會直接操作 DOM,而是通知依賴這個 Signal 的 Consumer。Angular 接著會標記相關的 View 與祖先路徑,讓後續的 Change Detection 能沿著 View tree 找到這個可能受到影響的 View,再進一步判斷是否需要重新執行對應的 Template binding。
重新檢查時,Angular 會將這一次計算出的值與上一次的 binding value 進行比較,只有當值真的發生變化時,才會進一步更新實際的 DOM。
因此,Signal 解決的核心問題是「誰受到這次資料變更的影響」,而真正「DOM 要不要更新」這件事情,仍然會交由 Angular 的變更偵測機制處理。
Signal 與 Angular 的簡化流程
前面我們知道,Signal 並不是在資料改變時直接去操作 DOM,而是先找出「有哪些地方依賴這個 Signal」,再交給 Angular 後續的 Change Detection 處理。
如果把整個流程拆開來看,大致可以分成以下幾個步驟:
Step 1:Template 讀取 Signal:建立依賴關係(今天主要說明這個部分)
Step 2:Signal 發生變化:確認是否需要傳遞變更
Step 3:標記受到影響的 View 與祖先路徑
Step 4:Angular 執行 Change Detection
Step 5:比較 binding value 並更新 DOM
Step 1:Template 讀取 Signal:建立依賴關係
當 Angular Template 執行時,如果讀取了一個 Signal,Angular 會記錄這個 Signal 與目前 Template View 之間的依賴關係。
在這個響應式系統中,可以先理解兩個重要角色:
Producer:負責提供響應式資料,例如signal()。Consumer:負責讀取Producer的資料。當Consumer讀取Producer時,Angular 會建立兩者之間的依賴關係。
我們先以 AppComponent 為例:
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<p>目前數量:{{ count() }}</p>
`,
})
export class AppComponent {
count = signal(0);
}
當 Template 執行到 count() 時,Angular 會知道目前的 Template View 正在讀取 count 這個 Signal,並建立兩者之間的依賴關係。
這個過程可以簡化理解成:
count Signal
Producer
│
│ Template 執行 count()
▼
AppComponent Template View
Consumer
這是 Signal 更新流程的第一步:在讀取 Signal 時建立依賴關係。隨著不同的 Signal 被讀取,Angular 會持續記錄 Producer 與 Consumer 之間的關係,最後形成一張 Reactive Graph,用來描述整個響應式系統中的依賴關係。
PS:實際上 Angular 底層的依賴追蹤機制會更加複雜,不過目前可以先用 Producer、Consumer 與 Template View 之間的關係來理解整體概念。
從 Angular 原始碼角度來看
實際上,Angular 底層還會多一層 Reactive Consumer 的處理。Template View 並不是直接和 Signal 建立依賴,而是透過一個與 LView 關聯的 Consumer 來參與 Reactive Graph。
補充:
LView是 Angular 在執行期間,用來保存 View 狀態與相關執行資料的內部結構。
- 觀念 1:Angular 會先為目前的
LView取得對應的ReactiveLViewConsumer
export function getOrBorrowReactiveLViewConsumer(
lView: LView,
): ReactiveLViewConsumer {
return (
lView[REACTIVE_TEMPLATE_CONSUMER] ??
borrowReactiveLViewConsumer(lView)
);
}
如果這個 LView 已經有對應的 Consumer,就直接沿用;如果還沒有,Angular 會取得一個可用的 ReactiveLViewConsumer
因此,需要參與 Signal 依賴追蹤的 View,會透過 ReactiveLViewConsumer 加入 Reactive Graph,並由這個 Consumer 記錄目前 View 所依賴的 Signal。
當 count 這類 Signal Producer 發生變化時,Angular 就能沿著依賴關係找到對應的 Consumer,進而得知哪些 View 可能受到這次資料變更影響,而需要重新檢查。
- 觀念 2:這個
Consumer本身也會以ReactiveNode的形式存在於Reactive Graph中
ReactiveNode可以理解成Reactive Graph裡的一個節點,而Producer與Consumer則是這個節點在依賴關係中扮演的角色。有些節點只會是Producer或Consumer,也有些節點會同時具備兩種角色,例如computed()。
export const REACTIVE_LVIEW_CONSUMER_NODE = {
...REACTIVE_NODE,
consumerIsAlwaysLive: true,
kind: 'template',
consumerMarkedDirty: (node: ReactiveLViewConsumer) => {
markAncestorsForTraversal(node.lView!);
},
consumerOnSignalRead(this: ReactiveLViewConsumer): void {
this.lView![REACTIVE_TEMPLATE_CONSUMER] = this;
},
};
這邊有提供兩個 Hook:
- consumerOnSignalRead():當 Template 讀取
Signal時觸發,將目前的ReactiveLViewConsumer記錄到對應的LView上,讓 Angular 能在後續流程中取得這個 Consumer,持續參與Signal的依賴追蹤。 - consumerMarkedDirty():當
Consumer因為依賴的Producer發生變化而被標記為 dirty 時觸發。這個 Hook 會進一步呼叫markAncestorsForTraversal()標記祖先路徑,讓後續的Change Detection能沿著View tree找到這個可能需要重新檢查的 View。
第二個 Hook 特別重要,因為這個可能需要重新檢查的 View,可能位在一條原本會被 Change Detection 略過的分支中,例如祖先元件使用 OnPush,而這一輪本身沒有其他原因需要重新檢查。
透過這個 ReactiveNode,Angular 能在 Reactive Graph 中記錄 Consumer 與 Producer 的依賴關係,並在 Producer 發生變化時,找到對應的 Consumer,進一步銜接後續的 View 檢查流程。
整體關係可以先簡化成:
AppComponent LView
│
▼
ReactiveLViewConsumer
Consumer
│
│ depends on
▼
count Signal
Producer
所以實際參與 Reactive Graph 的,是與這個 View 關聯的 ReactiveLViewConsumer。透過這層關係,Angular 才能在 Signal 發生變化時,找到相關的 Consumer,並進一步判斷哪些 View 可能需要重新檢查。
資料來源
- Angular Signals 官方文件:說明
Signal、Reactive Context 與依賴追蹤的基本概念。 - Angular 原始碼:reactive_lview_consumer.ts:包含
ReactiveLViewConsumer、consumerMarkedDirty()、consumerOnSignalRead()等實作。 - Angular 原始碼:view_utils.ts:包含
markAncestorsForTraversal()與 View traversal 相關邏輯。 - Angular Signals Implementation:說明
Producer、Consumer、Reactive Graph、Push/Pull 等底層設計。
留言
留言使用 GitHub 帳號登入(Giscus),留言會存在 GitHub Discussions。