RxJS 实战:使用 switchMap 处理请求竞态条件
概述
在用户交互频繁的应用中,经常会遇到竞态条件(Race Condition)问题。比如用户快速点击按钮,或者输入框快速输入,可能会触发多个请求,而这些请求的响应顺序是不确定的。本章将介绍如何使用 RxJS 的 switchMap 操作符来优雅地解决这个问题。
什么是竞态条件?
竞态条件是指多个异步操作同时进行,但它们的完成顺序不确定,导致最终结果可能不是我们期望的。在 Web 开发中,常见的竞态条件场景包括:
- 快速点击按钮:用户快速点击"搜索"按钮,触发多个搜索请求
- 输入框输入:用户快速输入,每次输入都触发请求
- 标签页切换:用户快速切换标签页,每个标签页都发起数据请求
switchMap 操作符简介
switchMap 是 RxJS 中处理竞态条件的利器。它的特点是:当新的值到达时,会自动取消之前未完成的内部 Observable。
基本语法
source$.pipe(
switchMap(value => createObservable(value))
)
关键特性
- 自动取消:新的值到达时,自动取消之前的 Observable
- 只保留最新:只处理最新的请求,忽略之前的请求
- 避免竞态:确保最终结果对应最新的操作
实战场景:处理快速点击请求
假设我们有一个按钮,点击后会随机调用不同的 API(delay1、delay2、delay3)。如果用户快速点击,我们希望只处理最后一次点击的请求。
实现思路
- 使用
Subject作为请求触发器 - 使用
switchMap处理请求,自动取消之前的请求 - 记录请求历史,展示哪些请求被取消了
核心代码
// 请求触发 Subject
private requestTrigger$ = new Subject<string>();
// 销毁 Subject
private destroy$ = new Subject<void>();
ngOnInit(): void {
// 使用 switchMap 处理请求竞态条件
this.requestTrigger$
.pipe(
switchMap((apiUrl) => {
const api = this.apis.find(a => a.url === apiUrl);
const apiName = api?.name || 'unknown';
// 创建请求记录
const recordId = ++this.requestCounter;
const record: RequestRecord = {
id: recordId,
apiName,
apiUrl,
startTime: Date.now(),
status: 'pending'
};
// 将之前的 pending 请求标记为 cancelled
this.requestRecords.forEach(r => {
if (r.status === 'pending') {
r.status = 'cancelled';
r.endTime = Date.now();
}
});
this.requestRecords.unshift(record);
this.loading = true;
this.currentApiName = apiName;
this.cdr.detectChanges();
return this.http.get<DelayApiResponse>(`${this.apiBaseUrl}${apiUrl}`)
.pipe(
catchError(err => {
// 捕获错误
console.error(`请求 ${apiUrl} 失败:`, err);
return of({
success: false,
message: err.message || '请求失败',
data: {
delay: null as any,
timestamp: new Date().toISOString(),
info: '请求失败'
}
} as DelayApiResponse);
})
);
}),
takeUntil(this.destroy$)
)
.subscribe({
next: (response) => {
// 找到最新的 pending 请求记录
const latestRecord = this.requestRecords.find(r => r.status === 'pending');
if (latestRecord) {
latestRecord.status = 'completed';
latestRecord.endTime = Date.now();
latestRecord.response = response;
}
this.currentResult = response;
this.loading = false;
this.cdr.detectChanges();
},
error: (err) => {
// 处理错误
const latestRecord = this.requestRecords.find(r => r.status === 'pending');
if (latestRecord) {
latestRecord.status = 'completed';
latestRecord.endTime = Date.now();
latestRecord.error = err.message || '请求失败';
}
this.loading = false;
this.cdr.detectChanges();
}
});
}
// 触发请求
triggerRequest(): void {
const randomApi = this.apis[Math.floor(Math.random() * this.apis.length)];
this.requestTrigger$.next(randomApi.url);
}
关键点解析
1. switchMap 的取消机制
当 requestTrigger$ 发出新值时,switchMap 会:
- 取消之前未完成的 HTTP 请求(如果可能)
- 开始新的请求
- 只处理最新请求的响应
2. 请求记录管理
通过维护请求记录列表,我们可以:
- 追踪每个请求的状态(pending、completed、cancelled)
- 展示请求历史
- 分析哪些请求被取消了
3. 错误处理
使用 catchError 确保单个请求的错误不会中断整个流,而是返回一个错误响应对象。
与其他操作符的对比
switchMap vs mergeMap
| 特性 | switchMap | mergeMap |
|---|---|---|
| 行为 | 取消之前的请求 | 并发执行所有请求 |
| 适用场景 | 只需要最新结果 | 需要所有结果 |
| 资源占用 | 低(只保留一个请求) | 高(可能多个请求) |
switchMap vs concatMap
| 特性 | switchMap | concatMap |
|---|---|---|
| 行为 | 取消之前的请求 | 按顺序执行 |
| 适用场景 | 只需要最新结果 | 需要保证顺序 |
| 执行方式 | 中断之前的请求 | 等待之前的请求完成 |
实际应用场景
1. 搜索输入框
// 用户输入时,只处理最新的搜索请求
searchInput.valueChanges.pipe(
debounceTime(300), // 防抖
distinctUntilChanged(), // 去重
switchMap(query =>
this.searchService.search(query)
)
).subscribe(results => {
// 只显示最新搜索的结果
this.searchResults = results;
});
2. 标签页切换
// 切换标签时,取消之前标签的数据请求
tabSwitch$.pipe(
switchMap(tabId =>
this.loadTabData(tabId)
)
).subscribe(data => {
// 只显示当前标签的数据
this.currentTabData = data;
});
3. 路由参数变化
// 路由参数变化时,取消之前的数据请求
route.params.pipe(
switchMap(params =>
this.loadData(params.id)
)
).subscribe(data => {
// 只显示最新路由的数据
this.data = data;
});
性能优化建议
1. 结合防抖使用
对于输入框场景,可以结合 debounceTime 使用:
input$.pipe(
debounceTime(300),
switchMap(value => this.request(value))
)
2. 添加加载状态
通过维护加载状态,可以给用户更好的反馈:
switchMap(apiUrl => {
this.loading = true;
return this.http.get(apiUrl).pipe(
finalize(() => this.loading = false)
);
})
3. 错误重试
对于可能失败的请求,可以添加重试:
switchMap(apiUrl =>
this.http.get(apiUrl).pipe(
retry(2),
catchError(err => of(null))
)
)
注意事项
- 取消请求的限制:HTTP 请求的取消取决于浏览器和 HTTP 客户端的支持
- 副作用处理:如果请求有副作用(如创建资源),需要谨慎使用
switchMap - 用户体验:频繁取消请求可能会让用户困惑,需要适当的 UI 反馈
总结
switchMap 是处理竞态条件的强大工具,它通过自动取消之前的请求来确保只处理最新的操作。在实际项目中,合理使用 switchMap 可以:
- 避免竞态条件:确保结果对应最新的操作
- 节省资源:取消不必要的请求,减少服务器压力
- 提升用户体验:只显示最新的结果,避免混乱
记住:当你只需要最新结果时,使用 switchMap;当你需要所有结果时,使用 mergeMap 或 forkJoin。