⚡ Angular 進階:RxJS、效能優化與 Signals
📌 RxJS 操作符
RxJS(Reactive Extensions for JavaScript)是 Angular 處理非同步資料流的核心工具。
常用操作符
import { of, from, interval, combineLatest, forkJoin } from 'rxjs';
import { map, filter, switchMap, debounceTime, distinctUntilChanged,
catchError, retry, tap, take } from 'rxjs/operators';
// map — 轉換資料(類似 Array.map)
of(1, 2, 3).pipe(
map(x => x * 10)
).subscribe(v => console.log(v)); // 10, 20, 30
// filter — 過濾資料(類似 Array.filter)
of(1, 2, 3, 4, 5).pipe(
filter(x => x % 2 === 0)
).subscribe(v => console.log(v)); // 2, 4
// switchMap — 切換到新的 Observable(最常用!取消前一個請求)
// 使用情境:搜尋框輸入時,只保留最新的搜尋結果
this.searchControl.valueChanges.pipe(
debounceTime(300), // 等使用者停止輸入 300ms
distinctUntilChanged(), // 值沒變就不發請求
switchMap(term => // 切換到新的搜尋請求(自動取消舊的)
this.searchService.search(term).pipe(
catchError(() => of([])) // 錯誤時回傳空陣列
)
)
).subscribe(results => {
this.searchResults = results;
});
// combineLatest — 組合多個 Observable 的最新值
combineLatest([
this.route.paramMap, // 路由參數
this.filterService.filters$ // 篩選條件
]).pipe(
switchMap(([params, filters]) => {
const id = params.get('categoryId');
return this.productService.getProducts(id, filters);
})
).subscribe(products => {
this.products = products;
});
// forkJoin — 等所有 Observable 都完成(類似 Promise.all)
forkJoin({
user: this.userService.getUser(userId),
orders: this.orderService.getOrders(userId),
preferences: this.prefService.getPreferences(userId)
}).subscribe(({ user, orders, preferences }) => {
// 三個請求都完成後才執行
this.user = user;
this.orders = orders;
this.preferences = preferences;
});
📌 Angular Signals(新版響應式 API)
Signals 是 Angular 16+ 引入的新響應式系統,比傳統的 Zone.js 更精確、更高效。
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<h2>計數器:{{ count() }}</h2>
<p>雙倍值:{{ doubleCount() }}</p>
<button (click)="increment()">+1</button>
<button (click)="reset()">重置</button>
`
})
export class CounterComponent {
// signal — 可讀可寫的響應式值
count = signal(0);
// computed — 根據其他 signal 計算的衍生值(自動追蹤依賴)
doubleCount = computed(() => this.count() * 2);
// effect — 當依賴的 signal 變化時自動執行副作用
logEffect = effect(() => {
console.log(`計數器變為:${this.count()}`);
});
increment() {
this.count.update(v => v + 1); // 基於目前值更新
// 也可以用 this.count.set(10); 直接設定
}
reset() {
this.count.set(0);
}
}
💡 Signal vs Observable:
- Signal 是同步的,適合管理 UI 狀態
- Observable 是非同步的,適合處理事件流和 HTTP 請求
- 兩者可以互相轉換:
toSignal()和toObservable()
📌 變更偵測策略 OnPush
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
@Component({
selector: 'app-product-card',
changeDetection: ChangeDetectionStrategy.OnPush, // 👈 啟用 OnPush
template: `
<div class="card">
<h3>{{ product.name }}</h3>
<p>NT$ {{ product.price }}</p>
</div>
`
})
export class ProductCardComponent {
@Input() product!: Product;
// OnPush 策略下,只有當 @Input() 的「參考」改變時才會重新渲染
// 如果只是修改物件的屬性(mutation),Angular 不會偵測到變化
// 所以搭配 OnPush 時,要用**不可變資料**(Immutable Data)
}
預設 vs OnPush 對比
預設策略(Default):
任何事件 → 檢查所有元件 → 效能差(大型應用會卡頓)
OnPush 策略:
只在以下情況重新渲染:
1. @Input() 的參考改變
2. 元件內的事件觸發
3. Observable 發出新值(配合 async pipe)
4. Signal 值改變
→ 效能好!只更新需要更新的元件
📌 虛擬滾動(Virtual Scrolling)
// 需要安裝 @angular/cdk
// npm install @angular/cdk
import { ScrollingModule } from '@angular/cdk/scrolling';
@Component({
selector: 'app-big-list',
standalone: true,
imports: [ScrollingModule],
template: `
<!-- 即使有 10,000 筆資料,DOM 上只會渲染可見的幾十筆 -->
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let item of items" class="item">
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
`,
styles: [`
.viewport { height: 400px; }
.item { height: 50px; }
`]
})
export class BigListComponent {
items = Array.from({ length: 10000 }, (_, i) => ({ name: `項目 ${i + 1}` }));
}
📌 Standalone Components(無模組化)
Angular 14+ 引入了 Standalone Components,不再需要 NgModule:
// 傳統模組方式(舊)
@NgModule({
declarations: [AppComponent, HeaderComponent, FooterComponent],
imports: [BrowserModule, FormsModule, HttpClientModule],
bootstrap: [AppComponent]
})
export class AppModule { }
// Standalone 方式(新,推薦)
@Component({
selector: 'app-root',
standalone: true, // 👈 宣告為獨立元件
imports: [HeaderComponent, FooterComponent, RouterOutlet], // 直接匯入依賴
template: `
<app-header />
<router-outlet />
<app-footer />
`
})
export class AppComponent { }
📌 Zone.js 與 JavaScript 事件循環
Angular 傳統上使用 Zone.js 來偵測狀態變化。它的原理是攔截所有非同步操作:
JavaScript 事件循環(Event Loop)
┌──────────────────────────────────────┐
│ Call Stack(呼叫堆疊) │
│ ↓ 執行同步程式碼 │
├──────────────────────────────────────┤
│ Web APIs │
│ ↓ setTimeout、HTTP 請求、DOM 事件 │
├──────────────────────────────────────┤
│ Task Queue(任務佇列) │
│ ↓ 非同步回呼排隊等待 │
├──────────────────────────────────────┤
│ Zone.js 在這裡攔截! │
│ ↓ 每當非同步操作完成,Zone.js 通知 │
│ Angular 執行變更偵測 │
└──────────────────────────────────────┘
// Zone.js 攔截的操作包括:
// - setTimeout / setInterval
// - Promise.then
// - addEventListener(DOM 事件)
// - XMLHttpRequest / fetch
// - WebSocket
// 這就是為什麼你改一個變數,UI 就自動更新的原因!
// Zone.js 知道「有非同步操作完成了」→ 觸發變更偵測 → 更新 DOM
// Angular 18+ 支援 Zoneless 模式(不再依賴 Zone.js)
// 配合 Signals 使用,效能更好
📌 小結
- RxJS 操作符(
switchMap、combineLatest、forkJoin)是處理複雜非同步流的利器 - Signals 是新一代響應式 API,比 Zone.js 更精確
- OnPush 變更偵測策略大幅提升效能
- 虛擬滾動解決大量資料的渲染效能問題
- Standalone Components 簡化了模組管理
- Zone.js 透過攔截 JS 事件循環來偵測變化(底層機制)