异步到底是什么?
同步:你去奶茶店点单,站在柜台前等,直到拿到奶茶才离开(一步等一步,后面的事全停)。
对应代码: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 想象成你手机上的公众号推送,完全对应它的核心逻辑:
先看一个最简单的 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 是 可订阅的事件流,还能主动控制,比如加防抖,过滤,转换等。
代码中from 和 fromEvent 都是 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 内的顺序:retry → timeout → catchError。
-
第一步:
from(axios.get)把 Promise 转成 Observable,准备发起请求; -
第二步(retry):先监听错误,但此时还没执行请求,只是注册了 “重试规则”;
-
第三步(timeout):启动 5 秒计时器,然后发起请求;
- 如果请求 5 秒内成功:直接把数据传给下游,
catchError不执行; - 如果请求 5 秒内失败(超时 / 接口报错):抛出错误,交给上游的
retry;
- 如果请求 5 秒内成功:直接把数据传给下游,
-
第四步(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 只有等结果出来了才能处理。