RxJS 响应式编程入门:从事件流到状态管理的实战指南

By | 2026年9月26日

为什么你需要 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 会在任意源发出时,组合最新值。适合表单多字段联动。

常见坑与避坑指南

  1. 忘记订阅:Observable 是冷启动,不订阅不会执行。
  2. 操作符顺序错误:如 debounceTime 放在 switchMap 之后会导致请求无法取消。
  3. 无限流:interval 等无限流需配合 take 或 takeUntil 使用。
  4. 错误处理位置:在外部 catchError 会终止整个流,应尽量在内部处理。
  5. 性能:大量数据流时,使用 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 一书