前言

Angular 16 引入了全新的響應式狀態管理工具 Signal,這是 Angular 在響應式程式設計上的重大突破。Signal 提供了更簡單、更高效的方式來管理應用程式的狀態,並且能夠自動追蹤變更。這篇文章將帶你認識 Signal 的基本概念和使用方式。

Signal 是什麼?

SignalAngular 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

特性SignalObservable
訂閱管理自動手動
語法複雜度簡單較複雜
記憶體洩漏風險需注意
變更檢測精確追蹤整個元件
異步操作需配合其他工具原生支援
學習曲線平緩較陡峭

進階範例:購物車

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 提供了更簡單、更高效的狀態管理方案。

參考資料