1. map 操作符
map
是最基本的轉換操作符,用于對 Observable 發出的每個值進行一對一轉換。
基本特點:
- 同步操作
- 一對一轉換
- 不改變 Observable 的發出時機
詳細示例:
import { of } from 'rxjs';
import { map } from 'rxjs/operators';// 示例1:簡單數值轉換
of(1, 2, 3).pipe(map(x => x * 2)
).subscribe(result => console.log(result));
// 輸出: 2, 4, 6// 示例2:對象屬性轉換
const users = of({ id: 1, name: 'Alice', age: 25 },{ id: 2, name: 'Bob', age: 30 }
);users.pipe(map(user => ({...user,name: user.name.toUpperCase(),isAdult: user.age >= 18}))
).subscribe(console.log);
/* 輸出:
{id: 1, name: "ALICE", age: 25, isAdult: true}
{id: 2, name: "BOB", age: 30, isAdult: true}
*/// 示例3:API響應處理
import { from } from 'rxjs';function fetchUser(userId) {return from(fetch(`https://jsonplaceholder.typicode.com/users/${userId}`).then(response => response.json()));
}fetchUser(1).pipe(map(user => user.name)
).subscribe(name => console.log('用戶名:', name));
// 輸出: 用戶名: Leanne Graham
2. mergeMap (flatMap) 操作符
mergeMap
用于將一個值映射成一個新的 Observable,并同時訂閱所有這些內部 Observable,合并它們的輸出。
基本特點:
- 一對多轉換
- 同時維護多個內部訂閱
- 不取消之前的訂閱
- 適合并行處理
詳細示例:
import { of, interval } from 'rxjs';
import { mergeMap, take } from 'rxjs/operators';// 示例1:將每個值轉換為新的Observable
of('a', 'b', 'c').pipe(mergeMap(letter => interval(1000).pipe(take(3),map(i => letter + i)))
).subscribe(console.log);
/* 輸出 (每秒一個):
a0
b0
c0
a1
b1
c1
a2
b2
c2
*/// 示例2:實際API請求
function fetchPosts(userId) {return from(fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`).then(response => response.json()));
}of(1, 2, 3).pipe(mergeMap(userId => fetchPosts(userId))
).subscribe(posts => {console.log(`用戶${posts[0]?.userId}的帖子數:`, posts.length);
});
/* 輸出:
用戶1的帖子數: 10
用戶2的帖子數: 10
用戶3的帖子數: 10
*/// 示例3:結合Promise使用
function uploadFile(file) {return new Promise(resolve => {setTimeout(() => {resolve(`文件${file.name}上傳成功`);}, 1000);});
}const files = of({ name: 'document.pdf' },{ name: 'image.jpg' }
);files.pipe(mergeMap(file => from(uploadFile(file)))
).subscribe(result => console.log(result));
/* 輸出 (大約同時):
文件document.pdf上傳成功
文件image.jpg上傳成功
*/
3. switchMap 操作符
switchMap
會在每次發出新值時取消之前的內部 Observable 訂閱,并訂閱新的 Observable。
基本特點:
- 一對多轉換
- 只保留最新的內部訂閱
- 自動取消之前的訂閱
- 適合搜索輸入等場景
詳細示例:
import { fromEvent, interval } from 'rxjs';
import { switchMap, take } from 'rxjs/operators';// 示例1:基礎用法
fromEvent(document.getElementById('searchInput'), 'input').pipe(switchMap(event => {const query = event.target.value;return from(fetch(`https://api.example.com/search?q=${query}`).then(res => res.json()));})
).subscribe(results => {console.log('搜索結果:', results);
});// 示例2:與interval結合
const source$ = of('start').pipe(switchMap(() => interval(1000).pipe(take(5),map(i => `值 ${i}`)))
);source$.subscribe(console.log);
/* 輸出 (每秒一個):
值 0
值 1
值 2
值 3
值 4
*/// 示例3:實際應用 - 取消之前的請求
function searchBooks(query) {return from(fetch(`https://openlibrary.org/search.json?q=${query}`).then(res => res.json()));
}const searchInput = document.getElementById('bookSearch');
fromEvent(searchInput, 'input').pipe(map(event => event.target.value),filter(query => query.length > 2), // 至少3個字符才搜索debounceTime(300), // 防抖300msdistinctUntilChanged(), // 值變化才繼續switchMap(query => searchBooks(query))
).subscribe(results => {console.log('找到書籍:', results.docs);
});
4. tap 操作符
tap
用于在 Observable 流中執行副作用操作,不影響數據流。
基本特點:
- 用于調試、日志記錄
- 執行副作用但不修改數據
- 不影響原始流
詳細示例:
import { of } from 'rxjs';
import { tap, map } from 'rxjs/operators';// 示例1:基本調試
of(1, 2, 3).pipe(tap(value => console.log('收到原始值:', value)),map(x => x * 2),tap(value => console.log('轉換后的值:', value))
).subscribe();
/* 輸出:
收到原始值: 1
轉換后的值: 2
收到原始值: 2
轉換后的值: 4
收到原始值: 3
轉換后的值: 6
*/// 示例2:實際應用 - 記錄API請求
function getUser(userId) {return from(fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)).pipe(tap(response => {console.log(`請求用戶${userId}的狀態:`, response.status);if (!response.ok) {throw new Error('請求失敗');}}),switchMap(response => from(response.json())));
}getUser(1).pipe(tap(user => {console.log('成功獲取用戶:', user.name);// 可以在這里更新UI狀態document.getElementById('loading').style.display = 'none';}),catchError(error => {console.error('獲取用戶失敗:', error);return of(null);})
).subscribe();// 示例3:狀態管理
let requestCount = 0;function fetchData() {return from(fetch('/api/data')).pipe(tap(() => {requestCount++;console.log(`當前請求數: ${requestCount}`);document.getElementById('spinner').style.display = 'block';}),switchMap(response => response.json()),tap(() => {document.getElementById('spinner').style.display = 'none';}));
}
綜合對比示例
import { of, fromEvent } from 'rxjs';
import { map, mergeMap, switchMap, tap, delay } from 'rxjs/operators';// 模擬API函數
function simulateApi(id, delayTime) {return of(`結果 ${id}`).pipe(delay(delayTime));
}// 對比mergeMap和switchMap
const button = document.getElementById('myButton');fromEvent(button, 'click').pipe(map((_, index) => index + 1), // 點擊次數tap(count => console.log(`點擊 #${count}`)),// 嘗試切換這兩個操作符查看區別// mergeMap(count => simulateApi(count, 1000))switchMap(count => simulateApi(count, 1000))
).subscribe(result => console.log('收到:', result));/*
使用mergeMap時的可能輸出:
點擊 #1
點擊 #2
點擊 #3
收到: 結果 1
收到: 結果 2
收到: 結果 3使用switchMap時的可能輸出:
點擊 #1
點擊 #2 (在1秒內點擊)
點擊 #3 (在1秒內點擊)
收到: 結果 3 (只收到最后一個結果)
*/
實際應用場景總結
-
map:
- 簡單數據轉換
- 提取對象屬性
- 格式化數據
-
mergeMap:
- 并行處理多個請求
- 文件上傳
- 需要保留所有內部訂閱的場景
-
switchMap:
- 搜索輸入自動完成
- 取消之前的HTTP請求
- 路由導航
-
tap:
- 調試和日志記錄
- 執行UI更新等副作用
- 監控狀態變化
記住選擇操作符的關鍵:
- 是否需要并行處理?→
mergeMap
- 是否需要取消之前的請求?→
switchMap
- 是否需要簡單轉換?→
map
- 是否需要執行副作用?→
tap
在 RxJS 的 pipe
中,操作符的執行順序是從上到下、從左到右的線性執行流程。讓我們通過你提到的 map
、tap
和 switchMap
組合來分析它們的執行順序。
基本執行順序規則
- 數據流經 pipe 的順序:從第一個操作符開始,依次傳遞給后續操作符
- 同步操作符(如
map
、tap
):立即執行 - 異步/高階操作符(如
switchMap
):會創建新的 Observable - 每個操作符都會接收上一個操作符的輸出
典型組合示例分析
import { fromEvent } from 'rxjs';
import { map, tap, switchMap } from 'rxjs/operators';fromEvent(document.getElementById('searchInput'), 'input').pipe(tap(event => console.log('1. 原始事件:', event)), // 第1步map(event => event.target.value), // 第2步tap(value => console.log('2. 提取的值:', value)), // 第3步switchMap(query => fetch(`/api/search?q=${query}`)) // 第4步
).subscribe(response => {console.log('5. 最終結果:', response); // 第5步
});
詳細執行流程
假設用戶在搜索輸入框中輸入 “rxjs”:
-
tap(event => console.log('1. 原始事件:', event))
- 最先執行
- 打印原始的 input 事件對象
- 不影響數據流,將原樣傳遞事件對象
-
map(event => event.target.value)
- 從事件對象中提取輸入框的值
- 將
event
轉換為字符串值(如 “rxjs”)
-
tap(value => console.log('2. 提取的值:', value))
- 打印提取后的值
- 不影響數據流,原樣傳遞值
-
switchMap(query => fetch(
/api/search?q=${query}))
- 高階操作,會做以下事情:
a. 取消之前可能未完成的 fetch 請求(如果是連續輸入)
b. 發起新的 fetch 請求
c. 將 Promise 轉換為 Observable - 等待 API 響應
- 高階操作,會做以下事情:
-
subscribe 回調
- 最后執行
- 收到 fetch 的響應對象
可視化執行順序
輸入事件 → tap(記錄原始事件) → map(提取值) → tap(記錄值) → switchMap(發起請求) → 訂閱接收響應
關鍵注意事項
-
switchMap
的特殊行為:- 當新值到達時,會取消前一個內部 Observable
- 這意味著如果用戶快速連續輸入,只有最后一次請求會被處理
-
tap
的位置影響:// 示例1:tap在switchMap之前 .pipe(tap(x => console.log('before switchMap', x)),switchMap(x => fetchData(x)) )// 示例2:tap在switchMap之后 .pipe(switchMap(x => fetchData(x)),tap(x => console.log('after switchMap', x)) )
- 示例1的 tap 記錄的是 switchMap 前的值
- 示例2的 tap 記錄的是 API 返回的數據
-
錯誤處理:
- 如果
switchMap
內部的 Observable 出錯,錯誤會跳過后續操作符直接到達 subscribe 的 error 回調
- 如果
實際場景執行示例
假設我們有一個搜索框,用戶依次輸入:r → rx → rxj → rxjs
fromEvent(searchInput, 'input').pipe(debounceTime(300),map(event => event.target.value),tap(query => console.log(`正在處理查詢: ${query}`)),switchMap(query => from(fetch(`/api/search?q=${query}`).then(res => res.json()))
).subscribe({next: results => console.log('搜索結果:', results),error: err => console.error('搜索失敗:', err)
});
可能的執行過程:
-
用戶輸入 ‘r’ → 300ms 沒有新輸入 → 觸發搜索
- tap: “正在處理查詢: r”
- switchMap: 發起搜索 “r” 的請求
-
用戶在 300ms 內繼續輸入 ‘rx’ → 取消 ‘r’ 的請求
- tap: “正在處理查詢: rx”
- switchMap: 發起搜索 “rx” 的請求
-
用戶繼續快速輸入 ‘rxj’ → 取消 ‘rx’ 的請求
- tap: “正在處理查詢: rxj”
- switchMap: 發起搜索 “rxj” 的請求
-
用戶最后輸入 ‘rxjs’ 并停止 300ms → 取消 ‘rxj’ 的請求
- tap: “正在處理查詢: rxjs”
- switchMap: 發起搜索 “rxjs” 的請求
- 最終只有 “rxjs” 的搜索結果會到達 subscribe
總結執行順序要點
- 線性流水線處理:數據像水流一樣依次通過每個操作符
- 同步操作符立即執行:map 和 tap 會立即對每個值進行處理
- switchMap 是異步邊界:它會開啟一個新的異步上下文
- 訂閱只看到最終結果:subscribe 收到的是經過整個管道處理后的結果
- 錯誤會跳過后續步驟:任何操作符出錯都會直接跳到錯誤處理