switchMap、mergeMap、concatMap 的核心区别
这三个操作符都是用于处理高阶 Observable(Observable of Observable)的,但它们的处理策略完全不同:
1. switchMap
特点: 取消之前的内部 Observable,只处理最新的
工作原理:
- 当新的值到达时,取消之前未完成的 Observable
- 只保留最新的 Observable 的结果
- 适合需要取消旧请求的场景
示例代码:
javascriptimport { of, interval } from 'rxjs'; import { switchMap, take, delay } from 'rxjs/operators'; // 模拟异步请求 function makeRequest(id) { return of(`Result ${id}`).pipe(delay(1000)); } interval(500).pipe( switchMap(id => makeRequest(id)), take(5) ).subscribe(console.log); // 输出: Result 4 // 解释: 因为每500ms触发一次,但请求需要1000ms // 所以每次新请求来时,之前的请求都被取消了 // 最终只有最后一个请求完成了
实际应用场景:
- 搜索框输入:每次输入都取消上一次的搜索请求
- 自动完成:只显示最新输入的结果
- 导航切换:取消未完成的页面加载
javascript// 搜索框示例 searchInput.pipe( switchMap(query => searchAPI(query)) ).subscribe(results => { // 只显示最新搜索的结果 displayResults(results); });
2. mergeMap
特点: 并行处理所有内部 Observable
工作原理:
- 同时订阅所有内部 Observable
- 所有 Observable 的结果都会被发出
- 不保证顺序,结果可能交错
示例代码:
javascriptimport { of } from 'rxjs'; import { mergeMap, delay } from 'rxjs/operators'; function makeRequest(id) { return of(`Result ${id}`).pipe(delay(id * 200)); } of(1, 2, 3).pipe( mergeMap(id => makeRequest(id)) ).subscribe(console.log); // 输出: Result 1, Result 2, Result 3 // 解释: 所有请求并行执行,按完成顺序输出
实际应用场景:
- 并行加载多个资源
- 批量处理独立任务
- 不需要顺序的并发请求
javascript// 并行加载用户数据示例 merge( getUserProfile(userId), getUserPosts(userId), getUserComments(userId) ).pipe( mergeMap(response => response.json()) ).subscribe(data => { // 所有数据并行加载完成 updateUI(data); });
3. concatMap
特点: 顺序处理内部 Observable,一个完成后再处理下一个
工作原理:
- 按顺序订阅内部 Observable
- 当前 Observable 完成后才订阅下一个
- 保证结果的顺序
示例代码:
javascriptimport { of } from 'rxjs'; import { concatMap, delay } from 'rxjs/operators'; function makeRequest(id) { return of(`Result ${id}`).pipe(delay(500)); } of(1, 2, 3).pipe( concatMap(id => makeRequest(id)) ).subscribe(console.log); // 输出: Result 1, Result 2, Result 3 // 解释: 每个请求按顺序执行,前一个完成后才执行下一个
实际应用场景:
- 需要保证顺序的请求
- 依赖前一个结果的后续请求
- 防止服务器过载
javascript// 顺序上传文件示例 files.pipe( concatMap(file => uploadFile(file)) ).subscribe(result => { // 文件按顺序上传 console.log('Uploaded:', result); });
对比总结
| 特性 | switchMap | mergeMap | concatMap |
|---|---|---|---|
| 执行方式 | 取消旧的,只保留最新的 | 并行执行所有 | 顺序执行 |
| 结果顺序 | 只保留最新结果 | 不保证顺序 | 保证顺序 |
| 并发数 | 1(同时只有1个) | 无限制 | 1(同时只有1个) |
| 适用场景 | 搜索、自动完成 | 并行加载 | 顺序处理 |
| 性能 | 最快(取消旧请求) | 最快(并行) | 较慢(顺序) |
| 内存占用 | 低 | 高 | 低 |
选择指南
使用 switchMap 当:
- 需要取消旧请求
- 只关心最新结果
- 搜索、自动完成等场景
- 避免不必要的网络请求
使用 mergeMap 当:
- 需要并行处理
- 请求之间没有依赖
- 需要最大化性能
- 不关心结果顺序
使用 concatMap 当:
- 需要保证顺序
- 请求之间有依赖
- 需要限制并发数
- 避免服务器过载
性能考虑
javascript// 性能对比示例 const source = interval(100).pipe(take(10)); // switchMap: 只处理最后一个 source.pipe( switchMap(x => of(x).pipe(delay(1000))) ).subscribe(); // mergeMap: 并行处理所有(可能造成性能问题) source.pipe( mergeMap(x => of(x).pipe(delay(1000))) ).subscribe(); // concatMap: 顺序处理(可能较慢) source.pipe( concatMap(x => of(x).pipe(delay(1000))) ).subscribe();
实际项目中的选择
javascript// 1. 搜索功能 - 使用 switchMap searchInput.pipe( debounceTime(300), distinctUntilChanged(), switchMap(query => searchAPI(query)) ).subscribe(results => displayResults(results)); // 2. 批量加载 - 使用 mergeMap productIds.pipe( mergeMap(id => getProductDetails(id)) ).subscribe(products => renderProducts(products)); // 3. 顺序操作 - 使用 concatMap commands.pipe( concatMap(command => executeCommand(command)) ).subscribe(result => logResult(result));
注意事项
- 内存泄漏: mergeMap 可能创建大量并发请求,需要注意内存使用
- 取消逻辑: switchMap 会自动取消,但 concatMap 和 mergeMap 不会
- 错误处理: 任何一个内部 Observable 出错都会导致整个流失败
- 取消订阅: 始终记得取消订阅以避免内存泄漏
最佳实践
javascript// 1. 结合错误处理 searchInput.pipe( switchMap(query => searchAPI(query).pipe( catchError(error => of([])) ) ) ).subscribe(results => displayResults(results)); // 2. 限制并发数(使用 mergeMap) import { mergeMap } from 'rxjs/operators'; source.pipe( mergeMap(value => process(value), 3) // 限制并发数为3 ).subscribe(); // 3. 添加重试机制 source.pipe( switchMap(value => apiCall(value).pipe( retry(3), catchError(error => of(defaultValue)) ) ) ).subscribe();