Angular 觀察者模式的現代實現方式

概述

雖然 RxJS 仍然是 Angular 的核心,但現代 Angular (v17+) 提供了更輕量級的替代方案來實現觀察者模式。本文比較不同實現方式的優缺點。

實現方式比較

1. 🎯 Angular Signals (推薦 - Angular 17+)

實現方式

import { Injectable, signal, computed, effect } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class LanguageService {

  // 信號作為狀態存儲
  private currentLanguage = signal<string>('en-us');

  // 計算信號 - 自動響應變化
  readonly language = this.currentLanguage.asReadonly();

  // 支持的語言列表
  readonly languageList = ['en-us', 'zh-tw', 'zh-cn'];

  constructor(private translateService: TranslateService) {
    // 效果 - 當語言改變時自動執行副作用
    effect(() => {
      const lang = this.currentLanguage();
      console.log('Language changed to:', lang);
      localStorage.setItem('language', lang);
      // 這裡可以執行其他副作用
    });
  }

  setLang(lang: string) {
    this.currentLanguage.set(lang);
    this.translateService.use(lang);
  }

  getCurrentLang(): string {
    return this.currentLanguage();
  }
}

使用方式

@Component({...})
export class AppComponent {
  // 在模板中直接使用
  currentLang = this.languageService.language;

  constructor(private languageService: LanguageService) {}

  changeLanguage(lang: string) {
    this.languageService.setLang(lang);
    // UI 會自動更新!
  }
}
<!-- 模板中直接綁定 -->
<p>Current language: {{ currentLang() }}</p>

優點

  • 零依賴: 不需要 RxJS
  • 自動響應: 模板自動更新
  • 類型安全: 完全類型化
  • 性能優化: Angular 內建優化
  • 簡單直觀: 類似 Vue 3 的響應式

缺點

  • Angular 17+ 限定: 無法在舊版本使用
  • 學習成本: 新概念需要適應

2. 📢 EventEmitter (簡單替代)

實現方式

import { Injectable, EventEmitter } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class LanguageService {

  private _currentLanguage = 'en-us';
  readonly languageList = ['en-us', 'zh-tw', 'zh-cn'];

  // 簡單的事件發射器
  languageChanged = new EventEmitter<string>();

  constructor(private translateService: TranslateService) {}

  setLang(lang: string) {
    this._currentLanguage = lang;
    this.translateService.use(lang);
    this.languageChanged.emit(lang); // 發射事件
  }

  getCurrentLang(): string {
    return this._currentLanguage;
  }
}

使用方式

@Component({...})
export class AppComponent implements OnInit, OnDestroy {

  currentLang = 'en-us';
  private subscription?: Subscription;

  ngOnInit() {
    // 手動訂閱
    this.subscription = this.languageService.languageChanged
      .subscribe(lang => {
        this.currentLang = lang;
        // 手動觸發變更檢測
        this.cdr.detectChanges();
      });
  }

  ngOnDestroy() {
    this.subscription?.unsubscribe();
  }
}

優點

  • 簡單: 容易理解
  • 輕量: 比 RxJS 簡單
  • Angular 原生: 不需要額外依賴

缺點

  • 手動管理: 需要手動訂閱/取消訂閱
  • 手動更新: 需要手動觸發變更檢測
  • 同步問題: 可能有時機問題

3. 🎪 原生 JavaScript CustomEvent

實現方式

@Injectable({
  providedIn: 'root'
})
export class LanguageService {

  private _currentLanguage = 'en-us';
  readonly languageList = ['en-us', 'zh-tw', 'zh-cn'];

  constructor(private translateService: TranslateService) {}

  setLang(lang: string) {
    this._currentLanguage = lang;
    this.translateService.use(lang);

    // 發射自定義事件
    const event = new CustomEvent('languageChanged', {
      detail: { language: lang }
    });
    window.dispatchEvent(event);
  }

  getCurrentLang(): string {
    return this._currentLanguage;
  }
}

使用方式

@Component({...})
export class AppComponent implements OnInit, OnDestroy {

  currentLang = 'en-us';

  ngOnInit() {
    // 監聽全局事件
    window.addEventListener('languageChanged', (event: any) => {
      this.currentLang = event.detail.language;
      this.cdr.detectChanges();
    });
  }

  ngOnDestroy() {
    window.removeEventListener('languageChanged', () => {});
  }
}

優點

  • 零依賴: 純 JavaScript
  • 全局性: 可以跨組件通信

缺點

  • 記憶體洩漏: 容易忘記移除監聽器
  • 類型不安全: 事件數據是 any
  • 調試困難: 事件流難以追蹤

4. 🔄 簡單的回調函數

實現方式

@Injectable({
  providedIn: 'root'
})
export class LanguageService {

  private _currentLanguage = 'en-us';
  private listeners: ((lang: string) => void)[] = [];
  readonly languageList = ['en-us', 'zh-tw', 'zh-cn'];

  constructor(private translateService: TranslateService) {}

  setLang(lang: string) {
    this._currentLanguage = lang;
    this.translateService.use(lang);

    // 通知所有監聽器
    this.listeners.forEach(listener => listener(lang));
  }

  getCurrentLang(): string {
    return this._currentLanguage;
  }

  // 註冊監聽器
  onLanguageChange(callback: (lang: string) => void) {
    this.listeners.push(callback);

    // 返回取消訂閱函數
    return () => {
      const index = this.listeners.indexOf(callback);
      if (index > -1) {
        this.listeners.splice(index, 1);
      }
    };
  }
}

使用方式

@Component({...})
export class AppComponent implements OnInit, OnDestroy {

  currentLang = 'en-us';
  private unsubscribe?: () => void;

  ngOnInit() {
    // 註冊監聽器
    this.unsubscribe = this.languageService.onLanguageChange(lang => {
      this.currentLang = lang;
      this.cdr.detectChanges();
    });
  }

  ngOnDestroy() {
    this.unsubscribe?.(); // 取消訂閱
  }
}

優點

  • 極簡: 最基本的實現
  • 控制: 完全控制訂閱邏輯

缺點

  • 重複代碼: 每個服務都要實現
  • 功能有限: 沒有 RxJS 的豐富操作符

性能和生態比較

實現方式 包大小 學習成本 功能豐富度 性能 推薦度
Signals ⭐⭐⭐⭐⭐
RxJS Subject ⭐⭐⭐⭐
EventEmitter ⭐⭐⭐
CustomEvent ⭐⭐
回調函數 ⭐⭐

實際建議

對於新專案 (Angular 17+)

// 推薦使用 Signals
private currentLanguage = signal<string>('en-us');
readonly language = this.currentLanguage.asReadonly();

對於現有專案

// 繼續使用 RxJS,但考慮逐步遷移到 Signals
language$ = new BehaviorSubject<string>('en-us');

對於簡單場景

// EventEmitter 足夠
languageChanged = new EventEmitter<string>();

遷移策略

從 RxJS 遷移到 Signals

// 舊代碼
@Injectable()
export class OldService {
  data$ = new BehaviorSubject<string>('initial');
  updateData(value: string) {
    this.data$.next(value);
  }
}

// 新代碼
@Injectable()
export class NewService {
  private data = signal<string>('initial');
  readonly dataSignal = this.data.asReadonly();

  updateData(value: string) {
    this.data.set(value);
  }
}

總結

現代 Angular 確實提供了更輕量級的觀察者模式實現:

  1. Signals 是最推薦的選擇 - 現代、類型安全、性能優良
  2. EventEmitter 適合簡單場景 - 輕量但功能有限
  3. RxJS 仍然強大 - 適合複雜的數據流處理

選擇取決於專案需求、Angular 版本和團隊熟悉度。 d:\wpg\shipment-notice-platform-web\angular-observer-patterns.md