1. Change Detection 的演進:從全域檢查到精準更新
Angular 以及現行的其他前端框架,核心任務之一就是透過 Change Detection(變更檢測) 機制,來決定何時該更新網頁的 DOM Tree。
回顧歷史,早期的 AngularJS 採用的是「髒檢查(Dirty Checking)」機制來比對數值變化。到了 Angular (v2+),則改透過 Zone.js 來攔截非同步事件(如 Click、HTTP Request),在事件結束時觸發檢測。此外,雖然引入了 ChangeDetectionStrategy.OnPush 來縮減檢查範圍並提升速度,但當專案規模擴大時,仍面臨兩大核心挑戰:
- 由上而下的檢測成本:不可避免地需要從 Root Node 一路傳遞檢查至底層的 Child Nodes。
- 對 Zone.js 的強依賴:應用程式必須綁定 Zone.js 才能運作。
為了徹底解決上述問題,Angular 團隊推出了全新的解決方案——Signals。
舊時代的挑戰:Zone.js
Zone.js 就像是一個全域的「警報器」。它不知道「什麼」資料變了,只知道「有事情發生了」(例如:滑鼠點擊、定時器結束、API 回傳)。當警報響起,Angular 就必須檢查整個元件樹(Component Tree),確認是否有變數發生異動。
🏢 想像一棟公寓大樓: 如果其中一位住戶重新粉刷了廚房,檢查員(Angular + Zone.js)因為不知道是誰動了手腳,為了確保資料正確,只好去按「每一間」公寓的門鈴,逐一檢查大家的廚房是否變更了。這顯然非常沒有效率。
新時代的救星:Signals
Signals 在「資料(Data)」與「使用資料的地方(Template)」之間,建立了直接的依賴關係圖(Dependency Graph)。
當一個 Signal 的值改變時,Angular 能夠精準鎖定並只更新依賴該 Signal 的 DOM 節點。它會直接跳過其餘無關的元件,不再進行多餘的全域檢查。
2. Signal 基本用法
Signal 本質上是一個數值的包裝器 (Wrapper)。當內部的數值改變時,它會主動通知 Angular 進行更新。
import { signal } from '@angular/core';
// 1. 定義 (單一真值來源 / Source of Truth)
count = signal(0);
// 2. 讀取 (重要:永遠記得使用括號來呼叫!)
console.log(this.count());
如何更新數值 (Updating Values)
Signal 提供了兩種寫入數值的方法,視情境選擇:
set(value):完全取代 (Replacement)
this.count.set(5);
- 適用情境:當你不在乎舊數值,只想直接覆蓋時(例如:重置表單)
update(fn):基於原值的更新 (Evolution)
// current 代表當前的數值
this.count.update(current => current + 1);
- 適用情境:當新數值是透過「舊數值」計算而來時(例如:計數器 +1)。
衍生狀態 (Computed Signals)
根據其他的 Signal 自動推導出「新的資料」。它具備 Memoization (智慧快取) 特性:只有當依賴的來源 Signal 改變時,才會重新運算;否則會直接回傳快取值。 (注意:Computed Signal 是唯讀的)
import { computed, signal } from '@angular/core';
price = signal(100);
tax = signal(0.2);
// 當 price 或 tax 任一改變,total 就會自動更新
total = computed(() =>
this.price() * (1 + this.tax())
);
副作用 (Effect)
當 Signal 改變時,自動觸發「與外部世界互動」的操作。它不負責產出資料,而是負責執行行為(例如:寫入 LocalStorage、DOM 操作、記錄 Log)。
import { effect, signal } from '@angular/core';
count = signal(0);
name = signal('Gemini');
constructor() {
// 註冊 effect (必須在 Injection Context 中,如 constructor)
effect(() => {
// Angular 會自動追蹤依賴:偵測到使用了 count() 和 name()
// 只要其中一個改變,這個 block 就會重新執行
console.log(`目前的計數是: ${this.count()}, 用戶是: ${this.name()}`);
});
}
3.元件溝通的新標準 (Signal Inputs & Model)
除了內部狀態,Angular 也將 Signal 的概念延伸到了元件之間的溝通(Inputs/Outputs),讓資料流更加一致。
Signal Inputs (input)
新的 input() 函式取代了傳統的 @Input() 裝飾器。它回傳的是一個唯讀的 Signal,這意味著我們可以直接對 Input 使用 computed,而不再需要 ngOnChanges。
import { input, computed } from '@angular/core';
// 舊寫法: @Input() userId: string;
// 新寫法: 自動成為唯讀 Signal
userId = input.required<string>();
// 直接衍生新狀態,當 Input 改變時自動更新
welcomeMessage = computed(() => `Hello, User ${this.userId()}`);
雙向綁定 (model)
以前要實現雙向綁定需要 @Input 加上 @Output。現在只需要一個 model(),它是一個可讀也可寫的 Signal。
import { model } from '@angular/core';
// 定義一個可讀可寫的 Signal Input
checked = model(false);
toggle() {
// 修改它會自動通知父元件
this.checked.update(v => !v);
}
4. Signals vs RxJS:誰該負責什麼?
很多人會問:「有了 Signals,我還需要 RxJS 嗎?」 其實,它們就像是工具箱裡的「螺絲起子」與「扳手」,各有各的用途。
黃金法則:State vs Events
- Signals 是用來「保存數值 (State)」的 它就像一個水桶,隨時去撈都有水。只要你需要顯示在畫面上,或者是一個會變動的變數,選 Signals 就對了。它讓 Template 的寫法變得非常乾淨,不再需要
asyncpipe。 - RxJS 是用來「處理過程 (Events)」的 它就像一條河流 (Stream),強項在於控制水流。當你需要處理「時間」或「事件」時,RxJS 是王者。
- 使用情境:
- 想等使用者停止打字 300ms 後再搜尋?(Debounce)
- 想取消上一次還沒跑完的 API 請求?(SwitchMap)
- 這些都是 Signals 做不到,但 RxJS 能輕鬆解決的。
Angular 的新常態:Reactively Unified
未來的 Angular 開發模式將會是「雙劍合璧」:
- 用 RxJS 處理邏輯:在 Service 層處理複雜的 API 呼叫與資料流。
- 用 Signal 呈現結果:透過
toSignal把 Observable 轉成 Signal,讓 Component 簡單地「拿資料」。
範例:從 RxJS 到 Signal (toSignal)
import { toSignal } from '@angular/core/rxjs-interop';
// 1. 使用 RxJS 處理搜尋邏輯 (Debounce, SwitchMap)
const searchResults$ = this.searchTerm$.pipe(
debounceTime(300),
switchMap(term => this.apiService.search(term))
);
// 2. 轉成 Signal 給 Template 使用,初始值為空陣列
// Template 中直接使用 {{ results() }} 即可
results = toSignal(searchResults$, { initialValue: [] });
範例:從 Signal 到 RxJS (toObservable)
有時候我們需要監聽 Signal 的變化來觸發 API 請求,這時就需要反向轉換。
import { toObservable } from '@angular/core/rxjs-interop';
// 當 query 這個 signal 改變時,轉成 Stream 進行後續處理
query$ = toObservable(this.query);
this.query$.pipe(
debounceTime(300),
switchMap(q => this.api.search(q))
).subscribe();
5.總結
Signals 的引入,標誌著 Angular 進入了一個全新的「反應式文藝復興」時代。
它不僅解決了 Zone.js 長期以來的效能瓶頸,更透過「細粒度(Fine-grained)」的更新機制,為應用程式帶來了預設的高效能。同時,Signals 與 RxJS 的互補關係,讓我們在處理同步狀態與非同步事件時,擁有更清晰的邊界與工具選擇。
雖然學習新語法需要時間,但這項投資絕對是值得的。隨著 Angular 生態系逐漸向 Signal-based components 轉型,掌握 Signals 將不再只是選項,而是現代 Angular 開發者的必備技能。














