RxJS 觀察者模式在 LanguageService 中的應用
概述
LanguageService 使用 RxJS 的觀察者模式來處理語言狀態變化,這是一個經典的發佈-訂閱模式實現。
核心概念
觀察者模式 (Observer Pattern)
觀察者模式定義了一種一對多的依賴關係,讓多個觀察者對象同時監聽某一個主題對象。當主題對象的狀態發生變化時,所有依賴於它的觀察者都會得到通知並自動更新。
RxJS Subject
RxJS 的 Subject 是一個特殊的 Observable,它可以同時作為 Observable 和 Observer: - 作為 Observable: 可以被訂閱 - 作為 Observer: 可以發送數據給訂閱者
LanguageService 中的實現
1. language$ = new ReplaySubject<LangChangeEvent>(1)
字段含義
language$: 命名慣例,$表示這是一個 ObservableReplaySubject<LangChangeEvent>(1): 緩衝區大小為 1 的重播主體
ReplaySubject 特性
// ReplaySubject 會記住最後 N 個值
// 當有新的訂閱者時,會立即收到最近的 N 個值
language$ = new ReplaySubject<LangChangeEvent>(1);
為什麼使用 ReplaySubject?
- 狀態同步: 新組件訂閱時能立即獲取當前語言狀態
- 事件重播: 確保不會錯過語言變化事件
- 緩衝區控制: 只保留最新的語言狀態
2. 發佈事件
setLang(lang: string) {
// 當語言改變時,發佈事件給所有訂閱者
this.translateService.onLangChange.pipe(take(1)).subscribe(result => {
this.language$.next(result); // 發送事件
localStorage.setItem(LanguageService.LANGUAGE, lang);
});
this.translateService.use(lang);
}
訂閱語法比較
語法 1: subscribe()
// 只關心事件發生,不處理數據
languageService.language$.subscribe();
// 相當於
languageService.language$.subscribe({
next: () => {}, // 空處理函數
error: () => {}, // 默認錯誤處理
complete: () => {} // 默認完成處理
});
使用場景
- 只想觸發副作用(如日誌記錄)
- 測試 Observable 是否正常工作
- 簡單的事件監聽
語法 2: subscribe(event => { ... })
// 處理接收到的數據
languageService.language$.subscribe(event => {
console.log('Language changed:', event.lang);
});
// 相當於
languageService.language$.subscribe({
next: (event) => {
console.log('Language changed:', event.lang);
},
error: (err) => console.error('Error:', err),
complete: () => console.log('Completed')
});
使用場景
- 需要處理事件數據
- 執行業務邏輯
- 更新 UI 狀態
完整訂閱對象語法
languageService.language$.subscribe({
next: (event: LangChangeEvent) => {
// 處理正常事件
console.log('Language changed to:', event.lang);
this.updateUI(event.lang);
},
error: (error: any) => {
// 處理錯誤
console.error('Language change error:', error);
},
complete: () => {
// 處理完成(通常不會發生,因為 Subject 不會完成)
console.log('Language monitoring completed');
}
});
實際應用場景
組件中的使用
@Component({...})
export class AppComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
// 訂閱語言變化
this.languageService.language$
.pipe(takeUntil(this.destroy$)) // 防止記憶體洩漏
.subscribe(event => {
console.log('Language changed:', event.lang);
// 更新組件狀態
this.currentLang = event.lang;
// 重新載入數據
this.loadLocalizedData();
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
服務間通信
@Injectable()
export class TranslationService {
constructor(private languageService: LanguageService) {
// 監聽語言變化,重新載入翻譯
this.languageService.language$.subscribe(event => {
this.loadTranslations(event.lang);
});
}
}
ReplaySubject vs 其他 Subject
比較表
| Subject 類型 | 緩衝區 | 新訂閱者行為 | 使用場景 |
|---|---|---|---|
Subject |
無 | 只接收訂閱後的事件 | 簡單事件 |
BehaviorSubject |
1 | 立即收到最新值 | 狀態管理 |
ReplaySubject |
N | 收到最近 N 個值 | 事件歷史 |
AsyncSubject |
1 | 只在完成時收到最後值 | 最終結果 |
LanguageService 為什麼選 ReplaySubject?
// BehaviorSubject 也可以,但 ReplaySubject 更靈活
language$ = new BehaviorSubject<LangChangeEvent | null>(null);
// ReplaySubject 允許更複雜的初始化邏輯
language$ = new ReplaySubject<LangChangeEvent>(1);
記憶體管理
重要提醒
// ✅ 正確:使用 takeUntil 防止洩漏
private destroy$ = new Subject<void>();
ngOnInit() {
this.languageService.language$
.pipe(takeUntil(this.destroy$))
.subscribe(event => { ... });
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
// ❌ 錯誤:沒有取消訂閱會造成記憶體洩漏
ngOnInit() {
this.languageService.language$.subscribe(event => { ... });
}
總結
language$ 的角色
- 事件總線: 集中管理語言狀態變化
- 狀態同步: 確保所有組件獲取最新語言狀態
- 解耦通信: 服務和組件間的鬆耦合通信
訂閱語法選擇
subscribe(): 簡單事件觸發subscribe(callback): 數據處理和業務邏輯subscribe({next, error, complete}): 完整事件處理
這種設計實現了:
- 響應式編程: 聲明式處理狀態變化
- 組件解耦: 服務和組件間鬆耦合
- 狀態一致性: 所有訂閱者同步更新
- 記憶體安全: 正確的訂閱管理