对rxjs的理解和基本使用

73 阅读5分钟

异步到底是什么?

同步:你去奶茶店点单,站在柜台前等,直到拿到奶茶才离开(一步等一步,后面的事全停)。 对应代码:const res = fetchData(); console.log(res);, 如果 fetchData 是同步的,代码会卡着等它执行完。

异步:你点单后拿了个取餐号,不用站着等,能去旁边玩手机 / 逛超市,等奶茶做好了,店员喊你的号你再去拿,不用等,能做别的事。 对应代码:fetchData().then(res => console.log(res));,发起请求后,代码继续往下走,等数据回来再执行回调。

前端天天接触的异步场景:

  • 接口请求(fetch/axios):发起请求后不用等,数据回来才执行 then 里的逻辑;
  • 定时器(setTimeout):设定时间后,代码继续走,到点才执行回调;
  • 事件监听(click/scroll):页面加载后代码走完,用户触发事件才执行逻辑。

rxjs 核心概念

核心三要素:生产者、观察者、订阅。

  • 生产者(Observable) :产生数据的源头(比如定时器、接口请求、事件);

  • 观察者(Observer) :接收数据的消费者(包含 next/error/complete 三个方法);

  • 订阅(subscribe) :连接生产者和观察者的开关——只有订阅后,Observable 才会开始产生数据(这是和 Promise 最大的区别之一:Promise 一旦创建就立即执行,Observable 是 “懒执行”)。

把 rxjs 想象成你手机上的公众号推送,完全对应它的核心逻辑:

image.png

先看一个最简单的 Observable 示例,结合前端熟悉的场景:

// 1. 引入 RxJS 核心模块(NestJS 已内置,无需额外安装)
import { Observable } from 'rxjs';

// 2. 创建一个 Observable(生产者:每隔1秒产生一个数字)
const numberStream$ = new Observable((observer) => {
  let count = 0;
  // 模拟异步产生数据
  const timer = setInterval(() => {
    count++;
    // 发送正常数据(对应观察者的 next 方法)
    observer.next(count);
    
    // 模拟完成(产生3个值后结束)
    if (count === 3) {
      observer.complete(); // 通知数据流结束
      clearInterval(timer);
    }
    
    // 模拟错误(可选)
    // if (count === 2) {
    //   observer.error(new Error('数字超过2了!'));
    //   clearInterval(timer);
    // }
  }, 1000);

  // 取消订阅时执行(清理资源,比如定时器)
  return () => {
    clearInterval(timer);
    console.log('订阅已取消,清理定时器');
  };
});

// 3. 订阅 Observable(观察者:消费数据)
const subscription = numberStream$.subscribe({
  next: (value) => console.log('收到值:', value), // 接收正常数据
  error: (err) => console.error('出错了:', err),   // 接收错误
  complete: () => console.log('数据流结束'),        // 接收完成通知
});

// 4. 可选:取消订阅(比如组件销毁时,避免内存泄漏)
// setTimeout(() => {
//   subscription.unsubscribe();
// }, 2500); // 2.5秒后取消,只会收到1、2两个值

执行结果:

收到值: 1 
收到值: 2 
收到值: 3 
数据流结束

再来看一个监听按钮点击:

// 传统事件监听(异步、多值,但不好控制)
const btn = document.querySelector('button');
// 订阅(监听)点击事件
const handleClick = () => console.log('按钮被点了');
btn.addEventListener('click', handleClick);

// 取消订阅(移除监听)
// btn.removeEventListener('click', handleClick);

现在用 rxjs 实现一模一样的功能,你看有多像:

import { fromEvent } from 'rxjs';

// 1. 创建 Observable(对应公众号)
const btnClick$ = fromEvent(document.querySelector('button'), 'click');

// 2. 订阅(对应关注公众号)
const subscription = btnClick$.subscribe({
  next: () => console.log('按钮被点了'), // 对应收到推文
  error: (err) => console.error('出错了', err), // 对应公众号出问题
  complete: () => console.log('不会执行,因为点击事件不会结束'),
});

// 3. 取消订阅(对应取关)
// subscription.unsubscribe();

传统事件监听是浏览器内置的异步逻辑,而 Observable 是把这种异步逻辑封装成了可复用、可操作的对象 ,比如你可以给这个点击流加防抖

import { fromEvent } from 'rxjs';
import { debounceTime } from 'rxjs/operators';

// 给点击事件加 1 秒防抖(前端超常用)
const btnClick$ = fromEvent(document.querySelector('button'), 'click').pipe(
  debounceTime(1000) // 1 秒内连续点击只认最后一次
);

btnClick$.subscribe(() => console.log('防抖后的点击'));

这就是 rxjs 的优势:把零散的异步事件变成数据流,可以像操作数组一样加各种规则(防抖、过滤、转换)

事件监听(如 click)能产生多个值,但是传统的写法只能被动监听,但是 rxjs 是 可订阅的事件流还能主动控制,比如加防抖,过滤,转换等

代码中fromfromEvent 都是 RxJS 里用来快速创建 Observable 的核心 API,目的是把你熟悉的普通数据 / 事件转换成 Observable 数据流,这样就能用上 RxJS 丰富的操作符(比如防抖、过滤、重试)。

它的好处是对比手动写 new Observable()from/fromEvent 一行就搞定

怎么理解 rxjs

Promise 是前端最熟悉的异步工具,但它有个硬伤:只能干一次活,比如接口请求成功只返回一次数据。

举个生活例子:

  • Promise 就像点一杯奶茶:点完要么拿到(resolve),要么做不了(reject),就这一次结果;
  • Observable 就像订每日鲜奶:每天固定时间送,能送很多次,你不想订了还能取消(unsubscribe),送奶工就不送了。

再对应前端场景:

  • Promise 适合:一次接口请求、一次文件上传(只需要一次结果);
  • Observable 适合:实时聊天消息(持续收消息)、滚动事件(持续触发)、WebSocket 通信(持续收发数据)、后端分批查数据库(分批返回数据),这些都是持续产生数据的场景。

在前端中input框的实时输入就是这种持续产生数据的场景:

import { fromEvent } from 'rxjs';
import { debounceTime, map, switchMap } from 'rxjs/operators';
import axios from 'axios';

// 把输入框 input 事件转成 Observable
const input = document.querySelector('input');
const input$ = fromEvent(input, 'input');

// 防抖 + 发请求搜索
input$.pipe(
  debounceTime(500), // 500ms 防抖
  map((e: InputEvent) => (e.target as HTMLInputElement).value), // 提取输入值
  switchMap(keyword => axios.get(`/api/search?keyword=${keyword}`)) // 发请求(自动取消前一次请求)
).subscribe(res => console.log('搜索结果:', res.data));

这种持续输入,多次请求的场景,用 Promise 写需要手动管理请求取消、防抖逻辑,而 RxJS 几行操作符就搞定。

在Node.js中处理文件流 / 网络流:

import { fromEvent } from 'rxjs';
import { createReadStream } from 'fs';

// 读取大文件(可持续产生数据块)
const stream = createReadStream('./big-file.txt');
fromEvent(stream, 'data').pipe(
  // 对每个数据块做处理(比如解析、过滤)
).subscribe(chunk => console.log('读取到数据块', chunk));

你可能会问:那为什么很多 NestJS 代码里,单次接口请求也用 RxJS?

答案是:**RxJS 能给一次性数据叠加 增强能力 **,比如:

import { from } from 'rxjs';
import { retry, timeout, catchError } from 'rxjs/operators';

// 单次接口请求(Promise)→ 转 Observable → 加重试/超时/错误处理
from(axios.get('/api/users')).pipe(
  retry(3), // 失败自动重试3次
  timeout(5000), // 5秒超时报错
  catchError(() => from([{ name: '默认用户' }])) // 错误兜底
).subscribe(res => console.log(res));

代码执行流程是严格按 pipe 内的顺序retrytimeoutcatchError

  • 第一步:from(axios.get) 把 Promise 转成 Observable,准备发起请求;

  • 第二步(retry):先监听错误,但此时还没执行请求,只是注册了 “重试规则”;

  • 第三步(timeout):启动 5 秒计时器,然后发起请求;

    • 如果请求 5 秒内成功:直接把数据传给下游,catchError 不执行;
    • 如果请求 5 秒内失败(超时 / 接口报错):抛出错误,交给上游的 retry
  • 第四步(retry 处理错误):

    • 若重试次数没到 3 次:重新发起请求,回到第三步(重置 timeout 计时);
    • 若重试次数到 3 次:停止重试,把错误传递给下游的 catchError
  • 第五步(catchError):捕获所有上游错误(超时 / 重试耗尽 / 接口报错),返回一个包含 “默认用户” 的新 Observable,避免流中断,最终把默认数据传给 subscribe

注意:retry 重试的是整个上游 :每次重试都会重新执行 from(axios.get),且重新走 timeout 逻辑(5 秒计时重置);

这些能力(重试、超时、错误兜底)用 Promise 也能实现,但需要手动写很多代码,而 RxJS 用现成的操作符就能一键叠加。

这是 RxJS 的附加价值,但不是它的核心场景

我们在 nestjs 中,从拦截器 interceptor中来体会 rxjs 的核心价值。

拦截器的本质是对 请求 → 处理 → 响应 这个异步流程做拦截和修改,而 RxJS 擅长处理流式的异步过程

你可以把这个流程想象成一条水管,RxJS 就是 水管上的各种阀门 / 过滤器,能在数据流动的任意环节做修改,且不破坏整体流程。

后端返回的数据经常需要统一格式(比如加 code/message/data),用 RxJS 的 map 操作符能极简实现:

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

// 统一响应格式的拦截器
@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // next.handle() 返回 Observable(请求处理的数据流)
    return next.handle().pipe(
      // 对响应数据做「流式转换」
      map((data) => ({
        code: 200,
        message: 'success',
        data: data,
      }))
    );
  }
}

需要知道的是:拦截器里的核心对象是 Observable。NestJS 拦截器的 intercept 方法中,next.handle() 返回的不是 Promise,而是 Observable,它代表请求处理的整个异步数据流(从请求进入到响应返回的全过程)。

不用等请求完全结束(Promise 只能等结果出来再处理),而是在「响应数据流返回的过程中」直接修改数据,逻辑更简洁,且能处理「持续响应」(比如 WebSocket 推送的多批数据)。

不用等请求完全结束(Promise 只能等结果出来再处理),而是在「响应数据流返回的过程中」直接修改数据,逻辑更简洁,且能处理「持续响应」(比如 WebSocket 推送的多批数据)。

不用等请求完全结束(Promise 只能等结果出来再处理),而是在「响应数据流返回的过程中」直接修改数据,逻辑更简洁,且能处理「持续响应」(比如 WebSocket 推送的多批数据)。

说三遍。

介绍几个常用的创建流的方法

of

  • 作用:创建一个同步的 Observable,发出指定的值,然后完成。
  • 适用场景:当你有一组已知的值,想将它们作为数据流发出时。
import { of } from 'rxjs';

// 创建一个 Observable,发出 1, 2, 3,然后完成
const numbers$ = of(1, 2, 3);

numbers$.subscribe({
    next: value => console.log('收到值:', value),
    complete: () => console.log('数据流完成')
});

// 输出:
// 收到值:1
// 收到值:2
// 收到值:3
// 数据流完成

这里需要理解数据流的概念,数据是一下一下的传递的,而不是一下子全部发出去的。

from

  • 作用:将数组、Promise、迭代器等转换为流。
  • 适用场景:当你有一个数组、Promise 或其他可迭代对象,想将其转换为 流 时。
import { from } from 'rxjs';

// 将 Promise 转换为 Observable
const promise$ = from(new Promise(resolve => {
    setTimeout(() => resolve('Hello, RxJS!'), 1000);
}));

promise$.subscribe({
    next: value => console.log('收到值:', value),
    complete: () => console.log('数据流完成')
});

// 输出:
// (1 秒后)
// 收到值:Hello, RxJS!
// 数据流完成

interval

  • 作用:创建一个定时器流,每隔一段时间发出一个递增的数字。
  • 适用场景:当你需要定期执行某个操作时(比如轮询)。
import { interval } from 'rxjs';

// 每隔 1 秒发出一个数字,从 0 开始
const timer$ = interval(1000);

timer$.subscribe(value => {
    console.log('当前值:', value);
});

// 输出:
// 当前值:0
// 当前值:1
// 当前值:2
// (每隔 1 秒输出一次)

timer

  • 作用:创建一个定时器流,可以指定延迟时间和间隔时间。
  • 适用场景:当你需要延迟执行某个操作。

fromEvent

  • 作用:将 DOM 事件转换为流。
  • 适用场景:当你需要监听 DOM 事件(如点击、输入等)时。

// 监听按钮的点击事件
const button = document.querySelector('button');
const click$ = fromEvent(button, 'click');

click$.subscribe(() => {
    console.log('按钮被点击了!');
});

介绍几个常用的操作符(Operators)

1. map

  • 作用:对 流 发出的每个值进行转换。
  • 使用场景:当你需要对数据流中的每个值进行处理时。
const numbers$ = of(1, 2, 3);

numbers$.pipe(
    map(x => x * 2) // 将每个值乘以 2
).subscribe(value => {
    console.log('转换后的值:', value);
});

// 输出:
// 转换后的值:2
// 转换后的值:4
// 转换后的值:6

2. filter

  • 作用:筛选出满足条件的值。
  • 使用场景:当你需要过滤数据流中的某些值时。
const numbers$ = of(1, 2, 3, 4, 5);

numbers$.pipe(
    filter(x => x % 2 === 0) // 只保留偶数
).subscribe(value => {
    console.log('筛选后的值:', value);
});

// 输出:
// 筛选后的值:2
// 筛选后的值:4

3. debounceTime vs throttleTime

  • debounceTime :事件触发后,等待 n 毫秒内没有新事件,才发射最后一个值(防抖动)
  • throttleTime: n 毫秒内只发射第一个事件(节流)。
const input = document.querySelector('input');
const input$ = fromEvent(input, 'input');

input$.pipe(
    debounceTime(500), // 防抖 500 毫秒
    map(event => event.target.value) // 提取输入框的值
).subscribe(value => {
    console.log('用户输入:', value);
});

4. tap

  • 作用:对 流 发出的每个值执行副作用操作,但不改变值。
  • 使用场景:当你需要在数据流中执行一些操作(如日志记录)时。
const numbers$ = of(1, 2, 3);

numbers$.pipe(
    tap(value => console.log('当前值:', value)), // 记录日志
    map(x => x * 2)
).subscribe(value => {
    console.log('转换后的值:', value);
});

// 输出:
// 当前值:1
// 转换后的值:2
// 当前值:2
// 转换后的值:4
// 当前值:3

总结

rxjs 有发布者 observable,消费者 observer,订阅 subscribe。 这个发布者observable就是产生数据的源头,它可以源源不断的产生数据,组成数据流,在最终的消费 next 之前,可以对这个多次产生的数据(数据流)做各种处理。而 promise 只有等结果出来了才能处理。