WinddSnow

Angular-RxJS-and-Reactive-Programming-Observable-Subject-Operators-Async-Pipe

字数统计: 5.4k阅读时长: 22 min
2026/08/02

第118课:RxJS 与 Angular 响应式编程——Observable、Subject、操作符、async 管道、取消订阅策略

Angular 与 React/Vue 最根本的差异之一在于它深度集成 RxJS——一个用于处理异步数据流和事件的可组合响应式编程库。在 Angular 中,HTTP 请求返回的是 Observable,路由参数是 Observable,表单值变更是 Observable,组件间通信也可以通过 Subject 实现。RxJS 不是 Angular 的附属品,而是 Angular 数据层架构的核心支柱。理解 Observable惰性求值本质、Subject多播能力、操作符(operator)的链式转换逻辑、以及 async 管道的自动订阅与取消机制,是构建复杂、高性能、无内存泄漏的 Angular 应用的关键。本节课将从核心概念出发,逐一拆解 Observable 与 Promise 的差异、SubjectBehaviorSubject 的用法、最常用的操作符分类、async 管道的工作原理,以及五种取消订阅策略,最后通过一个带搜索和防抖的产品列表服务将所学知识串联。


1. Observable:惰性推送的数据流

Observable 是 RxJS 的核心类型,代表一个可订阅的异步数据流。它可以同步或异步地发出多个值(或错误、或完成通知),然后结束。与 Promise 不同,Observable惰性的——仅当有人订阅它时,它才开始执行;且它可以取消——你可以随时退订,从而中止未完成的异步操作。

1.1 创建 Observable

1
2
3
4
5
6
7
8
9
10
11
import { Observable } from 'rxjs';

// 手动创建一个 Observable
const stream$ = new Observable<number>(subscriber => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
// 发出错误(之后不再发出值)
// subscriber.error(new Error('Something went wrong'));
subscriber.complete(); // 发出完成通知
});

订阅 Observable

1
2
3
4
5
6
7
8
const subscription = stream$.subscribe({
next: (value) => console.log('收到值:', value),
error: (err) => console.error('出错:', err),
complete: () => console.log('数据流结束'),
});

// 取消订阅
subscription.unsubscribe();

执行结果:控制台依次输出 收到值: 1收到值: 2收到值: 3数据流结束。调用 unsubscribe() 后,即使后续还有 next 调用,订阅者也不会再收到值。

1.2 Observable 与 Promise 的对比

特性 Promise Observable
执行时机 创建后立即执行(eager)。 仅当被订阅时才执行(lazy)。
返回值数量 仅一个值(resolve 或 reject)。 可以发出零到无限多个值
能否取消 不能取消(一旦开始就无法中止)。 可以取消(调用 unsubscribe())。
操作符 .then() / .catch() / .finally() 丰富的操作符链(map、filter、debounce 等)。
使用场景 单次异步操作(如 HTTP 请求的结果)。 事件流、WebSocket、定时器、多次值等。

关键结论:在 Angular 中,HTTP 请求虽然每个请求只发出一个值后完成(行为类似 Promise),但 HttpClient 返回 Observable 是为了让你可以组合多个请求、添加重试、取消未完成的请求(如组件卸载时自动取消),以及利用 async 管道简化模板订阅。

1.3 Observable 的命名约定

社区约定以 $ 后缀命名 Observable 类型的变量(如 users$loading$data$)。这不是语法要求,但有助于在代码中一眼识别数据流。


2. Subject:多播的 Observable

Subject 是一种特殊的 Observable,它既可以作为数据生产者(你可以在代码中主动调用 subject.next(value) 推送值),也可以作为数据消费者(你可以订阅它)。与普通 Observable 每次订阅都独立执行不同,Subject多播的——所有订阅者共享同一个执行上下文,它们收到的是相同的值序列。

2.1 四种 Subject 类型

Subject 类型 特性 适用场景
Subject 基础版。订阅者仅收到订阅之后发出的值。 事件总线、组件间通信。
BehaviorSubject 需要一个初始值。新订阅者会立即收到当前的最新值(或初始值)。 状态管理、缓存当前值。
ReplaySubject 缓冲指定数量的历史值。新订阅者会重放缓冲区中的所有值。 聊天记录、最近 N 条日志。
AsyncSubject 仅在 Observable 完成时发出最后一个值 类似 Promise 的行为(仅最终值重要)。

2.2 Subject 作为组件间事件总线

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class EventBusService {
// 通知事件:携带字符串消息
private notificationSubject = new Subject<string>();
notification$ = this.notificationSubject.asObservable();

sendNotification(message: string): void {
this.notificationSubject.next(message);
}
}

发送方调用 eventBus.sendNotification('新消息'),订阅方通过 eventBus.notification$.subscribe(msg => ...) 接收。由于 Subject 是多播的,多个订阅者会同时收到相同的消息。

2.3 BehaviorSubject 作为状态容器

BehaviorSubject 是 Angular 服务中最常用的 Subject 类型,因为它始终持有当前值,适合作为组件可观察状态的来源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Injectable({ providedIn: 'root' })
export class UserStateService {
// 初始化时必须提供一个值
private userSubject = new BehaviorSubject<User | null>(null);
user$ = this.userSubject.asObservable();

setUser(user: User): void {
this.userSubject.next(user);
}

clearUser(): void {
this.userSubject.next(null);
}

// 同步获取当前值(不需要订阅)
get currentUser(): User | null {
return this.userSubject.value;
}
}

关键行为:当组件在 setUser 调用之后才订阅 user$,它仍会立即收到上一次发出的 User 对象。这使得组件无论何时订阅,都能获得最新的状态。

注意BehaviorSubject 通过 .value 属性暴露当前值。但过度使用 .value 会削弱响应式编程的优势——推荐尽量通过订阅 .subscribe()async 管道来消费数据,仅在需要同步获取快照时使用 .value


3. 操作符:数据流的转换管道

操作符是 RxJS 最强大的特性。它们允许你通过 .pipe() 方法将多个操作符串联起来,形成一个数据处理管道——前一个操作符的输出作为后一个的输入。操作符分为两大类:创建型操作符(创建新 Observable)和管道型操作符(在 pipe 内部使用,转换数据流)。

3.1 创建型操作符

这些是 Observable 类的静态方法,用于从各种来源创建 Observable。

操作符 用途 示例
of(...values) 将一组固定值依次发出。 of(1, 2, 3)
from(iterable) 将数组、Promise、或可迭代对象转为 Observable。 from([1, 2, 3])
fromEvent(el, ev) 从 DOM 事件创建 Observable。 fromEvent(button, 'click')
interval(ms) 每隔指定毫秒发出递增数字。 interval(1000) → 0, 1, 2, …
timer(ms) 延迟指定毫秒后发出一个值,然后完成。 timer(1000) → 0 (1秒后)
combineLatest 组合多个 Observable,当任一发出值时,合并最新值发出。 combineLatest([a$, b$])
forkJoin 等待所有 Observable 完成,然后发出每个的最后一个值。 forkJoin([req1$, req2$])
merge 将多个 Observable 的输出合并为一个流。 merge(click$, keyup$)

3.2 管道型操作符(最常用)

这些是 pipe() 内部使用的函数,用于转换、过滤和组合数据流。

转换类

操作符 用途 示例
map(fn) 将每个发出的值转换为新值。 map(response => response.data)
switchMap(fn) 取消前一个内部 Observable,切换到新的。 搜索框中用户输入后取消上一次请求。
concatMap(fn) 排队执行内部 Observable,前一个完成才开始下一个。 表单提交——按顺序保存。
mergeMap(fn) 并行执行所有内部 Observable,按完成顺序发出。 多个独立请求同时发送。
exhaustMap(fn) 忽略新请求,直到当前内部 Observable 完成。 登录按钮——防止重复提交。

过滤类

操作符 用途 示例
filter(predicate) 仅发出满足条件的值。 filter(value => value > 0)
debounceTime(ms) 延迟发出,如果在延迟期间有新值,重新计时。 搜索输入防抖。
distinctUntilChanged() 仅当值与前一次不同时才发出。 避免重复请求相同数据。
take(n) 仅取前 n 个值,然后自动完成并取消订阅。 仅关心第一次事件。
takeUntil(notifier$) 一直取值,直到 notifier$ 发出值或完成。 组件卸载时自动取消订阅(见第 5 节)。
first() 取第一个值(或满足条件的第一个值),然后完成。 仅关心初始化数据。

工具类

操作符 用途
tap(fn) 执行副作用(如 console.log、更新 loading 状态),不改变数据。
catchError(fn) 捕获错误并返回一个新的 Observable 或抛出错误。
retry(n) 发生错误时重试 n 次。
finalize(fn) Observable 完成或出错时执行回调(类似于 finally)。
shareReplay(n) 共享源 Observable 的订阅,并重放最后 n 个值给新订阅者。

3.3 switchMap vs concatMap vs mergeMap 的经典对比

1
2
3
4
5
6
7
8
9
10
// 场景:用户快速点击三个按钮,每个按钮触发一个 HTTP 请求

// switchMap:取消前两个,仅执行最后一个请求
fromEvent(button, 'click').pipe(switchMap(() => this.http.get('/api')));

// concatMap:按顺序执行,一个完成再开始下一个(总共发出三次响应)
fromEvent(button, 'click').pipe(concatMap(() => this.http.get('/api')));

// mergeMap:并行执行,按完成顺序发出响应(可能乱序)
fromEvent(button, 'click').pipe(mergeMap(() => this.http.get('/api')));

选择原则

  • 搜索输入 → switchMap(取消旧请求)。
  • 保存表单 → concatMap(按顺序提交,避免覆盖)。
  • 独立数据面板 → mergeMap(并行加载,更快)。
  • 登录按钮 → exhaustMap(忽略重复点击)。

4. async 管道:自动订阅与取消

async 管道是 Angular 模板中最强大的工具之一。它接收一个 ObservablePromise自动订阅,返回发出的最新值;当组件销毁时,它自动取消订阅,彻底消除手动管理订阅生命周期导致的内存泄漏风险。

4.1 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component({
selector: 'app-user-list',
template: `
<div *ngIf="users$ | async as users; else loading">
<li *ngFor="let user of users">{{ user.name }}</li>
</div>
<ng-template #loading>加载中...</ng-template>
`,
})
export class UserListComponent {
users$ = this.http.get<User[]>('/api/users'); // Observable
constructor(private http: HttpClient) {}
}

关键行为

  • users$ | async 订阅 users$,当 Observable 发出值时更新视图。
  • as users 将发出的值赋值给局部变量 users,供后续使用。
  • *ngIf 在值为 nullundefined 时显示 else 模板,避免访问空对象属性。
  • UserListComponent 被销毁时,async 管道自动调用 unsubscribe(),你无需手动处理。

4.2 与 *ngFor 配合

1
2
3
<li *ngFor="let user of users$ | async">
{{ user.name }}
</li>

4.3 多个 Observable 的组合

1
2
3
4
<ng-container *ngIf="{ users: users$ | async, stats: stats$ | async } as data">
<p>总用户数:{{ data.users?.length }}</p>
<p>活跃用户:{{ data.stats?.active }}</p>
</ng-container>

*ngIf 可以接收一个对象字面量,将多个 async 管道的值组合为一个字典对象,一次性判断所有数据是否就绪。


5. 取消订阅策略:防止内存泄漏

Observable 订阅如果不手动取消,会在组件销毁后继续存在,导致内存泄漏(闭包引用了已销毁的组件实例)。以下是五种安全的取消订阅策略,按推荐度排序。

5.1 async 管道(最佳:零手动管理)

在模板中使用 async 管道,Angular 自动处理订阅和取消。这是首选的、最安全的方式

5.2 takeUntil + Subject 模式(最通用的手动方案)

创建一个 Subject 作为“销毁信号”,在所有订阅后追加 .pipe(takeUntil(this.destroy$)),在 ngOnDestroy 中发出信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Component, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

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

ngOnInit(): void {
this.userService.users$
.pipe(takeUntil(this.destroy$))
.subscribe(users => this.users = users);

this.http.get('/api/config')
.pipe(takeUntil(this.destroy$))
.subscribe(config => this.config = config);
}

ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

原理takeUntil(destroy$) 监听 destroy$,当 destroy$ 发出任意值(或完成)时,自动调用 unsubscribe() 取消上游订阅。在 ngOnDestroy 中调用 destroy$.next() 触发所有订阅的清理。

5.3 take(1)(仅需第一个值)

如果你只关心 Observable 发出的第一个值(类似于 Promise 行为),使用 take(1)。获取第一个值后自动取消订阅。

1
this.http.get('/api/config').pipe(take(1)).subscribe(config => this.config = config);

适用场景:单次 HTTP 请求、初始化加载。

5.4 Subscription.add()(手动管理订阅池)

将每个订阅加入一个订阅池,在销毁时统一取消。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Subscription } from 'rxjs';

@Component({ ... })
export class MyComponent implements OnDestroy {
private subs = new Subscription();

ngOnInit(): void {
this.subs.add(
this.userService.users$.subscribe(users => this.users = users)
);
this.subs.add(
this.http.get('/api/config').subscribe(config => this.config = config)
);
}

ngOnDestroy(): void {
this.subs.unsubscribe();
}
}

5.5 untilDestroyed(需要 @ngneat/until-destroy 第三方库)

使用社区库进一步简化,通过装饰器自动注入销毁逻辑:

1
2
3
4
5
6
7
8
9
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';

@UntilDestroy()
@Component({ ... })
export class MyComponent {
ngOnInit(): void {
this.userService.users$.pipe(untilDestroyed(this)).subscribe(...);
}
}

6. 综合实战:带防抖搜索的产品列表服务

以下代码展示了 Observable、Subject、操作符和 async 管道如何在一个真实场景中协同工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// product.model.ts
export interface Product {
id: number;
name: string;
price: number;
category: string;
}

// product.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Subject, Observable, combineLatest } from 'rxjs';
import {
map,
switchMap,
debounceTime,
distinctUntilChanged,
startWith,
} from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class ProductService {
constructor(private http: HttpClient) {}

// 搜索关键词流
private searchSubject = new BehaviorSubject<string>('');
search$ = this.searchSubject.asObservable();

// 分类筛选流
private categorySubject = new BehaviorSubject<string>('all');
category$ = this.categorySubject.asObservable();

// 加载状态
private loadingSubject = new BehaviorSubject<boolean>(false);
loading$ = this.loadingSubject.asObservable();

// 产品数据流:组合搜索、分类,自动发起 HTTP 请求
products$: Observable<Product[]> = combineLatest([
this.search$.pipe(debounceTime(300), distinctUntilChanged()),
this.category$,
]).pipe(
tap(() => this.loadingSubject.next(true)),
switchMap(([search, category]) =>
this.http.get<Product[]>('/api/products', {
params: {
q: search,
category: category === 'all' ? '' : category,
},
})
),
tap(() => this.loadingSubject.next(false)),
);

setSearch(term: string): void {
this.searchSubject.next(term);
}

setCategory(category: string): void {
this.categorySubject.next(category);
}
}

// product-list.component.ts
import { Component, OnInit } from '@angular/core';
import { ProductService } from '../../services/product.service';

@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
})
export class ProductListComponent {
products$ = this.productService.products$;
loading$ = this.productService.loading$;

constructor(private productService: ProductService) {}

onSearchInput(value: string): void {
this.productService.setSearch(value);
}

onCategoryChange(category: string): void {
this.productService.setCategory(category);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- product-list.component.html -->
<div class="search-bar">
<input #searchInput
type="text"
placeholder="搜索产品..."
(input)="onSearchInput(searchInput.value)" />
<select (change)="onCategoryChange($any($event.target).value)">
<option value="all">全部分类</option>
<option value="electronics">电子产品</option>
<option value="clothing">服装</option>
<option value="food">食品</option>
</select>
</div>

<div *ngIf="loading$ | async" class="loading">搜索中...</div>

<ul *ngIf="products$ | async as products; else empty">
<li *ngFor="let product of products">
{{ product.name }} — ¥{{ product.price }} ({{ product.category }})
</li>
</ul>

<ng-template #empty>
<p>暂无匹配的产品</p>
</ng-template>

设计要点

  • searchSubjectcategorySubject 作为数据源头,组件通过调用服务的方法更新它们。
  • combineLatest 将搜索关键词和分类组合为一个流,任一变化都触发后续逻辑。
  • debounceTime(300) 防止每次按键都发送 HTTP 请求。
  • distinctUntilChanged 确保仅当搜索词实际变化时才发送请求。
  • switchMap 在新搜索词到来时取消上一个未完成的 HTTP 请求,避免竞态条件。
  • products$loading$ 通过 async 管道在模板中直接使用,无需手动订阅。
  • 组件类中完全没有 subscribe 调用——所有订阅通过 async 管道管理,自动取消。

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) 关于 ObservablePromise 的核心区别,以下描述正确的是?
    A. Observable 是立即执行的,Promise 是惰性的。
    B. Observable 可以发出多个值且可以取消,Promise 只能发出一个值且不可取消。
    C. Observable 只能通过 .then() 处理结果。
    D. Promise 可以使用 pipe() 链式调用操作符。

  2. (单选) 在搜索输入框场景中,为了取消前一个未完成的 HTTP 请求并仅响应最后一次输入,应使用哪个高阶映射操作符?
    A. concatMap
    B. mergeMap
    C. switchMap
    D. exhaustMap

  3. (填空) 在 Angular 模板中,使用 ______ 管道可以自动订阅 Observable 并在组件销毁时自动取消订阅。

  4. (多选) 以下哪些是正确的 Angular 取消订阅策略?
    A. 使用 async 管道,零手动管理。
    B. 使用 takeUntil(this.destroy$) 模式,在 ngOnDestroy 中触发销毁信号。
    C. 订阅后忽略,等待浏览器垃圾回收。
    D. 将所有订阅加入 Subscription 池,在 ngOnDestroy 中统一取消。

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要为 Angular 应用创建一个实时搜索组件和一个搜索服务。要求如下:

  • SearchService:使用 providedIn: 'root'。内部使用 Subject<string> 接收搜索关键词。通过 debounceTime(300)distinctUntilChangedswitchMap 将关键词转换为 API 请求(返回 Observable<string[]>),暴露 results$loading$ 两个 Observable。
  • SearchComponent:注入 SearchService。模板中使用 async 管道订阅 results$loading$。包含一个输入框,通过 (input) 事件调用服务的方法发送关键词。显示加载状态和结果列表。
  • 使用 takeUntil 模式在组件中管理手动订阅(如果需要的话),并确保无内存泄漏。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 Angular 服务和组件代码。提示词中需明确指定操作符的链式调用顺序、async 管道的使用、以及防抖和取消策略。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请为 Angular 应用创建实时搜索服务和组件。需要创建以下文件:

  1. src/app/services/search.service.ts
    • @Injectable({ providedIn: 'root' })
    • 私有 searchSubject = new Subject<string>()
    • 私有 loadingSubject = new BehaviorSubject<boolean>(false)
    • results$: Observable<string[]>:由 searchSubject 经过 pipe(debounceTime(300), distinctUntilChanged(), tap(() => loadingSubject.next(true)), switchMap(term => this.http.get<string[]>('/api/search', { params: { q: term } })), tap(() => loadingSubject.next(false))) 生成。
    • loading$ = this.loadingSubject.asObservable()
    • search(term: string): void { this.searchSubject.next(term); }
  2. src/app/components/search/search.component.ts
    • 选择器 app-search
    • 构造函数注入 private searchService: SearchService
    • 属性 results$ = this.searchService.results$loading$ = this.searchService.loading$
    • 方法 onInput(value: string): void { this.searchService.search(value); }
    • 模板:<input (input)="onInput($any($event.target).value)" placeholder="搜索..." />*ngIf="loading$ | async" 显示加载状态。*ngIf="results$ | async as results; else noResults" 包裹 <ul> 列表。noResults 模板显示“无结果”。
  3. 所有代码添加 JSDoc 注释。完成后列出所有文件内容。

四、面试真题与参考答案

题目(字节跳动前端面试题):

请解释 Angular 中 async 管道的工作原理,以及为什么推荐在模板中使用它而非在组件中手动订阅。switchMapmergeMap 的区别是什么?在构建搜索功能时,为何通常使用 switchMap 而不是 mergeMap?请结合竞态条件(Race Condition)说明。

参考答案

async 管道接收一个 ObservablePromise,自动订阅它,并将发出的值标记为视图中的变化。当组件被销毁时,async 管道自动调用 unsubscribe(),完全消除手动管理订阅导致的内存泄漏风险。推荐使用 async 管道而非手动订阅的原因:它简化了代码(无需声明 Subscription 变量和实现 ngOnDestroy),且从根本上杜绝了开发者忘记取消订阅的可能性。

switchMapmergeMap 的区别在于对内部 Observable 的处理方式:

  • switchMap:当源 Observable 发出新值时,立即取消前一个内部 Observable 的订阅,然后订阅新的内部 Observable。任何时候只有一个活跃的内部 Observable。
  • mergeMap同时维护所有内部 Observable 的订阅,任一发出值都立刻传递给下游。所有内部 Observable 并行执行,发出的值可能交错。

在搜索功能中,用户快速输入“A” → “AB” → “ABC”时,会连续触发三次 HTTP 请求。如果使用 mergeMap,三个请求并行发送,而网络延迟不可预测——可能出现第三个请求(“ABC”)先返回,第一个请求(“A”)后返回,导致最终显示的结果对应的是旧的搜索词“A”。这就是竞态条件switchMap 在每次新输入时取消前一个请求,确保仅最后一个请求的结果被使用,完美解决了竞态问题。因此搜索场景必须使用 switchMap


课后练习答案

一、概念自测答案

  1. B

    • 解析:Observable 是惰性的,可发出多个值且可取消;Promise 是立即执行的,仅一个值且不可取消。A 描述相反,C 描述的是 Promise,D 的 pipe()Observable 的方法。
  2. C

    • 解析:switchMap 在新值到来时取消前一个内部 Observable,适合搜索输入。concatMap 排队执行,mergeMap 并行执行,exhaustMap 忽略新值。
  3. async

    • 解析:Angular 的 async 管道自动订阅并取消,是最安全的方式。
  4. A、B、D

    • 解析:C 错误,垃圾回收不会自动取消订阅——Observable 的订阅是强引用,必须显式取消。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请为 Angular 创建实时搜索服务和组件。要求:

  • SearchService:Subject 接收关键词,debounceTime 300ms,distinctUntilChanged,switchMap 发起 HTTP 请求,暴露 results$ 和 loading$。
  • SearchComponent:注入服务,async 管道绑定模板,输入框触发搜索。
  • 使用 takeUntil 管理手动订阅(如有)。输出 service.ts 和 component.ts 的完整代码。”
CATALOG
  1. 1. 第118课:RxJS 与 Angular 响应式编程——Observable、Subject、操作符、async 管道、取消订阅策略
    1. 1.1. 1. Observable:惰性推送的数据流
      1. 1.1.1. 1.1 创建 Observable
      2. 1.1.2. 1.2 Observable 与 Promise 的对比
      3. 1.1.3. 1.3 Observable 的命名约定
    2. 1.2. 2. Subject:多播的 Observable
      1. 1.2.1. 2.1 四种 Subject 类型
      2. 1.2.2. 2.2 Subject 作为组件间事件总线
      3. 1.2.3. 2.3 BehaviorSubject 作为状态容器
    3. 1.3. 3. 操作符:数据流的转换管道
      1. 1.3.1. 3.1 创建型操作符
      2. 1.3.2. 3.2 管道型操作符(最常用)
      3. 1.3.3. 3.3 switchMap vs concatMap vs mergeMap 的经典对比
    4. 1.4. 4. async 管道:自动订阅与取消
      1. 1.4.1. 4.1 基本用法
      2. 1.4.2. 4.2 与 *ngFor 配合
      3. 1.4.3. 4.3 多个 Observable 的组合
    5. 1.5. 5. 取消订阅策略:防止内存泄漏
      1. 1.5.1. 5.1 async 管道(最佳:零手动管理)
      2. 1.5.2. 5.2 takeUntil + Subject 模式(最通用的手动方案)
      3. 1.5.3. 5.3 take(1)(仅需第一个值)
      4. 1.5.4. 5.4 Subscription.add()(手动管理订阅池)
      5. 1.5.5. 5.5 untilDestroyed(需要 @ngneat/until-destroy 第三方库)
    6. 1.6. 6. 综合实战:带防抖搜索的产品列表服务
    7. 1.7. 课后练习
      1. 1.7.1. 一、概念自测(选择题 / 填空题)
      2. 1.7.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.7.3. 三、Agent 模式下的提示词示例
      4. 1.7.4. 四、面试真题与参考答案
    8. 1.8. 课后练习答案
      1. 1.8.1. 一、概念自测答案
      2. 1.8.2. 二、AI 编程任务参考答案(提示词示例)