RxJS 六個 map 系列 operator 的差別:map、mergeMap/flatMap、concatMap、switchMap、exhaustMap
文字解析 · 1 支影片 · 產生於 2026-10-01 00:54
章節(8)
1. Outline
- 起點 · Map, switchMap, mergeMap, flatMap, concatMap, exhaustMap in RxJS - what is the difference?
用同一個實驗骨架(來源 0 到 4、每個內層 delay 500ms)依序換上 mergeMap、concatMap、switchMap、exhaustMap,讓 console 的輸出差異替四個 operator 下定義,最後回到真實的 API 串接情境解釋為什麼換 operator 也照樣能跑。
2. YouTuber 的思維推導
Map, switchMap, mergeMap, flatMap, concatMap, exhaustMap in RxJS - what is the difference?
Monsterlessons Academy · 9m48s · 字幕 en · vision=on (整支影片是編輯器與 console 的對照 demo,作者反覆說「你看瀏覽器裡」「這裡我改成 concatMap」,輸出結果(一次全出 / 逐一出現 / 只剩一個)只有看畫面才分得出來,所以 vision 開啟。) · YouTube
預估 vs 實際耗時
| 階段 | 預估 | 實際 |
|---|---|---|
| fetch | 6s | 1s |
| segment | 50s | – |
| shot | 24s | 24s |
| analyze | 3m45s | 6m52s |
| render | 5s | – |
作者的出發點不是「這些 operator 分別做什麼」,而是「為什麼看完文件還是選不出來」。他判斷混亂的根源在於:文件是一頁一個 operator 寫的,每頁都在解釋單獨一個的行為,讀者永遠拿不到彼此的對照;而 Stack Overflow 上同一個問題有人用 switchMap、有人用 concatMap 且都能跑,更讓人懷疑差別是不是根本不存在。於是他採取一個實驗室的作法:先把 map 從六個裡切出去,因為 map 只是把值換成另一個值,其他五個是拿到值以後再產生一個新的 Observable,兩者根本不同類;接著他搭一個固定骨架——來源固定是 from([0,1,2,3,4]),內層固定是 of(x).pipe(delay(500)),印出的 log 固定,唯一的變數是 pipe 裡那一個 operator。
因為來源是同步的、五個值幾乎同時抵達,而每個值的工作都要花 500 毫秒,五個工作必然重疊,operator 怎麼安排這些重疊的工作就完全暴露在 console 的輸出上:mergeMap 全部一起跑(0~4 同時出現)、concatMap 排隊(一個一個出現)、switchMap 新的砍舊的(只剩 4)、exhaustMap 忙的時候擋掉新的(只剩 0)。四種輸出各自不同,差別因此不必背、用看的就記得住。最後他回到一開始的困惑,用「先拿 user 再拿 user details」這個最常見的串接情境示範:concatMap 換成 switchMap 結果一模一樣。
他的結論是這不代表兩者沒差,而是這個情境本身分不出來——HTTP 請求只吐一個值就結束,從頭到尾只有一個內層 Observable,沒有東西需要等、也沒有東西需要取消。也就是說,選哪個 operator 取決於來源會不會在前一個工作結束前再吐值;不會的時候怎麼選都對,會的時候才分勝負。
推理鏈:每段留給下一段的線索
每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。
- 六個 map 為什麼分不清 → 既然六個裡有一個根本不同類,第一步就是先把 map 拆出去單獨講完、確認它為什麼不該跟其他五個放在一起比。
- map:純轉換,不建新的 observable → map 排除之後,剩下五個的共同點是 callback 都回傳新的 Observable,而它們的差別只會在「多個新 Observable 同時存在」的時候顯現。所以下一步必須先造出一個能讓多個工作重疊、又能用肉眼看出時間差的固定實驗場。
- 共用實驗骨架:0 到 4 加 delay 500 → 骨架就緒,唯一沒定的是 pipe 裡那個 operator。合理的起點是先放最寬鬆、對已經在跑的工作完全不干涉的那一個,看看「全部放行」長什麼樣子。
- mergeMap / flatMap:全部同時開,誰也不取消 → 全部平行的直接後果是順序不保證,而且無法用前一個工作的結果去做下一件事。如果需求正好相反——非得等前一個結束才能開始下一個——就必須有人負責排隊。
- concatMap:排隊,等前一個完成 → 排隊保證了順序也保證了每筆都做到,但它的前提是「每一筆都還有價值」。如果新的值一出現就代表舊的工作已經沒意義了——例如搜尋框裡使用者又多打了一個字——那麼把舊工作做完不只是浪費,還可能讓過期的結果蓋掉新的。這種情況需要的是相反的策略。
- switchMap:新的來了就砍掉舊的 → switchMap 的取捨很清楚:永遠保留最新、丟掉舊的。既然有人選最新,就會有相反的需求——已經在做的事不准被打斷、後來的請求寧可不要。最後一個 operator 要回答的就是這一端。
- exhaustMap:忙的時候,後來的一律忽略 → 四種策略都攤在桌上了,但這樣就回到第 1 段那個原始困惑:既然差異這麼明顯,為什麼 Stack Overflow 上不同人用不同 operator 解同一個問題,卻都能跑出正確結果?最後要用一個真實情境把這個矛盾解掉。
- 真實例子:為何 concatMap 和 switchMap 結果一樣
3. 逐段說明
Map, switchMap, mergeMap, flatMap, concatMap, exhaustMap in RxJS - what is the difference?
1. 六個 map 為什麼分不清 0:00–0:51
作者先講痛點:RxJS 裡有 map、mergeMap、flatMap、concatMap、switchMap、exhaustMap 六個名字很像的 operator。看文件只能懂單一個,仍不知道何時該用哪個;上 Stack Overflow 又會看到不同人用不同 operator 解同一個問題,於是懷疑它們到底有沒有差。因此他決定用同一個簡單例子把全部差異一次講清楚。
推理因為作者假設觀眾早就會用這些 operator 的語法、卡住的是「該選哪一個」,所以他不逐個介紹功能,而是先把混亂的來源攤開來講:文件一頁只講一個 operator,看完只懂單一個、仍然不知道何時用哪個;跑去 Stack Overflow 又看到不同人用不同 operator 解同一題,於是懷疑它們到底有沒有差。既然痛點是「缺乏對照」,他的解法就只能是「用同一個例子把六個一次比出來」,這決定了整支影片的形式。
AI 補充值得補一句作者沒明說、但決定了後面順序的事:這六個名字雖然並排出現,其實是一比五。map 做的事是把每個值換成另一個值;另外五個做的事是拿到值以後再生出一個新的 Observable,然後把那個 Observable 吐出來的東西攤平回主線。只要牽涉到「同時有好幾個工作在跑」,就會出現誰等誰、誰砍誰、誰被忽略的問題,而 map 從來不會遇到這件事。所以真正需要比較的其實只有五個,而且比較的軸只有一條:新工作來的時候,正在跑的舊工作怎麼辦。
2. map:純轉換,不建新的 observable 0:51–2:13
作者先把 map 從其他五個裡切出來:map 只是把 stream 裡每個值換成另一個值,不需要 subscribe。他用 from 建了 1 到 5 的 stream,在 pipe 裡用 map 把每個 item 乘以十,訂閱後 console 印出 10、20、30、40、50。結論是 map 跟其他所有 map 是完全不同的東西。


推理因為上一段的判斷是「六個其實是一比五」,所以作者開場第一句就是「map 跟其他所有 map 完全不一樣」,然後用最小的例子證明這句話:from 建一個 1 到 5 的來源,pipe 裡放 map,把每個 item 乘以十,subscribe 之後 console 依序出現 10、20、30、40、50。他特別強調「不需要用到 subscribe」,指的是不必為了改值而自己訂閱、也不必在 map 裡再訂閱另一個 Observable——map 拿到值、回傳新值,事情就結束了。畫面上 10 到 50 五個值一次列完、彼此之間沒有任何間隔,正好證明 map 不會產生任何等待或取消的行為。
- CObservable:描述會隨時間吐出零到多個值的物件,要被 subscribe 才會真的開始跑→ 畫地圖
- Cmap:把每個值換成另一個值,callback 回傳普通值不是 Observable,沒有等待/取消問題→ 畫地圖
- Rpipe 裡的 operator 何時才真的執行→ 存+回想
AI 補充作者說「完全不一樣」但沒說破差在哪裡,補上這一步:差別在 callback 的回傳值。map 的 callback 回傳一個普通的值(item * 10 是數字 50),RxJS 直接把它送給下游。另外五個的 callback 回傳的是一個新的 Observable,於是主線上流的東西變成「Observable 裡面裝著 Observable」,必須有人決定怎麼把裡層攤平——要不要等、要不要取消、要不要忽略,五個 operator 的分歧全部從這裡長出來。map 回傳的是值不是 Observable,所以它根本沒有這個決策點,也就沒有什麼好比的。另外一個容易忽略的細節:畫面上 foo$ 是寫在 class 的屬性、subscribe 寫在 constructor,這代表 pipe(map(...)) 那一行只是「描述」,在有人 subscribe 之前完全不會執行,這也是後面所有實驗都要靠 subscribe 才看得到 console 輸出的原因。
3. 共用實驗骨架:0 到 4 加 delay 500 2:13–3:06
要比較剩下四個 operator,作者先搭一個固定的測試骨架:一個叫 example 的函式,收一個 operator 當參數,內部用 from 產生 0、1、2、3、4,在 pipe 裡呼叫傳進來的 operator,再接一個 delay 500 毫秒,最後印出成功、失敗與完成。之後只換 operator、其他都不動。

推理因為上一段確認了剩下五個 operator 的差別只在「多個工作重疊時怎麼安排」,所以作者做了一件實驗方法上最關鍵的事:固定所有變因,只留 operator 一個變數。畫面上的 example 函式收一個 operator 當參數,內部永遠是 from([0,1,2,3,4])、永遠在 pipe 裡呼叫傳進來的那個 operator、內層永遠是 of(x).pipe(delay(500))、永遠印出成功/失敗/完成。之後每一段他只改一個字,console 的變化就只能歸因於那個字。他說 delay 五百毫秒「很重要」,正是因為沒有這個延遲,五個工作瞬間就結束、彼此不會重疊,四個 operator 的輸出會長得一模一樣。
AI 補充作者說了 delay 很重要卻沒說重要在哪,補上這一步:看畫面上的寫法是 operator((x: any) => of(x).pipe(delay(500))),延遲是掛在「每個值各自產生的那個小 Observable」上,不是掛在外層的 from 上。這一點決定了整個實驗的結構——外層 from 在訂閱瞬間就把 0 到 4 同步吐完,五個值幾乎同時抵達 operator;而每個值要產生的工作各自需要 500 毫秒才結束。於是必然出現「第二個值到達時,第一個值的工作還在跑」的局面,而這正是五個 operator 唯一有分歧的場景。換句話說,這個骨架是刻意製造衝突用的。另外 console 最後那行「xxx completed」是模板字串 operator.name 印出來的,也就是函式自己的名字,所以每次換 operator,那行字會自動跟著換——之後可以靠它確認跑的是哪一個。
術語:inner Observable
4. mergeMap / flatMap:全部同時開,誰也不取消 3:06–4:55
flatMap 只是 mergeMap 的別名。跑起來的結果是:等五百毫秒之後,0 到 4 幾乎同時出現在 console,然後 mergeMap completed。原因是每個進來的值都立刻建立一個新的 observable,而且先前的 observable 全部保持存活,五個 observable 是同時開始延遲的,所以一起結束。

推理因為上一段刻意讓五個值幾乎同時抵達、每個工作各花五百毫秒,所以只要看 console 的節奏就能反推 operator 做了什麼。畫面上的結果是:等了五百毫秒,0、1、2、3、4 一次全部湧出,然後才是 mergeMap completed。作者由此反推出兩條規則:第一,每個值一進來就立刻建立內層 Observable,中間不等待;第二,先前建立的內層全部保持存活、沒有任何一個被移除。五個內層在幾乎同一瞬間開始各自的五百毫秒,自然也在幾乎同一瞬間結束,這就是「等一下子然後全部一起出現」的由來。他接著把 mergeMap 改寫成 flatMap,輸出完全相同,因為 flatMap 只是 mergeMap 的別名。
- C高階串流:值本身又是 Observable,四個攤平 operator 都是 map 產生它再用某種策略攤平→ 畫地圖
- CmergeMap:每個值一進來就立刻建內層、全部並行,不等也不取消,順序不保證→ 畫地圖
- RflatMap 只是 mergeMap 的舊別名→ 存+回想
- AmergeMap 接在高頻事件上會像不設限的服務窗口越開越多→ 批判類比
AI 補充作者示範的畫面剛好掩蓋了 mergeMap 一個重要性質,這裡要補:mergeMap 不保證輸出順序。它的輸出順序是「哪個內層先吐值就先送誰」,而不是來源的順序。這個例子裡五個內層的延遲都是同樣的 500 毫秒、又同時開始,所以看起來剛好是 0 到 4;如果把 delay 改成隨機值,console 很可能變成 3、0、4、1、2。真實情境更是如此——五個 HTTP 請求同時發出,回來的順序取決於伺服器,不取決於你發送的順序。這正是「不等」的代價:換來最快的總時間,但失去順序保證。另外要補一個作者沒提、實務上常用的安全閥:mergeMap 有第二個參數 concurrent,可以限制同時活著的內層數量,例如 mergeMap(fn, 3) 表示最多三個同時跑、其餘排隊。把它設成 1 的時候,行為就等同於下一種 operator 的「排隊」。
術語:higher-order Observable
「flat map is an alias for merge map and it is duplicated inside either xjs」
5. concatMap:排隊,等前一個完成 4:55–5:55
只把 mergeMap 換成 concatMap,0 到 4 一樣全部出現,但是一個一個慢慢出來,之間看得到延遲。因為 concatMap 會等前一個 observable 完成才建立下一個:0 的 observable 延遲五百毫秒結束後,才輪到 1,接著 2、3、4。要「拿前一個結果再做下一件事」時就用它。

推理因為上一段已經確認骨架的變因只有 operator 一個,所以作者只把 concatMap 這個字換上去、其他一行都沒動。console 的結果是 0 到 4 全部都在,一個都沒少,但這次是一個接一個慢慢出現,中間看得到明顯的間隔,最後才是 concatMap completed。值一個都沒少代表沒有人被取消或忽略;出現的節奏被拉開代表它們不是同時跑的。兩件事合起來只有一種解釋:concatMap 會等前一個內層完成才建立下一個。0 的內層跑滿五百毫秒結束後,1 的內層才被建立,接著才輪到 2、3、4,總共花掉大約兩秒半。作者因此給出使用時機:當你要拿前一個 Observable 的結果去做下一件事、而且非等不可的時候用它;mergeMap 則完全不等。
- CconcatMap:同一時間只跑一個內層,排隊等前一個完成,一個都不丟但可能無限排隊→ 畫地圖
- C完成通知:Observable 宣告不再吐值的結束訊號;被取消不會觸發它,是兩種不同結束→ 畫地圖
- RconcatMap 排隊策略的隱藏風險→ 存+回想
AI 補充這裡要補兩個作者沒提、但實務上會踩到的前提。第一,「等前一個完成」的前提是內層真的會完成。骨架裡的內層是 of(x).pipe(delay(500)),吐一個值就結束,所以隊伍能一直往前推進;如果內層換成 interval 這種永遠不會完成的 Observable,第一個工作會永遠佔著位置,後面的值就永遠排不到,整條串流看起來像卡死。第二,佇列本身沒有上限。concatMap 會把來不及處理的值全部留著,來源吐值的速度只要長期快過處理速度,佇列就會無限增長、記憶體跟著漲,而且使用者看到的延遲會越來越久。所以 concatMap 適合的是「每一筆都不能丟、順序又不能亂」的場景,例如依序送出離線期間累積的操作、或是必須照順序寫入的日誌,而不是綁在使用者連續輸入這種高頻事件上。
術語:complete
6. switchMap:新的來了就砍掉舊的 5:55–6:37
換成 switchMap,console 只印出 4,然後 completed。因為 switchMap 每次拿到新值都會取消掉前一個還沒完成的 observable:1 取消 0、2 取消 1,一路下去,最後只剩 4 活著跑完延遲。

推理因為上一段建立了「值一個都不能少」作為對照基準,所以這一段的 console 特別刺眼:只印出一個 4,然後 switchMap completed。作者從這個結果反推:明明五個值都進到了 operator、也都各自建立了內層,卻只有一個活到最後,唯一的解釋是每次新值抵達時,switchMap 會取消掉前一個還沒完成的內層。0 的內層建立後不到一毫秒,1 就到了,0 被取消;1 立刻被 2 取消,2 被 3 取消,3 被 4 取消。4 之後來源就沒有值了,沒有人來取消它,於是只有 4 安穩跑完五百毫秒、印出來。
- CswitchMap:新值一到就取消前一個還沒完成的內層,只保留最新的那一個→ 畫地圖
- C取消訂閱:中止進行中的訂閱;被取消不等於完成,清理要寫在 finalize→ 畫地圖
- RswitchMap 是唯一能真正省流量的策略→ 存+回想
AI 補充作者用「取消」帶過,這裡補上它的機制與後果。switch 的意思是切換訂閱:新值一到,switchMap 對舊的內層 unsubscribe,然後訂閱新的內層。關鍵是被取消訂閱和自然完成不一樣——被取消的內層不會發出完成通知,所以掛在它上面的 complete 處理不會執行,這是很多人 debug 時的困惑點;要在被取消時也做清理(例如關閉載入動畫),得用 finalize 而不是 complete。另一個實務上的好處是取消會往下傳遞:真實的 HttpClient 請求被取消訂閱時,瀏覽器會直接中止那個 HTTP 連線,不只是丟掉結果而已,所以 switchMap 是唯一能真正省下網路流量的策略。這也解釋了它最經典的用途:搜尋框的即時建議。使用者每多打一個字就發一次請求,舊的關鍵字結果已無意義且可能比新的晚回來、把正確結果蓋掉,switchMap 一次解決了浪費與競態兩個問題。
7. exhaustMap:忙的時候,後來的一律忽略 6:37–7:18
換成 exhaustMap,console 只印出 0。因為第一個 observable 還沒完成之前,後面所有的值都被忽略、不會建立新的 observable。它和 switchMap 剛好相反:switchMap 留最新的,exhaustMap 留最早的。

推理因為上一段建立了「只剩最後一個」這個極端,所以作者把 exhaustMap 換上去之後的畫面正好是鏡像:console 只印出一個 0,然後 exhaustMap completed。他反推的邏輯是,0 進來時沒有任何內層在跑,所以順利建立了帶延遲的內層;接下來 1、2、3、4 每一個都想建立新的內層,但都建立不了,因為 exhaustMap 看到已經有一個內層還在進行中,就把這些值直接忽略掉。等到 0 的五百毫秒跑完,來源早就結束了,也就沒有下一個值可以接手。於是四個 operator 在同一個骨架下給出四種可以互相對照的輸出:全部、逐一、只剩最新、只剩最早。
- CexhaustMap:內層還沒完成時忽略所有新值,不建新內層也不排隊,是 switchMap 的鏡像→ 畫地圖
- RexhaustMap 最典型的用途→ 存+回想
- E同一骨架下四種輸出:全部、逐一、只剩最新、只剩最早→ 存+演練
AI 補充補上名字與判準。exhaust 的意思是「把目前這個耗盡」——在現有內層耗盡(完成)之前,新來的值一律丟棄,而且是靜悄悄地丟,不會報錯也不會排隊,這一點和排隊策略最容易混淆:兩者都是同時只跑一個,但一個把後來的存起來、一個把後來的扔掉。最典型的用途是防止重複觸發:送出按鈕連點兩下只送一次、token 過期時多個請求同時要求續期只實際續一次、下拉更新在上一次還沒回來前不重複發。把四個 operator 放在同一張表上就記得住了——都不管別人的是 mergeMap,把別人排在後面的是 concatMap,砍掉舊的留新的是 switchMap,擋掉新的留舊的是 exhaustMap。要注意這四者的差別只有在「前一個還沒結束、下一個就來了」的時候才存在。
8. 真實例子:為何 concatMap 和 switchMap 結果一樣 7:18–9:48
回到最常見的情境:先打 getUser,再用結果打 getUserDetails。用 concatMap 完全正確,console 印出 id 1、age 30。但把它換成 switchMap,結果一模一樣——這正是大家在 Stack Overflow 看到的混亂來源。作者的解釋是:HTTP 請求只會發出一個值就完成,根本沒有「前一個 observable」需要取消,所以兩者在這個情境無法分辨;換成別的情境就會有差。



推理因為前面四段的差異都是在人造的高頻來源下逼出來的,所以作者刻意換一個真實情境來檢驗:先呼叫 getUser 拿到使用者,再把使用者傳進 getUserDetails 拿細節——畫面上 getUserDetails 的簽章必須吃一個 UserInterface,這就是「非得等前一個結果」的典型。他用 concatMap 寫,並說這完全正確,因為 concatMap 的定義就是等前一個 Observable 的結果再呼叫下一個;console 印出 id 1、age 30。接著他把 concatMap 換成 switchMap,重新載入,console 一模一樣。這正是他在開場說的、Stack Overflow 上讓人困惑的畫面。他的解釋是:在這個情境裡根本不存在「前一個內層」需要取消,因為 HTTP 請求只會吐一個值就結束,所以取消與否毫無差別,兩個 operator 自然無法分辨;但顯然不是所有情況都這樣。
AI 補充作者的結論對,但他把原因歸給「HTTP 只吐一個值」其實不夠精確,真正的判準在外層而不是內層。四個 operator 的分歧只發生在「內層還沒結束、外層又吐了下一個值」的時候。這裡外層是 getUser('1'),只吐一個 user 就完成,從頭到尾只有一個內層被建立,四個策略全都無事可做——把 mergeMap 或 exhaustMap 換上去,結果也會是同一個 id 1、age 30。所以正確的判斷順序是先問「來源會不會連續吐值」:像這樣一次性的請求,四個怎麼選都對,選 switchMap 或 concatMap 純粹是習慣;但只要來源換成按鈕點擊、輸入框、路由參數、輪詢,四個立刻分出勝負,而且選錯會產生只在使用者手快時才重現的 bug。順帶補一個示範與真實環境的落差:畫面上 getUser 回傳的是 of(user),它是同步完成的,沒有任何網路往返;真正的 HttpClient 是非同步的,而且每次訂閱都會重新發一次請求(第 2 段講的、要 subscribe 才會執行的性質),所以在真實程式裡多訂閱一次就會多打一次 API,這也是為什麼取消訂閱能省下實際流量。
術語:cold Observable
「switch map completes the previous observable and creates a new one」
4. 總結
作者的推理鏈是一條直線:混亂的根源是文件一頁只講一個 operator、永遠沒有對照 → 所以要用同一個例子把它們並排比 → 但六個裡 map 根本不同類(它只換值、不建立新的 Observable),先拆出去講完 → 剩下五個的共同點是 callback 回傳新的 Observable,差別只在「多個新 Observable 同時存在時怎麼安排」,所以造一個固定骨架:來源 0 到 4 幾乎同時抵達、每個內層各花 500 毫秒,只換 pipe 裡那一個字 → mergeMap 全部放行、互不干涉,於是 0 到 4 一次湧出 → 要有順序就得排隊,concatMap 等前一個完成,於是逐一出現 → 只要最新的就取消舊的,switchMap 只留下 4 → 反過來不准被打斷,exhaustMap 只留下 0 → 四種策略(全部/逐一/只剩最新/只剩最早)攤開後,回頭解掉開場的矛盾:真實的 HTTP 請求只送一個值就完成,根本沒有「前一個內層」可以取消或排隊,所以在單發請求的串接上 concatMap 與 switchMap 看起來一樣,差別只有在來源會連續發值時才顯現。
勘誤總整理
確定錯誤/已過時見仁見智(取決於版本或情境)
| 段落 | 原話(transcript 逐字) | 說明 |
|---|---|---|
| 4. mergeMap / flatMap:全部同時開,誰也不取消 3:10 |
「flat map is an alias for merge map and it is duplicated inside either xjs」 | 「flatMap 是 mergeMap 的別名」本身正確,但影片把它講成兩個可以互換使用的等價選項,沒有提到 flatMap 從 RxJS 7 起就被標記為 deprecated、官方明講會在下一個主版本移除。照影片改寫成 flatMap 今天多半仍能跑,但編輯器與建置工具會出現棄用警告,新程式碼應一律寫 mergeMap。 依據: RxJS 7 deprecations 清單:flatMap renamed to mergeMap, will be removed in v8 |
| 8. 真實例子:為何 concatMap 和 switchMap 結果一樣 8:58 |
「switch map completes the previous observable and creates a new one」 | switchMap 對前一個內層做的是取消訂閱(unsubscribe),不是讓它完成(complete)。兩者結果不同:被取消的內層不會發出完成通知,subscribe 的第三個 callback 與 last()、toArray() 這類等待完成的 operator 都不會觸發,清理要改寫在 finalize 裡。把取消講成完成,容易讓人以為舊請求的完成處理仍會照跑。 依據: RxJS 官方 switchMap 說明:on each emission the previous inner observable is cancelled |
5. 推薦三個下一步
1. 往下挖深:取消到底發生了什麼
影片說 switchMap 會「取消」前一個內層、exhaustMap 會「忽略」後來的值,但沒有講取消在底層是什麼——unsubscribe 怎麼往上傳、HTTP 請求會不會真的被 abort、teardown 函式何時執行。不補這一塊,就只能記結論、無法自己推斷沒看過的情境。
YouTube 搜尋:RxJS unsubscribe teardown explained RxJS switchMap cancel HTTP request RxJS subscription lifecycle
2. 往旁邊對照:其他 flattening operator 與 Promise 的對照
同一條「多個非同步工作怎麼安排」的軸線上還有 combineLatest、withLatestFrom、forkJoin、mergeMap 的 concurrent 參數,以及 Promise.all/for await 的相同取捨。把它們並排,才知道這四種策略是 RxJS 特有還是併發問題的通則。
YouTube 搜尋:RxJS combineLatest forkJoin withLatestFrom difference RxJS mergeMap concurrency limit Promise.all vs sequential await
3. 往上應用:搜尋框、按鈕連點與競態條件
影片最後說「單發請求分不出來」,等於把問題丟回真實情境:什麼時候來源才會連續發值。搜尋框輸入(switchMap + debounceTime)、送出按鈕連點(exhaustMap)、上傳佇列(concatMap)正是那些情境,也是 stale response 蓋掉新結果這類 bug 的來源。
YouTube 搜尋:RxJS typeahead switchMap debounceTime RxJS exhaustMap prevent double click submit race condition stale response frontend
- 接著看 ←Embrace Reactive Programming in React and Vue with RxJS - Fahad Ibnay Heylaal · 前站建立 Observable/pipe/operator,本站專攻 map 系列的併發取捨
- 相關 —JavaScript Visualized - Promise Execution · 同樣在講多個非同步工作的排隊與競態,一個用 Promise、一個用 Observable
- 接著看 →RxJS operators and their dangerous consequences · 前站挑對 operator,本站講同一批 operator 在邊界情況的陷阱