为什么你需要 RxJS?
如果你曾用 addEventListener 处理过复杂的交互逻辑,或者用 Promise 链式调用过多个异步请求,你可能会遇到这样的困境:取消操作困难、组合多个数据源繁琐、错误处理分散。RxJS 提供了一种统一的范式——把一切异步事件视为“流”,通过操作符进行声明式组合。
本文不会罗列所有操作符,而是通过一个真实场景:实时搜索(输入关键词,防抖后请求 API,并处理竞态),带你掌握 RxJS 的核心 20% 知识,解决 80% 的问题。
环境准备
我们使用 TypeScript 和 RxJS 7.x。创建一个新项目:
mkdir rxjs-search-demo && cd rxjs-search-demo
npm init -y
npm install rxjs typescript ts-node @types/node
npx tsc --init --target es2017 --module commonjs
为了模拟 API,我们写一个简单的函数,返回 Promise 并随机延迟:
// mock-api.ts
export function searchApi(keyword: string): Promise<string[]> {
const delay = Math.random() * 1000 + 200;
return new Promise((resolve) => {
setTimeout(() => {
resolve([`${keyword}-结果1`, `${keyword}-结果2`, `${keyword}-结果3`]);
}, delay);
});
}
第一步:创建流——从事件到 Observable
RxJS 的核心是 Observable,它代表一个可被观察的数据流。我们可以用 fromEvent 将 DOM 事件转为流:
import { fromEvent } from 'rxjs';
const input = document.createElement('input');
document.body.appendChild(input);
const input$ = fromEvent<InputEvent>(input, 'input');
input$.subscribe(event => {
console.log('输入值:', (event.target as HTMLInputElement).value);
});
注意:$ 后缀是社区约定,表示这是一个 Observable,不是强制语法。
但直接订阅会为每次击键触发,我们需要更精细的控制。
第二步:操作符管道——声明式处理流
操作符是返回新 Observable 的纯函数。我们用 pipe 将它们串联:
import { fromEvent, map, debounceTime, distinctUntilChanged } from 'rxjs';
const search$ = fromEvent<InputEvent>(input, 'input').pipe(
map(e => (e.target as HTMLInputElement).value.trim()),
debounceTime(300), // 等待300ms无新输入
distinctUntilChanged() // 仅在值变化时发出
);
search$.subscribe(keyword => {
console.log('准备搜索:', keyword);
});
map:转换事件为输入值debounceTime:防抖,避免频繁请求distinctUntilChanged:跳过重复值
💡 提示:操作符顺序至关重要。如果先 distinctUntilChanged 再 debounceTime,防抖会作用于去重后的流,效果不同。通常先防抖再去重更符合直觉。
第三步:切换流——解决竞态条件
当用户快速输入时,多个请求可能乱序返回。我们需要“取消”之前的请求,只保留最新结果。这时用 switchMap:
import { fromEvent, map, debounceTime, distinctUntilChanged, switchMap } from 'rxjs';
import { from } from 'rxjs';
import { searchApi } from './mock-api';
const results$ = fromEvent<InputEvent>(input, 'input').pipe(
map(e => (e.target as HTMLInputElement).value.trim()),
debounceTime(300),
distinctUntilChanged(),
switchMap(keyword => from(searchApi(keyword))) // 将Promise转为Observable
);
results$.subscribe(results => {
console.log('搜索结果:', results);
});
switchMap 会订阅最新的内部 Observable,并取消前一个。这是处理搜索、自动补全等场景的黄金法则。
注意:switchMap 适合“取消旧请求”,但如果你需要并行请求且不取消(如上传多个文件),应使用 mergeMap。
第四步:错误处理与重试
网络请求可能失败。RxJS 的错误会终止流,我们需要 catchError 来恢复:
import { catchError, retry } from 'rxjs/operators';
import { of } from 'rxjs';
const results$ = fromEvent<InputEvent>(input, 'input').pipe(
map(e => (e.target as HTMLInputElement).value.trim()),
debounceTime(300),
distinctUntilChanged(),
switchMap(keyword =>
from(searchApi(keyword)).pipe(
retry(2), // 失败重试2次
catchError(err => {
console.error('搜索失败:', err);
return of([]); // 返回空数组,保持流继续
})
)
)
);
💡 最佳实践:catchError 应放在内部管道中,这样单个请求失败不会终止整个搜索流。
第五步:管理订阅——避免内存泄漏
手动 subscribe 需要手动取消。在组件中,我们通常用 Subscription 或 takeUntil 来管理:
import { Subject, takeUntil } from 'rxjs';
const destroy$ = new Subject<void>();
results$.pipe(takeUntil(destroy$)).subscribe(results => {
// 更新UI
});
// 组件销毁时
destroy$.next();
destroy$.complete();
对于 Angular 用户,可以使用 async 管道自动管理。在 React 中,可以在 useEffect 的清理函数中取消订阅。
进阶:组合多个流
实际场景中,我们可能需要同时监听输入和按钮点击。用 combineLatest 或 withLatestFrom:
import { combineLatest } from 'rxjs';
const filter$ = fromEvent<InputEvent>(filterInput, 'input').pipe(
map(e => (e.target as HTMLInputElement).value)
);
const combined$ = combineLatest([search$, filter$]).pipe(
map(([keyword, filter]) => ({ keyword, filter }))
);
combineLatest 会在任意源发出时,组合最新值。适合表单多字段联动。
常见坑与避坑指南
- 忘记订阅:Observable 是冷启动,不订阅不会执行。
- 操作符顺序错误:如
debounceTime放在switchMap之后会导致请求无法取消。 - 无限流:
interval等无限流需配合take或takeUntil使用。 - 错误处理位置:在外部
catchError会终止整个流,应尽量在内部处理。 - 性能:大量数据流时,使用
share或shareReplay避免重复订阅。
总结
RxJS 的学习曲线主要在于思维转变:从“命令式处理事件”到“声明式组合流”。核心要点:
- 用
fromEvent、from、of创建流。 - 用
pipe和操作符进行转换、过滤、组合。 - 用
switchMap处理竞态,catchError恢复错误。 - 用
takeUntil管理订阅生命周期。
下一步,你可以探索 Subject 实现多播、scan 实现状态管理,或结合框架(Angular、React)的集成模式。记住:不要为了用 RxJS 而用 RxJS,当你的异步逻辑变得复杂时,它才是利器。
延伸阅读
- RxJS 官方文档:https://rxjs.dev/
- 弹珠图理解操作符:https://rxmarbles.com/
- 学习资源:RxJS in Action 一书