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 確實提供了更輕量級的觀察者模式實現:
- Signals 是最推薦的選擇 - 現代、類型安全、性能優良
- EventEmitter 適合簡單場景 - 輕量但功能有限
- RxJS 仍然強大 - 適合複雜的數據流處理
選擇取決於專案需求、Angular 版本和團隊熟悉度。