☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
angular 進階

⚡ 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 操作符(switchMapcombineLatestforkJoin)是處理複雜非同步流的利器
  • Signals 是新一代響應式 API,比 Zone.js 更精確
  • OnPush 變更偵測策略大幅提升效能
  • 虛擬滾動解決大量資料的渲染效能問題
  • Standalone Components 簡化了模組管理
  • Zone.js 透過攔截 JS 事件循環來偵測變化(底層機制)

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊