# 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`)*
浙公网安备 33010602011771号