前言
Angular 16 引入了全新的響應式狀態管理工具 Signal,這是 Angular 在響應式程式設計上的重大突破。Signal 提供了更簡單、更高效的方式來管理應用程式的狀態,並且能夠自動追蹤變更。這篇文章將帶你認識 Signal 的基本概念和使用方式。
Signal 是什麼?
Signal 是 Angular 16 引入的一種新的響應式狀態管理工具,可以追蹤應用中的狀態變化,並在發生變化時自動更新依賴該狀態的元件。
Signal 本質上是一個物件(封裝你給的物件型別 Signal<T>),當 Signal 偵測到值變更時,所有依賴該值的部分都會被自動通知,觸發重新渲染。
基本使用方式
import { signal } from "@angular/core";
// 創建一個初始值為 0 的信號
const count: Signal<number> = signal(0);
// 讀取信號的值,要加上括號即可取值
console.log(count()); // Output: 0
// 更新信號的值
count.set(5);
console.log(count()); // Output: 5
// 另一種更新信號的值的方式,取得舊值,並返回新值
count.update((prev) => prev + 1);
console.log(count()); // Output: 6
Writable Signal 和 Read-Only Signal
Signal 分為兩種類型:可寫入的和唯讀的。
Writable Signal(可寫入 Signal)
Writable Signal 允許你直接更新它的值,通過 .set() 或 .update() 方法來改變信號的狀態。
const count = signal(0);
// 使用 set() 設置新值
count.set(3); // 設置新值為 3
// 使用 update() 基於舊值更新
count.update((value) => value + 1); // 將目前的值加 1
方法說明:
set(value):直接設定新值update(fn):基於當前值計算新值
Read-Only Signal(唯讀 Signal)
Read-Only Signal 依賴其他 Signal 的值,並且它的值無法被直接修改。使用 computed() 方法來定義這個 Signal。
const count = signal(2);
const doubleCount = computed(() => count() * 2); // 這個 signal 依賴 count 的值
console.log(doubleCount()); // Output: 4
// 當 count 改變時,doubleCount 會自動更新
count.set(5);
console.log(doubleCount()); // Output: 10
當 count 的值改變時,doubleCount 也會自動更新。
Signal 的應用場景
Angular 16 之後更新了 Signal 這個新特性,並且推薦使用 Signal 來管理應用的狀態。
1. 狀態管理
可以用來管理單一狀態,並將狀態變更自動反映在 UI 上。
export class UserComponent {
userName = signal('John');
updateName(newName: string): void {
this.userName.set(newName);
}
}
2. 減少訂閱複雜性
與 Observable 相比,Signal 不需要手動訂閱和管理訂閱的工作,不需要擔心記憶體洩漏問題。
// Observable 方式 - 需要手動管理訂閱
data$: Observable<string>;
subscription: Subscription;
ngOnInit() {
this.subscription = this.data$.subscribe(value => {
this.value = value;
});
}
ngOnDestroy() {
this.subscription.unsubscribe(); // 需要手動取消訂閱
}
// Signal 方式 - 自動管理
data = signal('initial value');
// 不需要訂閱,不需要取消訂閱
3. 提升性能
在使用 ChangeDetectionStrategy.OnPush 策略時,Signal 能讓元件僅在狀態變更時更新,提高應用性能。
@Component({
selector: 'app-optimized',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<p>Count: {{ count() }}</p>`
})
export class OptimizedComponent {
count = signal(0); // 只有 count 變化時才會觸發變更檢測
}
建立一個簡單的計數器
讓我們用 Signal 建立一個簡單的計數器範例。
需求:
- 建立 count Signal,並初始化為 0
- 使用按鈕更新 count 的值
- 顯示 count 的值並計算兩倍的值
import { Component, signal, computed } from '@angular/core';
@Component({
selector: "app-counter",
standalone: true,
template: `
<div class="counter">
<h2>Signal Counter</h2>
<p>Count: {{ count() }}</p>
<p>Double Count: {{ doubleCount() }}</p>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset</button>
</div>
`,
styles: [`
.counter {
padding: 20px;
border: 2px solid #ccc;
border-radius: 8px;
}
button {
margin: 5px;
padding: 8px 16px;
}
`]
})
export class CounterComponent {
// Writable Signal
count = signal(0);
// Computed Signal(自動根據 count 計算)
doubleCount = computed(() => this.count() * 2);
increment(): void {
this.count.update((value) => value + 1);
}
decrement(): void {
this.count.update((value) => value - 1);
}
reset(): void {
this.count.set(0);
}
}
Signal vs Observable
| 特性 | Signal | Observable |
|---|---|---|
| 訂閱管理 | 自動 | 手動 |
| 語法複雜度 | 簡單 | 較複雜 |
| 記憶體洩漏風險 | 低 | 需注意 |
| 變更檢測 | 精確追蹤 | 整個元件 |
| 異步操作 | 需配合其他工具 | 原生支援 |
| 學習曲線 | 平緩 | 較陡峭 |
進階範例:購物車
import { Component, signal, computed } from '@angular/core';
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
@Component({
selector: 'app-shopping-cart',
template: `
<div>
<h2>Shopping Cart</h2>
<div *ngFor="let item of items()">
<p>{{ item.name }} - ${{ item.price }} x {{ item.quantity }}</p>
</div>
<p><strong>Total: ${{ total() }}</strong></p>
<p>Items Count: {{ itemCount() }}</p>
</div>
`
})
export class ShoppingCartComponent {
// 購物車項目
items = signal<CartItem[]>([
{ id: 1, name: 'Product A', price: 100, quantity: 2 },
{ id: 2, name: 'Product B', price: 200, quantity: 1 }
]);
// 計算總金額
total = computed(() =>
this.items().reduce((sum, item) =>
sum + item.price * item.quantity, 0
)
);
// 計算總商品數
itemCount = computed(() =>
this.items().reduce((sum, item) =>
sum + item.quantity, 0
)
);
addItem(item: CartItem): void {
this.items.update(items => [...items, item]);
}
removeItem(id: number): void {
this.items.update(items =>
items.filter(item => item.id !== id)
);
}
}
總結
Angular Signal 為狀態管理帶來了革命性的改變:
- 簡單直觀:不需要複雜的訂閱管理
- 自動追蹤:依賴的值改變時自動更新
- 效能優異:精確的變更檢測,減少不必要的渲染
- 類型安全:完整的 TypeScript 支援
- 易於測試:簡單的 API,容易編寫測試
Signal 是 Angular 未來發展的重要方向,建議新專案優先考慮使用 Signal 來管理狀態。雖然 Observable 仍然有其適用場景(特別是複雜的異步操作),但 Signal 提供了更簡單、更高效的狀態管理方案。
參考資料