# RxJS 学习指南

> 基于 Angular + RxJS,面向零基础读者。所有代码示例均可在本项目 `rxjs-demo/` 目录中直接运行。

---

## 目录

1. [核心概念](#1-核心概念)
2. [Observable 基础](#2-observable-基础)
3. [pipe + 基础 operators](#3-pipe--基础-operators)
4. [Subject 三兄弟](#4-subject-三兄弟)
5. [switchMap — 切换流](#5-switchmap--切换流)
6. [mergeMap — 并行流](#6-mergemap--并行流)
7. [forkJoin — 等所有完成](#7-forkjoin--等所有完成)
8. [combineLatest — 多流联动](#8-combinlatest--多流联动)
9. [catchError — 错误处理](#9-catcherror--错误处理)
10. [scan — 累加状态](#10-scan--累加状态)
11. [interval + takeUntil — 定时器与取消](#11-interval--takeuntil--定时器与取消)
12. [内存泄漏防范](#12-内存泄漏防范)
13. [Angular 中的最佳实践](#13-angular-中的最佳实践)
14. [常见错误速查](#14-常见错误速查)

---

## 1. 核心概念

### 心智模型:水管比喻

```
数据源(水源)  →  pipe(管道/过滤器)  →  subscribe(水龙头)
  of(1,2,3)          map / filter             console.log
```

| 概念 | 类比 | 说明 |
|------|------|------|
| **Observable** | 水管 | 描述数据如何流动,不订阅就不流 |
| **Observer** | 水龙头 | 接收数据的三个回调:`next` / `error` / `complete` |
| **subscribe()** | 拧开水龙头 | 触发执行,开始接收数据 |
| **pipe()** | 串联的过滤器 | 数据依次经过每个 operator |
| **operator** | 单个过滤器 | `map` / `filter` / `switchMap` 等 |
| **Subject** | 广播站 | 既是 Observable 又可以手动推送值 |
| **Subscription** | 水管连接 | `subscribe()` 的返回值,调用 `.unsubscribe()` 断开 |

### 同步 vs 异步

RxJS 统一了同步和异步的编程模型:

```typescript
// 同步 —— 立即执行完
of(1, 2, 3).subscribe(v => console.log(v));
// 立即输出: 1, 2, 3

// 异步 —— 延迟/持续执行
interval(1000).subscribe(v => console.log(v));
// 每秒输出: 0, 1, 2, 3, ...
```

---

## 2. Observable 基础

### 2.1 创建 Observable

```typescript
import { Observable, of, from, interval, timer } from 'rxjs';

// of:把多个值包装成 Observable(同步)
const nums$ = of(1, 2, 3, 4, 5);

// from:把数组 / Promise / 可迭代对象转成 Observable
const arr$ = from(['apple', 'banana', 'cherry']);
const promise$ = from(fetch('/api/data'));

// interval:每隔 n 毫秒发一个递增数字(从0开始)
const tick$ = interval(1000);   // 0, 1, 2, 3...

// timer:延迟 n 毫秒后发一个值,或延迟后再按间隔发
const delayed$ = timer(2000);              // 2秒后发 0,然后完成
const delayedInterval$ = timer(1000, 500); // 1秒后开始,每500ms发一个
```

### 2.2 subscribe 的三个回调

```typescript
nums$.subscribe({
  next:     (value) => console.log('收到值:', value),
  error:    (err)   => console.error('发生错误:', err),
  complete: ()      => console.log('流已完成'),
});

// 简写(只关心 next)
nums$.subscribe(value => console.log(value));
```

### 2.3 Observable 的特性

```typescript
const obs$ = new Observable<number>(subscriber => {
  console.log('开始执行');
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  subscriber.complete();
});

// 不订阅,"开始执行" 不会打印 —— 懒执行
obs$.subscribe(v => console.log(v)); // 这时才执行

// 每次 subscribe 都是独立执行
obs$.subscribe(v => console.log('第二次:', v)); // 再次执行
```

**命名约定**:Observable 变量习惯以 `$` 结尾,如 `user$`、`data$`。

---

## 3. pipe + 基础 operators

### 3.1 pipe 的作用

`pipe()` 把多个 operator 串联起来,数据按顺序经过每一个:

```typescript
import { of } from 'rxjs';
import { map, filter, tap, take } from 'rxjs/operators';

of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
  .pipe(
    tap(v => console.log('原始值:', v)),  // 只观察,不改变
    filter(v => v % 2 === 0),             // 过滤:只保留偶数
    map(v => v * 100),                    // 变换:乘以100
    take(3),                               // 只取前3个,然后自动完成
  )
  .subscribe(v => console.log('最终:', v));

// 输出:
// 原始值: 1
// 原始值: 2  → 最终: 200
// 原始值: 3
// 原始值: 4  → 最终: 400
// 原始值: 5
// 原始值: 6  → 最终: 600(take(3) 满足,完成)
```

### 3.2 常用基础 operators

| Operator | 作用 | 示例 |
|----------|------|------|
| `map` | 变换每个值 | `map(x => x * 2)` |
| `filter` | 过滤值 | `filter(x => x > 0)` |
| `tap` | 观察(调试用),不改变值 | `tap(x => console.log(x))` |
| `take(n)` | 只取前 n 个值,然后完成 | `take(5)` |
| `skip(n)` | 跳过前 n 个值 | `skip(2)` |
| `first()` | 只取第一个值 | `first()` |
| `last()` | 只取最后一个值(流完成后) | `last()` |
| `distinctUntilChanged` | 值没变就不发 | `distinctUntilChanged()` |
| `debounceTime(ms)` | 停止发值 n ms 后才触发 | `debounceTime(300)` |
| `delay(ms)` | 延迟 n ms 后才发 | `delay(500)` |

```typescript
// debounceTime 最常见场景:搜索框防抖
inputChange$.pipe(
  debounceTime(300),       // 用户停止输入 300ms 才触发
  distinctUntilChanged(),  // 输入内容没变就不重复请求
).subscribe(keyword => searchApi(keyword));
```

---

## 4. Subject 三兄弟

### 4.1 Subject

普通 Subject:没有初始值,晚来的订阅者收不到之前的值。

```typescript
import { Subject } from 'rxjs';

const event$ = new Subject<string>();

event$.subscribe(v => console.log('A:', v));
event$.subscribe(v => console.log('B:', v));

event$.next('click');  // A: click, B: click
event$.next('hover');  // A: hover, B: hover

// 晚来的订阅者 C,收不到前两条
event$.subscribe(v => console.log('C:', v));
event$.next('focus');  // A: focus, B: focus, C: focus

event$.complete(); // 通知所有订阅者流已结束
```

**使用场景**:组件间事件总线、手动触发某个操作。

### 4.2 BehaviorSubject ⭐(最常用)

有初始值,任何新订阅者立即收到当前值。**Angular Service 跨组件共享状态的标准做法。**

```typescript
import { BehaviorSubject } from 'rxjs';

// ─── service 文件 ───
@Injectable({ providedIn: 'root' })
export class UserStateService {
  // 私有,外部不能直接调用 next()
  private _userName$ = new BehaviorSubject<string>('游客');

  // 公开只读视图
  readonly userName$ = this._userName$.asObservable();

  // 读当前值(不需要订阅)
  get currentUserName(): string {
    return this._userName$.getValue();
  }

  login(name: string) { this._userName$.next(name); }
  logout()            { this._userName$.next('游客'); }
}

// ─── 组件文件 ───
@Component({
  template: `<p>当前用户:{{ userName$ | async }}</p>`
})
export class HeaderComponent {
  userStateService = inject(UserStateService);
  userName$ = this.userStateService.userName$;
}
```

```typescript
// 演示 BehaviorSubject 的"立即给当前值"特性
const count$ = new BehaviorSubject<number>(0);

count$.subscribe(v => console.log('订阅者A:', v)); // 立即输出: 0

count$.next(1); // A: 1
count$.next(2); // A: 2

// 晚来的订阅者 B,立即收到当前值 2
count$.subscribe(v => console.log('订阅者B:', v)); // 立即输出: 2

count$.next(3); // A: 3, B: 3
```

### 4.3 ReplaySubject

缓冲最近 N 条记录,新订阅者可以"重放"历史。

```typescript
import { ReplaySubject } from 'rxjs';

const log$ = new ReplaySubject<string>(3); // 缓冲最近3条

log$.next('消息1');
log$.next('消息2');
log$.next('消息3');
log$.next('消息4');
log$.next('消息5');

// 晚来的订阅者收到最近3条:消息3, 消息4, 消息5
log$.subscribe(v => console.log('重放:', v));
```

**使用场景**:消息记录、日志、用户操作历史。

### 4.4 三者对比

| | 初始值 | 新订阅者立即收到 | 场景 |
|-|--------|----------------|------|
| **Subject** | 无 | 无 | 事件总线 |
| **BehaviorSubject** | 必须有 | 当前值(1条) | **状态管理**(最常用) |
| **ReplaySubject(n)** | 无 | 最近 n 条 | 消息历史 |

---

## 5. switchMap — 切换流

**核心特点**:收到新值时,**取消上一次**的内部 Observable,切换到新的。

```typescript
import { switchMap } from 'rxjs/operators';

// 最典型场景:搜索框
// 用户快速输入 "a" → "an" → "ang",只关心最后一次 "ang" 的结果
searchInput$.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(keyword => http.get(`/api/search?q=${keyword}`)),
  //        ↑ 上一个请求还没回来,新的来了,自动取消旧的
).subscribe(results => this.results = results);
```

```typescript
// 演示取消效果
const click$ = new Subject<void>();

click$.pipe(
  switchMap(() => {
    console.log('发起新请求,取消旧请求');
    return of('结果').pipe(delay(1000));
  })
).subscribe(v => console.log(v));

click$.next(); // 发起请求
click$.next(); // 立即取消上一个,发起新请求
click$.next(); // 立即取消上一个,发起新请求
// 只有最后一个请求的结果会被处理
```

**何时用 switchMap**:只关心最新结果,旧的结果无意义时(搜索、路由切换、下拉选择触发查询)。

---

## 6. mergeMap — 并行流

**核心特点**:不取消旧的,所有内部 Observable **同时运行**。

```typescript
import { mergeMap } from 'rxjs/operators';
import { from } from 'rxjs';

// 同时发3个请求,谁先回来就先处理谁
from([1, 2, 3]).pipe(
  mergeMap(id => http.get(`/api/user/${id}`))
).subscribe(user => console.log('用户:', user));
// 请求1、2、3 同时发出,结果顺序不固定
```

```typescript
// switchMap vs mergeMap 对比
const ids$ = from([1, 2, 3]);

// switchMap:3个请求,前两个被取消,只有第3个的结果
ids$.pipe(switchMap(id => http.get(`/user/${id}`))).subscribe(...);

// mergeMap:3个请求全部发出,3个结果都处理
ids$.pipe(mergeMap(id => http.get(`/user/${id}`))).subscribe(...);
```

**何时用 mergeMap**:每个请求都需要处理结果,且顺序无所谓时(批量上传、并发加载)。

---

## 7. forkJoin — 等所有完成

等待多个 Observable **全部完成**后,把最后的值合并在一起发出。类似 `Promise.all()`。

```typescript
import { forkJoin } from 'rxjs';

// 页面初始化:需要3个接口都返回后才渲染
forkJoin({
  user:        http.get<User>('/api/user'),
  config:      http.get<Config>('/api/config'),
  permissions: http.get<string[]>('/api/permissions'),
}).subscribe({
  next: ({ user, config, permissions }) => {
    // 三个接口全部成功才到这里
    this.user = user;
    this.config = config;
    this.permissions = permissions;
    this.isLoaded = true;
  },
  error: (err) => {
    // 任意一个出错,整个 forkJoin 都进入 error
    console.error('加载失败:', err);
  }
});

// 并行 vs 串行的时间对比:
// 串行:500ms + 300ms + 400ms = 1200ms
// forkJoin 并行:max(500ms, 300ms, 400ms) = 500ms ✅
```

**注意**:如果其中一个 Observable 永不完成(如 Subject),forkJoin 也永远不会发值。

---

## 8. combineLatest — 多流联动

任意一个流发出新值时,用**所有流的最新值**组合在一起发出。

```typescript
import { combineLatest, BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';

// 场景:价格、数量、折扣任一变化,实时计算总价
const price$    = new BehaviorSubject<number>(100);
const quantity$ = new BehaviorSubject<number>(1);
const discount$ = new BehaviorSubject<number>(0);

combineLatest([price$, quantity$, discount$]).pipe(
  map(([price, qty, disc]) => price * qty * (1 - disc / 100))
).subscribe(total => console.log('总价:', total));

price$.next(200);    // 总价: 200  (200 × 1 × 1)
quantity$.next(3);   // 总价: 600  (200 × 3 × 1)
discount$.next(10);  // 总价: 540  (200 × 3 × 0.9)
```

```typescript
// 另一个场景:表单多个字段联合校验
combineLatest([password$, confirmPassword$]).pipe(
  map(([pwd, confirm]) => pwd === confirm),
).subscribe(isMatch => this.passwordMatch = isMatch);
```

**forkJoin vs combineLatest**| | 触发时机 | 适用场景 |
|-|---------|---------|
| `forkJoin` | 所有流都**完成**后触发一次 | 页面初始化并行加载 |
| `combineLatest` | 任一流发值就触发,**持续** | 多个状态联动计算 |

---

## 9. catchError — 错误处理

```typescript
import { catchError } from 'rxjs/operators';
import { of, throwError } from 'rxjs';

// 基本用法:捕获错误,返回默认值,流继续
http.get('/api/data').pipe(
  catchError(err => {
    console.error('请求失败:', err.message);
    return of([]);  // 用空数组兜底,流不中断
  })
).subscribe(data => this.data = data);

// 错误后重新抛出(上层处理)
http.get('/api/critical').pipe(
  catchError(err => {
    this.logErrorToServer(err);
    return throwError(() => err);  // 继续向上传递错误
  })
).subscribe({
  next: data => ...,
  error: err => this.showErrorDialog(err)  // 在这里最终处理
});
```

```typescript
// retry:自动重试
import { retry } from 'rxjs/operators';

http.get('/api/data').pipe(
  retry(3),           // 失败后最多重试3次
  catchError(err => of([]))
).subscribe(...);
```

---

## 10. scan — 累加状态

`scan` 像数组的 `reduce`,区别是**每一步都发出中间值**,非常适合维护累计状态。

```typescript
import { scan } from 'rxjs/operators';
import { Subject } from 'rxjs';

// 场景1:购物车数量
const cartAction$ = new Subject<number>();

cartAction$.pipe(
  scan((total, change) => Math.max(0, total + change), 0)
).subscribe(count => this.cartCount = count);

cartAction$.next(1);   // count: 1
cartAction$.next(1);   // count: 2
cartAction$.next(-1);  // count: 1
cartAction$.next(3);   // count: 4
```

```typescript
// 场景2:消息列表追加(不用每次重建数组)
const newMessage$ = new Subject<string>();

newMessage$.pipe(
  scan((messages, msg) => [...messages, msg], [] as string[])
).subscribe(messages => this.messages = messages);

newMessage$.next('你好');          // ['你好']
newMessage$.next('在吗?');        // ['你好', '在吗?']
newMessage$.next('有什么事吗?'); // ['你好', '在吗?', '有什么事吗?']
```

---

## 11. interval + takeUntil — 定时器与取消

```typescript
import { interval, Subject } from 'rxjs';
import { takeUntil, take, map } from 'rxjs/operators';

// takeUntil:当另一个 Observable 发出值时,自动停止
const stop$ = new Subject<void>();

interval(1000).pipe(
  takeUntil(stop$),          // stop$.next() 被调用时停止
  take(60),                   // 或者最多60次后自动停止
  map(i => `第 ${i + 1} 秒`),
).subscribe({
  next:     v  => console.log(v),
  complete: () => console.log('定时器已停止'),
});

// 5秒后停止
setTimeout(() => stop$.next(), 5000);
```

---

## 12. 内存泄漏防范

**不取消订阅 = 内存泄漏**,是 RxJS 在 Angular 中最常见的问题。

### 方法一:takeUntil(推荐)

```typescript
@Component({ ... })
export class MyComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    // 每个订阅都加 takeUntil(this.destroy$)
    someStream$.pipe(
      takeUntil(this.destroy$)
    ).subscribe(...);

    anotherStream$.pipe(
      takeUntil(this.destroy$)
    ).subscribe(...);

    // interval 之类的不会自动完成的流,必须这样处理
    interval(1000).pipe(
      takeUntil(this.destroy$)
    ).subscribe(...);
  }

  ngOnDestroy() {
    // 一行代码,取消所有订阅
    this.destroy$.next();
    this.destroy$.complete();
  }
}
```

### 方法二:async pipe(模板中自动管理)

```typescript
// 组件
@Component({
  template: `
    <p>{{ user$ | async | json }}</p>
    <li *ngFor="let item of list$ | async">{{ item }}</li>
  `
})
export class MyComponent {
  user$ = this.userService.getUser();   // 不需要手动订阅
  list$ = this.dataService.getList();   // async pipe 自动取消订阅
}
```

**能用 `async pipe` 的地方优先用 `async pipe`**,它完全不需要手动管理订阅。

### 方法三:take(1)(只需要一次的场景)

```typescript
// 只需要获取一次初始数据,不需要持续监听
this.userService.getUser().pipe(
  take(1)  // 收到第一个值后自动完成并取消订阅
).subscribe(user => this.user = user);
```

---

## 13. Angular 中的最佳实践

### 13.1 Service 状态管理模板

```typescript
@Injectable({ providedIn: 'root' })
export class ProductService {
  private http = inject(HttpClient);

  // 用 BehaviorSubject 维护状态
  private _products$ = new BehaviorSubject<Product[]>([]);
  private _loading$  = new BehaviorSubject<boolean>(false);
  private _error$    = new BehaviorSubject<string | null>(null);

  // 对外只暴露只读 Observable
  readonly products$ = this._products$.asObservable();
  readonly loading$  = this._loading$.asObservable();
  readonly error$    = this._error$.asObservable();

  loadProducts(): void {
    this._loading$.next(true);
    this._error$.next(null);

    this.http.get<Product[]>('/api/products').pipe(
      catchError(err => {
        this._error$.next(err.message);
        return of([]);
      }),
    ).subscribe(products => {
      this._products$.next(products);
      this._loading$.next(false);
    });
  }
}
```

### 13.2 组件订阅模板

```typescript
@Component({
  template: `
    @if (loading$ | async) { <p>加载中...</p> }
    @if (error$ | async; as err) { <p>错误: {{ err }}</p> }
    @for (p of products$ | async; track p.id) {
      <div>{{ p.name }}</div>
    }
  `
})
export class ProductListComponent implements OnInit {
  private productService = inject(ProductService);
  private destroy$ = new Subject<void>();

  // async pipe 绑定
  products$ = this.productService.products$;
  loading$  = this.productService.loading$;
  error$    = this.productService.error$;

  ngOnInit() { this.productService.loadProducts(); }
  ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }
}
```

### 13.3 HTTP 请求的标准写法

```typescript
// ✅ 推荐:在 service 里处理逻辑,组件只订阅结果
// service
getUserById(id: number): Observable<User> {
  return this.http.get<User>(`/api/user/${id}`).pipe(
    map(resp => resp),          // 可以做数据变换
    catchError(this.handleError) // 统一错误处理
  );
}

// ✅ 推荐:搜索防抖
searchUsers(term$: Observable<string>): Observable<User[]> {
  return term$.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap(term => this.http.get<User[]>(`/api/users?q=${term}`)),
    catchError(() => of([]))
  );
}
```

---

## 14. 常见错误速查

### ❌ 忘记取消订阅

```typescript
// ❌ 错误:interval 永远不会自动完成,组件销毁后仍在运行
ngOnInit() {
  interval(1000).subscribe(v => this.count = v);
}

// ✅ 正确
ngOnInit() {
  interval(1000).pipe(
    takeUntil(this.destroy$)
  ).subscribe(v => this.count = v);
}
```

### ❌ 嵌套 subscribe

```typescript
// ❌ 错误:嵌套 subscribe,无法管理内部订阅,容易泄漏
this.route.params.subscribe(params => {
  this.http.get(`/api/user/${params.id}`).subscribe(user => {
    this.user = user;
  });
});

// ✅ 正确:用 switchMap 展平
this.route.params.pipe(
  switchMap(params => this.http.get(`/api/user/${params.id}`)),
  takeUntil(this.destroy$)
).subscribe(user => this.user = user);
```

### ❌ 在 BehaviorSubject 上直接暴露

```typescript
// ❌ 错误:外部可以直接 next(),破坏封装
@Injectable()
export class UserService {
  public user$ = new BehaviorSubject<User | null>(null); // 危险!
}

// ✅ 正确:私有 Subject + 公开只读 Observable
@Injectable()
export class UserService {
  private _user$ = new BehaviorSubject<User | null>(null);
  readonly user$ = this._user$.asObservable();

  setUser(user: User) { this._user$.next(user); }
}
```

### ❌ 用 mergeMap 做搜索

```typescript
// ❌ 错误:用 mergeMap 做搜索,旧请求的结果会覆盖新结果
searchInput$.pipe(
  mergeMap(kw => http.get(`/search?q=${kw}`)) // 结果顺序不确定
).subscribe(results => this.results = results);

// ✅ 正确:用 switchMap,自动取消旧请求
searchInput$.pipe(
  switchMap(kw => http.get(`/search?q=${kw}`))
).subscribe(results => this.results = results);
```

### ❌ forkJoin 传入不会完成的 Observable

```typescript
// ❌ 错误:BehaviorSubject 永远不会完成,forkJoin 永远不会发值
const subject = new BehaviorSubject(0);
forkJoin([subject, http.get('/api')]).subscribe(...); // 永远不触发

// ✅ 正确:用 take(1) 让它完成一次
forkJoin([subject.pipe(take(1)), http.get('/api')]).subscribe(...);
```

---

## 附录:operators 速查表

### 变换类

| Operator | 说明 |
|----------|------|
| `map(fn)` | 逐个变换值 |
| `scan(fn, seed)` | 累加,每步都发出中间值 |
| `reduce(fn, seed)` | 累加,只在完成时发出最终值 |
| `switchMap(fn)` | 切换内部流,取消旧的 |
| `mergeMap(fn)` | 展平内部流,全部并行 |
| `concatMap(fn)` | 展平内部流,串行排队 |
| `exhaustMap(fn)` | 忽略新值,直到当前内部流完成 |

### 过滤类

| Operator | 说明 |
|----------|------|
| `filter(fn)` | 按条件过滤 |
| `take(n)` | 取前 n 个 |
| `takeUntil(obs$)` | 直到另一个流发值才停止 |
| `skip(n)` | 跳过前 n 个 |
| `debounceTime(ms)` | 停止发值 n ms 后触发 |
| `throttleTime(ms)` | n ms 内只取第一个 |
| `distinctUntilChanged` | 值没变不重复发 |
| `first()` | 只取第一个 |

### 组合类

| Operator | 说明 |
|----------|------|
| `forkJoin([...])` | 等所有完成,取各自最后一个值 |
| `combineLatest([...])` | 任一变化,取所有最新值 |
| `merge(...)` | 多个流合并,谁发就处理谁 |
| `concat(...)` | 多个流串行,前一个完成才开始下一个 |
| `zip(...)` | 多个流一一配对 |

### 错误处理类

| Operator | 说明 |
|----------|------|
| `catchError(fn)` | 捕获错误,返回备用 Observable |
| `retry(n)` | 出错后自动重试 n 次 |
| `retryWhen(fn)` | 自定义重试策略 |
| `finalize(fn)` | 流完成或出错时都执行(类似 finally) |

---

*文件位置:`src/app/rxjs-demo/RXJS_GUIDE.md`*
*配套代码:`src/app/rxjs-demo/rxjs-demo.service.ts`(可运行的12个示例函数)*
*交互演示:`src/app/rxjs-demo/rxjs-demo.component.ts`(启动项目后访问 `/rxjs-demo`)*

 

# RxJS 学习指南

> 基于 Angular + RxJS,面向零基础读者。所有代码示例均可在本项目 `rxjs-demo/` 目录中直接运行。

---

## 目录

1. [核心概念](#1-核心概念)
2. [Observable 基础](#2-observable-基础)
3. [pipe + 基础 operators](#3-pipe--基础-operators)
4. [Subject 三兄弟](#4-subject-三兄弟)
5. [switchMap — 切换流](#5-switchmap--切换流)
6. [mergeMap — 并行流](#6-mergemap--并行流)
7. [forkJoin — 等所有完成](#7-forkjoin--等所有完成)
8. [combineLatest — 多流联动](#8-combinlatest--多流联动)
9. [catchError — 错误处理](#9-catcherror--错误处理)
10. [scan — 累加状态](#10-scan--累加状态)
11. [interval + takeUntil — 定时器与取消](#11-interval--takeuntil--定时器与取消)
12. [内存泄漏防范](#12-内存泄漏防范)
13. [Angular 中的最佳实践](#13-angular-中的最佳实践)
14. [常见错误速查](#14-常见错误速查)

---

## 1. 核心概念

### 心智模型:水管比喻

```
数据源(水源)  →  pipe(管道/过滤器)  →  subscribe(水龙头)
  of(1,2,3)          map / filter             console.log
```

| 概念 | 类比 | 说明 |
|------|------|------|
| **Observable** | 水管 | 描述数据如何流动,不订阅就不流 |
| **Observer** | 水龙头 | 接收数据的三个回调:`next` / `error` / `complete` |
| **subscribe()** | 拧开水龙头 | 触发执行,开始接收数据 |
| **pipe()** | 串联的过滤器 | 数据依次经过每个 operator |
| **operator** | 单个过滤器 | `map` / `filter` / `switchMap` 等 |
| **Subject** | 广播站 | 既是 Observable 又可以手动推送值 |
| **Subscription** | 水管连接 | `subscribe()` 的返回值,调用 `.unsubscribe()` 断开 |

### 同步 vs 异步

RxJS 统一了同步和异步的编程模型:

```typescript
// 同步 —— 立即执行完
of(1, 2, 3).subscribe(v=>console.log(v));
// 立即输出: 1, 2, 3

// 异步 —— 延迟/持续执行
interval(1000).subscribe(v=>console.log(v));
// 每秒输出: 0, 1, 2, 3, ...
```

---

## 2. Observable 基础

### 2.1 创建 Observable

```typescript
import{ Observable, of, from, interval, timer }from'rxjs';

// of:把多个值包装成 Observable(同步)
constnums$ = of(1, 2, 3, 4, 5);

// from:把数组 / Promise / 可迭代对象转成 Observable
constarr$ = from(['apple', 'banana', 'cherry']);
constpromise$ = from(fetch('/api/data'));

// interval:每隔 n 毫秒发一个递增数字(从0开始)
consttick$ = interval(1000);   // 0, 1, 2, 3...

// timer:延迟 n 毫秒后发一个值,或延迟后再按间隔发
constdelayed$ = timer(2000);              // 2秒后发 0,然后完成
constdelayedInterval$ = timer(1000, 500); // 1秒后开始,每500ms发一个
```

### 2.2 subscribe 的三个回调

```typescript
nums$.subscribe({
  next:     (value) => console.log('收到值:', value),
  error:    (err)   => console.error('发生错误:', err),
  complete: ()      => console.log('流已完成'),
});

// 简写(只关心 next)
nums$.subscribe(value=>console.log(value));
```

### 2.3 Observable 的特性

```typescript
constobs$ = newObservable<number>(subscriber=>{
  console.log('开始执行');
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  subscriber.complete();
});

// 不订阅,"开始执行" 不会打印 —— 懒执行
obs$.subscribe(v=>console.log(v)); // 这时才执行

// 每次 subscribe 都是独立执行
obs$.subscribe(v=>console.log('第二次:', v)); // 再次执行
```

**命名约定**:Observable 变量习惯以 `$` 结尾,如 `user$``data$`

---

## 3. pipe + 基础 operators

### 3.1 pipe 的作用

`pipe()` 把多个 operator 串联起来,数据按顺序经过每一个:

```typescript
import{ of }from'rxjs';
import{ map, filter, tap, take }from'rxjs/operators';

of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
  .pipe(
    tap(v=>console.log('原始值:', v)),  // 只观察,不改变
    filter(v=>v % 2 === 0),             // 过滤:只保留偶数
    map(v=>v * 100),                    // 变换:乘以100
    take(3),                               // 只取前3个,然后自动完成
  )
  .subscribe(v=>console.log('最终:', v));

// 输出:
// 原始值: 1
// 原始值: 2  → 最终: 200
// 原始值: 3
// 原始值: 4  → 最终: 400
// 原始值: 5
// 原始值: 6  → 最终: 600(take(3) 满足,完成)
```

### 3.2 常用基础 operators

| Operator | 作用 | 示例 |
|----------|------|------|
| `map` | 变换每个值 | `map(x => x * 2)` |
| `filter` | 过滤值 | `filter(x => x > 0)` |
| `tap` | 观察(调试用),不改变值 | `tap(x => console.log(x))` |
| `take(n)` | 只取前 n 个值,然后完成 | `take(5)` |
| `skip(n)` | 跳过前 n 个值 | `skip(2)` |
| `first()` | 只取第一个值 | `first()` |
| `last()` | 只取最后一个值(流完成后) | `last()` |
| `distinctUntilChanged` | 值没变就不发 | `distinctUntilChanged()` |
| `debounceTime(ms)` | 停止发值 n ms 后才触发 | `debounceTime(300)` |
| `delay(ms)` | 延迟 n ms 后才发 | `delay(500)` |

```typescript
// debounceTime 最常见场景:搜索框防抖
inputChange$.pipe(
  debounceTime(300),       // 用户停止输入 300ms 才触发
  distinctUntilChanged(),  // 输入内容没变就不重复请求
).subscribe(keyword=>searchApi(keyword));
```

---

## 4. Subject 三兄弟

### 4.1 Subject

普通 Subject:没有初始值,晚来的订阅者收不到之前的值。

```typescript
import{ Subject }from'rxjs';

constevent$ = newSubject<string>();

event$.subscribe(v=>console.log('A:', v));
event$.subscribe(v=>console.log('B:', v));

event$.next('click');  // A: click, B: click
event$.next('hover');  // A: hover, B: hover

// 晚来的订阅者 C,收不到前两条
event$.subscribe(v=>console.log('C:', v));
event$.next('focus');  // A: focus, B: focus, C: focus

event$.complete(); // 通知所有订阅者流已结束
```

**使用场景**:组件间事件总线、手动触发某个操作。

### 4.2 BehaviorSubject ⭐(最常用)

有初始值,任何新订阅者立即收到当前值。**Angular Service 跨组件共享状态的标准做法。**

```typescript
import{ BehaviorSubject }from'rxjs';

// ─── service 文件 ───
@Injectable({ providedIn:'root'})
exportclassUserStateService {
  // 私有,外部不能直接调用 next()
  private_userName$ = newBehaviorSubject<string>('游客');

  // 公开只读视图
  readonlyuserName$ = this._userName$.asObservable();

  // 读当前值(不需要订阅)
  getcurrentUserName(): string{
    returnthis._userName$.getValue();
  }

  login(name: string) { this._userName$.next(name); }
  logout()            { this._userName$.next('游客'); }
}

// ─── 组件文件 ───
@Component({
  template:`<p>当前用户:{{ userName$ | async }}</p>`
})
exportclassHeaderComponent {
  userStateService = inject(UserStateService);
  userName$ = this.userStateService.userName$;
}
```

```typescript
// 演示 BehaviorSubject 的"立即给当前值"特性
constcount$ = newBehaviorSubject<number>(0);

count$.subscribe(v=>console.log('订阅者A:', v)); // 立即输出: 0

count$.next(1); // A: 1
count$.next(2); // A: 2

// 晚来的订阅者 B,立即收到当前值 2
count$.subscribe(v=>console.log('订阅者B:', v)); // 立即输出: 2

count$.next(3); // A: 3, B: 3
```

### 4.3 ReplaySubject

缓冲最近 N 条记录,新订阅者可以"重放"历史。

```typescript
import{ ReplaySubject }from'rxjs';

constlog$ = newReplaySubject<string>(3); // 缓冲最近3条

log$.next('消息1');
log$.next('消息2');
log$.next('消息3');
log$.next('消息4');
log$.next('消息5');

// 晚来的订阅者收到最近3条:消息3, 消息4, 消息5
log$.subscribe(v=>console.log('重放:', v));
```

**使用场景**:消息记录、日志、用户操作历史。

### 4.4 三者对比

| | 初始值 | 新订阅者立即收到 | 场景 |
|-|--------|----------------|------|
| **Subject** | 无 | 无 | 事件总线 |
| **BehaviorSubject** | 必须有 | 当前值(1条) | **状态管理**(最常用) |
| **ReplaySubject(n)** | 无 | 最近 n 条 | 消息历史 |

---

## 5. switchMap — 切换流

**核心特点**:收到新值时,**取消上一次**的内部 Observable,切换到新的。

```typescript
import{ switchMap }from'rxjs/operators';

// 最典型场景:搜索框
// 用户快速输入 "a" → "an" → "ang",只关心最后一次 "ang" 的结果
searchInput$.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(keyword=>http.get(`/api/search?q=${keyword}`)),
  //        ↑ 上一个请求还没回来,新的来了,自动取消旧的
).subscribe(results=>this.results = results);
```

```typescript
// 演示取消效果
constclick$ = newSubject<void>();

click$.pipe(
  switchMap(() =>{
    console.log('发起新请求,取消旧请求');
    returnof('结果').pipe(delay(1000));
  })
).subscribe(v=>console.log(v));

click$.next(); // 发起请求
click$.next(); // 立即取消上一个,发起新请求
click$.next(); // 立即取消上一个,发起新请求
// 只有最后一个请求的结果会被处理
```

**何时用 switchMap**:只关心最新结果,旧的结果无意义时(搜索、路由切换、下拉选择触发查询)。

---

## 6. mergeMap — 并行流

**核心特点**:不取消旧的,所有内部 Observable **同时运行**

```typescript
import{ mergeMap }from'rxjs/operators';
import{ from }from'rxjs';

// 同时发3个请求,谁先回来就先处理谁
from([1, 2, 3]).pipe(
  mergeMap(id=>http.get(`/api/user/${id}`))
).subscribe(user=>console.log('用户:', user));
// 请求1、2、3 同时发出,结果顺序不固定
```

```typescript
// switchMap vs mergeMap 对比
constids$ = from([1, 2, 3]);

// switchMap:3个请求,前两个被取消,只有第3个的结果
ids$.pipe(switchMap(id=>http.get(`/user/${id}`))).subscribe(...);

// mergeMap:3个请求全部发出,3个结果都处理
ids$.pipe(mergeMap(id=>http.get(`/user/${id}`))).subscribe(...);
```

**何时用 mergeMap**:每个请求都需要处理结果,且顺序无所谓时(批量上传、并发加载)。

---

## 7. forkJoin — 等所有完成

等待多个 Observable **全部完成**后,把最后的值合并在一起发出。类似 `Promise.all()`

```typescript
import{ forkJoin }from'rxjs';

// 页面初始化:需要3个接口都返回后才渲染
forkJoin({
  user:        http.get<User>('/api/user'),
  config:      http.get<Config>('/api/config'),
  permissions: http.get<string[]>('/api/permissions'),
}).subscribe({
  next: ({ user, config, permissions }) => {
    // 三个接口全部成功才到这里
    this.user = user;
    this.config = config;
    this.permissions = permissions;
    this.isLoaded =true;
  },
  error: (err) => {
    // 任意一个出错,整个 forkJoin 都进入 error
    console.error('加载失败:', err);
  }
});

// 并行 vs 串行的时间对比:
// 串行:500ms + 300ms + 400ms = 1200ms
// forkJoin 并行:max(500ms, 300ms, 400ms) = 500ms ✅
```

**注意**:如果其中一个 Observable 永不完成(如 Subject),forkJoin 也永远不会发值。

---

## 8. combineLatest — 多流联动

任意一个流发出新值时,用**所有流的最新值**组合在一起发出。

```typescript
import{ combineLatest, BehaviorSubject }from'rxjs';
import{ map }from'rxjs/operators';

// 场景:价格、数量、折扣任一变化,实时计算总价
constprice$    = newBehaviorSubject<number>(100);
constquantity$ = newBehaviorSubject<number>(1);
constdiscount$ = newBehaviorSubject<number>(0);

combineLatest([price$, quantity$, discount$]).pipe(
  map(([price, qty, disc]) =>price * qty * (1 - disc / 100))
).subscribe(total=>console.log('总价:', total));

price$.next(200);    // 总价: 200  (200 × 1 × 1)
quantity$.next(3);   // 总价: 600  (200 × 3 × 1)
discount$.next(10);  // 总价: 540  (200 × 3 × 0.9)
```

```typescript
// 另一个场景:表单多个字段联合校验
combineLatest([password$, confirmPassword$]).pipe(
  map(([pwd, confirm]) =>pwd === confirm),
).subscribe(isMatch=>this.passwordMatch = isMatch);
```

**forkJoin vs combineLatest**

| | 触发时机 | 适用场景 |
|-|---------|---------|
| `forkJoin` | 所有流都**完成**后触发一次 | 页面初始化并行加载 |
| `combineLatest` | 任一流发值就触发,**持续** | 多个状态联动计算 |

---

## 9. catchError — 错误处理

```typescript
import{ catchError }from'rxjs/operators';
import{ of, throwError }from'rxjs';

// 基本用法:捕获错误,返回默认值,流继续
http.get('/api/data').pipe(
  catchError(err=>{
    console.error('请求失败:', err.message);
    returnof([]);  // 用空数组兜底,流不中断
  })
).subscribe(data=>this.data = data);

// 错误后重新抛出(上层处理)
http.get('/api/critical').pipe(
  catchError(err=>{
    this.logErrorToServer(err);
    returnthrowError(() => err);  // 继续向上传递错误
  })
).subscribe({
  next:data=>...,
  error:err=>this.showErrorDialog(err)  // 在这里最终处理
});
```

```typescript
// retry:自动重试
import{ retry }from'rxjs/operators';

http.get('/api/data').pipe(
  retry(3),           // 失败后最多重试3次
  catchError(err=>of([]))
).subscribe(...);
```

---

## 10. scan — 累加状态

`scan` 像数组的 `reduce`,区别是**每一步都发出中间值**,非常适合维护累计状态。

```typescript
import{ scan }from'rxjs/operators';
import{ Subject }from'rxjs';

// 场景1:购物车数量
constcartAction$ = newSubject<number>();

cartAction$.pipe(
  scan((total, change) =>Math.max(0, total + change), 0)
).subscribe(count=>this.cartCount = count);

cartAction$.next(1);   // count: 1
cartAction$.next(1);   // count: 2
cartAction$.next(-1);  // count: 1
cartAction$.next(3);   // count: 4
```

```typescript
// 场景2:消息列表追加(不用每次重建数组)
constnewMessage$ = newSubject<string>();

newMessage$.pipe(
  scan((messages, msg) => [...messages, msg], [] asstring[])
).subscribe(messages=>this.messages = messages);

newMessage$.next('你好');          // ['你好']
newMessage$.next('在吗?');        // ['你好', '在吗?']
newMessage$.next('有什么事吗?'); // ['你好', '在吗?', '有什么事吗?']
```

---

## 11. interval + takeUntil — 定时器与取消

```typescript
import{ interval, Subject }from'rxjs';
import{ takeUntil, take, map }from'rxjs/operators';

// takeUntil:当另一个 Observable 发出值时,自动停止
conststop$ = newSubject<void>();

interval(1000).pipe(
  takeUntil(stop$),          // stop$.next() 被调用时停止
  take(60),                   // 或者最多60次后自动停止
  map(i=>`第 ${i + 1} 秒`),
).subscribe({
  next:     v  => console.log(v),
  complete: () => console.log('定时器已停止'),
});

// 5秒后停止
setTimeout(() =>stop$.next(), 5000);
```

---

## 12. 内存泄漏防范

**不取消订阅 = 内存泄漏**,是 RxJS 在 Angular 中最常见的问题。

### 方法一:takeUntil(推荐)

```typescript
@Component({ ...})
exportclassMyComponentimplementsOnInit, OnDestroy {
  privatedestroy$ = newSubject<void>();

  ngOnInit() {
    // 每个订阅都加 takeUntil(this.destroy$)
    someStream$.pipe(
      takeUntil(this.destroy$)
    ).subscribe(...);

    anotherStream$.pipe(
      takeUntil(this.destroy$)
    ).subscribe(...);

    // interval 之类的不会自动完成的流,必须这样处理
    interval(1000).pipe(
      takeUntil(this.destroy$)
    ).subscribe(...);
  }

  ngOnDestroy() {
    // 一行代码,取消所有订阅
    this.destroy$.next();
    this.destroy$.complete();
  }
}
```

### 方法二:async pipe(模板中自动管理)

```typescript
// 组件
@Component({
  template:`
    <p>{{ user$ | async | json }}</p>
    <li *ngFor="let item of list$ | async">{{ item }}</li>
  `
})
exportclassMyComponent {
  user$ = this.userService.getUser();   // 不需要手动订阅
  list$ = this.dataService.getList();   // async pipe 自动取消订阅
}
```

**能用 `async pipe` 的地方优先用 `async pipe`**,它完全不需要手动管理订阅。

### 方法三:take(1)(只需要一次的场景)

```typescript
// 只需要获取一次初始数据,不需要持续监听
this.userService.getUser().pipe(
  take(1)  // 收到第一个值后自动完成并取消订阅
).subscribe(user=>this.user = user);
```

---

## 13. Angular 中的最佳实践

### 13.1 Service 状态管理模板

```typescript
@Injectable({ providedIn:'root'})
exportclassProductService {
  privatehttp = inject(HttpClient);

  // 用 BehaviorSubject 维护状态
  private_products$ = newBehaviorSubject<Product[]>([]);
  private_loading$  = newBehaviorSubject<boolean>(false);
  private_error$    = newBehaviorSubject<string | null>(null);

  // 对外只暴露只读 Observable
  readonlyproducts$ = this._products$.asObservable();
  readonlyloading$  = this._loading$.asObservable();
  readonlyerror$    = this._error$.asObservable();

  loadProducts(): void{
    this._loading$.next(true);
    this._error$.next(null);

    this.http.get<Product[]>('/api/products').pipe(
      catchError(err=> {
        this._error$.next(err.message);
        returnof([]);
      }),
    ).subscribe(products=> {
      this._products$.next(products);
      this._loading$.next(false);
    });
  }
}
```

### 13.2 组件订阅模板

```typescript
@Component({
  template:`
    @if (loading$ | async) { <p>加载中...</p> }
    @if (error$ | async; as err) { <p>错误: {{ err }}</p> }
    @for (p of products$ | async; track p.id) {
      <div>{{ p.name }}</div>
    }
  `
})
exportclassProductListComponentimplementsOnInit {
  privateproductService = inject(ProductService);
  privatedestroy$ = newSubject<void>();

  // async pipe 绑定
  products$ = this.productService.products$;
  loading$  = this.productService.loading$;
  error$    = this.productService.error$;

  ngOnInit() { this.productService.loadProducts(); }
  ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }
}
```

### 13.3 HTTP 请求的标准写法

```typescript
// ✅ 推荐:在 service 里处理逻辑,组件只订阅结果
// service
getUserById(id: number): Observable<User> {
  return this.http.get<User>(`/api/user/${id}`).pipe(
    map(resp => resp),          // 可以做数据变换
    catchError(this.handleError) // 统一错误处理
  );
}

// ✅ 推荐:搜索防抖
searchUsers(term$: Observable<string>): Observable<User[]> {
  return term$.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap(term=>this.http.get<User[]>(`/api/users?q=${term}`)),
    catchError(() =>of([]))
  );
}
```

---

## 14. 常见错误速查

### ❌ 忘记取消订阅

```typescript
// ❌ 错误:interval 永远不会自动完成,组件销毁后仍在运行
ngOnInit() {
  interval(1000).subscribe(v=>this.count = v);
}

// ✅ 正确
ngOnInit() {
  interval(1000).pipe(
    takeUntil(this.destroy$)
  ).subscribe(v=>this.count = v);
}
```

### ❌ 嵌套 subscribe

```typescript
// ❌ 错误:嵌套 subscribe,无法管理内部订阅,容易泄漏
this.route.params.subscribe(params=>{
  this.http.get(`/api/user/${params.id}`).subscribe(user=> {
    this.user = user;
  });
});

// ✅ 正确:用 switchMap 展平
this.route.params.pipe(
  switchMap(params=>this.http.get(`/api/user/${params.id}`)),
  takeUntil(this.destroy$)
).subscribe(user=>this.user = user);
```

### ❌ 在 BehaviorSubject 上直接暴露

```typescript
// ❌ 错误:外部可以直接 next(),破坏封装
@Injectable()
exportclassUserService {
  publicuser$ = newBehaviorSubject<User | null>(null); // 危险!
}

// ✅ 正确:私有 Subject + 公开只读 Observable
@Injectable()
exportclassUserService {
  private_user$ = newBehaviorSubject<User | null>(null);
  readonlyuser$ = this._user$.asObservable();

  setUser(user: User) { this._user$.next(user); }
}
```

### ❌ 用 mergeMap 做搜索

```typescript
// ❌ 错误:用 mergeMap 做搜索,旧请求的结果会覆盖新结果
searchInput$.pipe(
  mergeMap(kw=>http.get(`/search?q=${kw}`)) // 结果顺序不确定
).subscribe(results=>this.results = results);

// ✅ 正确:用 switchMap,自动取消旧请求
searchInput$.pipe(
  switchMap(kw=>http.get(`/search?q=${kw}`))
).subscribe(results=>this.results = results);
```

### ❌ forkJoin 传入不会完成的 Observable

```typescript
// ❌ 错误:BehaviorSubject 永远不会完成,forkJoin 永远不会发值
constsubject = newBehaviorSubject(0);
forkJoin([subject, http.get('/api')]).subscribe(...); // 永远不触发

// ✅ 正确:用 take(1) 让它完成一次
forkJoin([subject.pipe(take(1)), http.get('/api')]).subscribe(...);
```

---

## 附录:operators 速查表

### 变换类

| Operator | 说明 |
|----------|------|
| `map(fn)` | 逐个变换值 |
| `scan(fn, seed)` | 累加,每步都发出中间值 |
| `reduce(fn, seed)` | 累加,只在完成时发出最终值 |
| `switchMap(fn)` | 切换内部流,取消旧的 |
| `mergeMap(fn)` | 展平内部流,全部并行 |
| `concatMap(fn)` | 展平内部流,串行排队 |
| `exhaustMap(fn)` | 忽略新值,直到当前内部流完成 |

### 过滤类

| Operator | 说明 |
|----------|------|
| `filter(fn)` | 按条件过滤 |
| `take(n)` | 取前 n 个 |
| `takeUntil(obs$)` | 直到另一个流发值才停止 |
| `skip(n)` | 跳过前 n 个 |
| `debounceTime(ms)` | 停止发值 n ms 后触发 |
| `throttleTime(ms)` | n ms 内只取第一个 |
| `distinctUntilChanged` | 值没变不重复发 |
| `first()` | 只取第一个 |

### 组合类

| Operator | 说明 |
|----------|------|
| `forkJoin([...])` | 等所有完成,取各自最后一个值 |
| `combineLatest([...])` | 任一变化,取所有最新值 |
| `merge(...)` | 多个流合并,谁发就处理谁 |
| `concat(...)` | 多个流串行,前一个完成才开始下一个 |
| `zip(...)` | 多个流一一配对 |

### 错误处理类

| Operator | 说明 |
|----------|------|
| `catchError(fn)` | 捕获错误,返回备用 Observable |
| `retry(n)` | 出错后自动重试 n 次 |
| `retryWhen(fn)` | 自定义重试策略 |
| `finalize(fn)` | 流完成或出错时都执行(类似 finally) |

---

*文件位置:`src/app/rxjs-demo/RXJS_GUIDE.md`*
*配套代码:`src/app/rxjs-demo/rxjs-demo.service.ts`(可运行的12个示例函数)*
*交互演示:`src/app/rxjs-demo/rxjs-demo.component.ts`(启动项目后访问 `/rxjs-demo`)*
posted on 2026-07-15 17:48  夜之独行者  阅读(6)  评论(0)    收藏  举报