RxJS 六個 map 系列 operator 的差別:map、mergeMap/flatMap、concatMap、switchMap、exhaustMap

文字解析 · 1 支影片 · 產生於 2026-10-01 00:54

🎧 語音解析📝 筆記✍️ 練習

🎧 語音解析 20m05s · TTS 講解與作者原聲交錯;點章節可跳。
邊聽邊看逐句講稿,點任一句從那裡開始
章節(8)

1. Outline

  1. 起點 · 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 取決於來源會不會在前一個工作結束前再吐值;不會的時候怎麼選都對,會的時候才分勝負。

推理鏈:每段留給下一段的線索

每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。

  1. 六個 map 為什麼分不清 → 既然六個裡有一個根本不同類,第一步就是先把 map 拆出去單獨講完、確認它為什麼不該跟其他五個放在一起比。
  2. map:純轉換,不建新的 observable → map 排除之後,剩下五個的共同點是 callback 都回傳新的 Observable,而它們的差別只會在「多個新 Observable 同時存在」的時候顯現。所以下一步必須先造出一個能讓多個工作重疊、又能用肉眼看出時間差的固定實驗場。
  3. 共用實驗骨架:0 到 4 加 delay 500 → 骨架就緒,唯一沒定的是 pipe 裡那個 operator。合理的起點是先放最寬鬆、對已經在跑的工作完全不干涉的那一個,看看「全部放行」長什麼樣子。
  4. mergeMap / flatMap:全部同時開,誰也不取消 → 全部平行的直接後果是順序不保證,而且無法用前一個工作的結果去做下一件事。如果需求正好相反——非得等前一個結束才能開始下一個——就必須有人負責排隊。
  5. concatMap:排隊,等前一個完成 → 排隊保證了順序也保證了每筆都做到,但它的前提是「每一筆都還有價值」。如果新的值一出現就代表舊的工作已經沒意義了——例如搜尋框裡使用者又多打了一個字——那麼把舊工作做完不只是浪費,還可能讓過期的結果蓋掉新的。這種情況需要的是相反的策略。
  6. switchMap:新的來了就砍掉舊的 → switchMap 的取捨很清楚:永遠保留最新、丟掉舊的。既然有人選最新,就會有相反的需求——已經在做的事不准被打斷、後來的請求寧可不要。最後一個 operator 要回答的就是這一端。
  7. exhaustMap:忙的時候,後來的一律忽略 → 四種策略都攤在桌上了,但這樣就回到第 1 段那個原始困惑:既然差異這麼明顯,為什麼 Stack Overflow 上不同人用不同 operator 解同一個問題,卻都能跑出正確結果?最後要用一個真實情境把這個矛盾解掉。
  8. 真實例子:為何 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 解同一個問題,於是懷疑它們到底有沒有差。因此他決定用同一個簡單例子把全部差異一次講清楚。

承上 前情提要裡假設你已經寫過 RxJS:知道 Observable 要 subscribe 才會動、知道 pipe 裡可以串 operator,也在專案裡遇過「先打一支 API、拿到結果再打下一支」的需求。作者直接從這個既有經驗的痛點切入,不從語法教起。

推理因為作者假設觀眾早就會用這些 operator 的語法、卡住的是「該選哪一個」,所以他不逐個介紹功能,而是先把混亂的來源攤開來講:文件一頁只講一個 operator,看完只懂單一個、仍然不知道何時用哪個;跑去 Stack Overflow 又看到不同人用不同 operator 解同一題,於是懷疑它們到底有沒有差。既然痛點是「缺乏對照」,他的解法就只能是「用同一個例子把六個一次比出來」,這決定了整支影片的形式。

AI 補充值得補一句作者沒明說、但決定了後面順序的事:這六個名字雖然並排出現,其實是一比五。map 做的事是把每個值換成另一個值;另外五個做的事是拿到值以後再生出一個新的 Observable,然後把那個 Observable 吐出來的東西攤平回主線。只要牽涉到「同時有好幾個工作在跑」,就會出現誰等誰、誰砍誰、誰被忽略的問題,而 map 從來不會遇到這件事。所以真正需要比較的其實只有五個,而且比較的軸只有一條:新工作來的時候,正在跑的舊工作怎麼辦。

RxJS

JavaScript 的響應式擴充函式庫

用 Observable 這種「會隨時間吐出多個值」的資料型別來處理非同步事件的 JavaScript 函式庫。

RxJS 是 ReactiveX 在 JavaScript 上的實作,Angular 內建採用它,所以影片裡的例子是一個 Angular component。它的核心賣點是把「一次的值」(Promise)推廣成「一串會隨時間到來的值」,因此可以同時描述 HTTP 回應、滑鼠點擊、鍵盤輸入、WebSocket 訊息。名字裡的 map 家族之所以這麼多個,正是因為「一串值」會撞上「前一個還沒處理完、下一個就來了」的情況,而 Promise 世界裡沒有這個問題,所以只需要一個 then。

相關術語: Observable (核心型別)

出處:第 1 段「六個 map 為什麼分不清」

留給下一段 既然六個裡有一個根本不同類,第一步就是先把 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 的實際寫法:pipe(map(item => item * 10)),是後面所有 operator 的對照基準
1:37 · map 的實際寫法:pipe(map(item => item * 10)),是後面所有 operator 的對照基準
console 出現 10 20 30 40 50,證明 map 只是逐值轉換、沒有任何延遲或取消行為
1:58 · 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 不會產生任何等待或取消的行為。

AI 補充作者說「完全不一樣」但沒說破差在哪裡,補上這一步:差別在 callback 的回傳值。map 的 callback 回傳一個普通的值(item * 10 是數字 50),RxJS 直接把它送給下游。另外五個的 callback 回傳的是一個新的 Observable,於是主線上流的東西變成「Observable 裡面裝著 Observable」,必須有人決定怎麼把裡層攤平——要不要等、要不要取消、要不要忽略,五個 operator 的分歧全部從這裡長出來。map 回傳的是值不是 Observable,所以它根本沒有這個決策點,也就沒有什麼好比的。另外一個容易忽略的細節:畫面上 foo$ 是寫在 class 的屬性、subscribe 寫在 constructor,這代表 pipe(map(...)) 那一行只是「描述」,在有人 subscribe 之前完全不會執行,這也是後面所有實驗都要靠 subscribe 才看得到 console 輸出的原因。

Observable

可觀察物件(串流)

一個描述「會隨時間吐出零到多個值、最後可能完成或出錯」的物件,要有人訂閱才會真的開始跑。

影片裡作者口語上都叫它 stream(串流),跟 Observable 是同一件事。它和 Promise 有三個關鍵差異:Promise 只吐一個值、Observable 可以吐很多個;Promise 建立當下就開始跑、Observable 預設是被訂閱時才跑;Promise 無法取消、Observable 可以透過取消訂閱中止。這三點正好是後面 concatMap(要等它結束)、switchMap(可以中途取消)能成立的前提。慣例上存放 Observable 的變數會加上錢字號結尾,例如畫面上的 foo$。

相關術語: subscribe (訂閱才會啟動)、RxJS (由此提供)

出處:第 2 段「map:純轉換,不建新的 observable」

map

轉換每個值

把串流裡的每一個值套用一個函式,換成另一個值再往下送。

和陣列的 Array.prototype.map 是同一個心智模型,只是作用在時間軸上而不是索引上。它不改變值的個數、不改變順序、也不引入任何延遲:進來幾個值就出去幾個值。判斷該不該用 map 的簡單標準是看 callback 回傳什麼——回傳普通值就用 map,回傳 Observable 就要改用後面五個之一,否則下游收到的會是一個沒被訂閱的 Observable 物件而不是你要的資料。

相關術語: mergeMap (回傳值 vs 回傳串流)

出處:第 2 段「map:純轉換,不建新的 observable」

pipe

串接管線

Observable 的方法,把一串 operator 接成處理管線,回傳一個新的 Observable。

pipe 本身不做事,只負責把 operator 由左到右串起來,值會依序穿過每一個 operator。它回傳的是新的 Observable,原本的來源不會被改動。RxJS 6 之後統一改成這種 pipe 寫法,取代舊版把 operator 掛在 Observable 原型上的鏈式寫法,好處是沒用到的 operator 可以被打包工具移除。

相關術語: map (放進管線裡)

出處:第 2 段「map:純轉換,不建新的 observable」

from

由既有資料建立串流

把陣列、Promise 或其他可迭代的東西轉成 Observable,訂閱時把裡面的元素一個接一個同步吐出來。

影片裡用 from([1,2,3,4,5]) 建立來源。關鍵性質是它同步、無延遲地連續吐值:訂閱的那一瞬間五個值就全部發出去了。這一點在下一階段的實驗裡是刻意選擇的——正因為值來得又快又密集,每個值各自要花的處理時間才會重疊,operator 之間的差異才有機會顯現。

相關術語: Observable (建立方式之一)

出處:第 2 段「map:純轉換,不建新的 observable」

subscribe

訂閱

對 Observable 註冊接收者,讓它真的開始執行並把值、錯誤、完成通知送過來。

subscribe 可以收三個 callback,依序是收到值、發生錯誤、完成,影片後面骨架裡的 console.log、空函式、印出 completed 就是這三個位置。沒有 subscribe,pipe 裡寫再多 operator 都不會執行,這叫做 cold(冷)行為。subscribe 會回傳一個 Subscription 物件,可以用它中止;這個中止機制正是 switchMap 能砍掉舊工作的底層手段。

相關術語: Observable (啟動它)

出處:第 2 段「map:純轉換,不建新的 observable」

留給下一段 map 排除之後,剩下五個的共同點是 callback 都回傳新的 Observable,而它們的差別只會在「多個新 Observable 同時存在」的時候顯現。所以下一步必須先造出一個能讓多個工作重疊、又能用肉眼看出時間差的固定實驗場。

3. 共用實驗骨架:0 到 4 加 delay 500 2:13–3:06

要比較剩下四個 operator,作者先搭一個固定的測試骨架:一個叫 example 的函式,收一個 operator 當參數,內部用 from 產生 0、1、2、3、4,在 pipe 裡呼叫傳進來的 operator,再接一個 delay 500 毫秒,最後印出成功、失敗與完成。之後只換 operator、其他都不動。

example 函式全貌:0..4 的來源、可替換的 operator、delay(500)、console 的 next/complete;後面四段的差異全部是這張圖裡只換一個字的結果
2:43 · example 函式全貌:0..4 的來源、可替換的 operator、delay(500)、console 的 next/complete;後面四段的差異全部是這張圖裡只換一個字的結果
承上 承上:上一段留下的要求是造一個能讓多個新 Observable 同時存在、又能用肉眼看出時間差的固定實驗場,這一段就是那個實驗場的設計。

推理因為上一段確認了剩下五個 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

operator

運算子

一個放進 pipe 裡、接收上游 Observable 並回傳新 Observable 的函式,用來描述資料要怎麼被處理。

RxJS 的 operator 分成兩類:像 map 這種只加工值的叫 pipeable operator,像 from、of 這種憑空造出 Observable 的叫 creation function。影片裡 example 函式把 operator 當參數傳進去,靠的就是所有 pipeable operator 的形狀一致——都是呼叫後回傳一個可以放進 pipe 的函式,所以 mergeMap、concatMap、switchMap、exhaustMap 可以互換而程式其他部分一個字都不用改。

相關術語: pipe (放進它裡面)

出處:第 3 段「共用實驗骨架:0 到 4 加 delay 500」

of

由給定的值建立串流

把傳進去的參數包成一個 Observable,訂閱時同步吐出這些值然後立刻完成。

of(x) 產生的是最單純的 Observable:一個值、馬上完成。骨架裡用它當內層工作的替身,是因為它的行為完全可預測,加上 delay 之後就變成一個「花五百毫秒吐一個值然後結束」的模型,剛好模擬一次 API 請求。它和 from 的差別在於 of([1,2,3]) 吐的是「一個陣列」,from([1,2,3]) 吐的是「三個數字」。

相關術語: from (對照組)

出處:第 3 段「共用實驗骨架:0 到 4 加 delay 500」

delay

延遲發送

把上游來的每個值往後推遲指定的毫秒數再往下送。

delay 推遲的是值抵達下游的時間,不是來源產生值的時間。在骨架裡它被放在內層 of(x) 的 pipe 中,所以每一個內層工作都變成「要跑五百毫秒才有結果」,用來模擬真實世界的網路往返。整段實驗的可視性完全建立在這個延遲上:把 500 改成 0,四個 operator 的 console 輸出會幾乎無法分辨。

相關術語: inner Observable (延遲加在它上)

出處:第 3 段「共用實驗骨架:0 到 4 加 delay 500」

inner Observable

內層串流

operator 的 callback 針對每一個進來的值所產生的那個新 Observable,例如骨架裡的 of(x).pipe(delay(500))。

相對地,被 pipe 的那個 from([0,1,2,3,4]) 叫做外層(source)Observable。整支影片要比較的五個 operator,做的事都是「幫你訂閱內層、把內層吐的值攤平回外層主線」,差別只在同時可以有幾個內層活著、以及新內層來的時候舊內層怎麼處置。看懂內層/外層的分野之後,後面每一段其實都在回答同一個問題:這個 operator 對已經存在的內層做了什麼。

相關術語: Observable (一種角色)、of (常用來建立)

出處:第 3 段「共用實驗骨架:0 到 4 加 delay 500」

留給下一段 骨架就緒,唯一沒定的是 pipe 裡那個 operator。合理的起點是先放最寬鬆、對已經在跑的工作完全不干涉的那一個,看看「全部放行」長什麼樣子。

4. mergeMap / flatMap:全部同時開,誰也不取消 3:06–4:55

flatMap 只是 mergeMap 的別名。跑起來的結果是:等五百毫秒之後,0 到 4 幾乎同時出現在 console,然後 mergeMap completed。原因是每個進來的值都立刻建立一個新的 observable,而且先前的 observable 全部保持存活,五個 observable 是同時開始延遲的,所以一起結束。

console 裡 0~4 同時湧出後才 completed,這就是「全部平行、沒有人被取消」的視覺證據
3:23 · console 裡 0~4 同時湧出後才 completed,這就是「全部平行、沒有人被取消」的視覺證據
承上 承上:上一段的骨架就緒、只差 pipe 裡那一個 operator,這一段填進去的是最寬鬆的 mergeMap——對已經在跑的內層完全不干涉。

推理因為上一段刻意讓五個值幾乎同時抵達、每個工作各花五百毫秒,所以只要看 console 的節奏就能反推 operator 做了什麼。畫面上的結果是:等了五百毫秒,0、1、2、3、4 一次全部湧出,然後才是 mergeMap completed。作者由此反推出兩條規則:第一,每個值一進來就立刻建立內層 Observable,中間不等待;第二,先前建立的內層全部保持存活、沒有任何一個被移除。五個內層在幾乎同一瞬間開始各自的五百毫秒,自然也在幾乎同一瞬間結束,這就是「等一下子然後全部一起出現」的由來。他接著把 mergeMap 改寫成 flatMap,輸出完全相同,因為 flatMap 只是 mergeMap 的別名。

AI 補充作者示範的畫面剛好掩蓋了 mergeMap 一個重要性質,這裡要補:mergeMap 不保證輸出順序。它的輸出順序是「哪個內層先吐值就先送誰」,而不是來源的順序。這個例子裡五個內層的延遲都是同樣的 500 毫秒、又同時開始,所以看起來剛好是 0 到 4;如果把 delay 改成隨機值,console 很可能變成 3、0、4、1、2。真實情境更是如此——五個 HTTP 請求同時發出,回來的順序取決於伺服器,不取決於你發送的順序。這正是「不等」的代價:換來最快的總時間,但失去順序保證。另外要補一個作者沒提、實務上常用的安全閥:mergeMap 有第二個參數 concurrent,可以限制同時活著的內層數量,例如 mergeMap(fn, 3) 表示最多三個同時跑、其餘排隊。把它設成 1 的時候,行為就等同於下一種 operator 的「排隊」。

術語:higher-order Observable

mergeMap

合併映射

每來一個值就立刻建立一個內層 Observable,所有內層並行執行,誰先有結果就先往下送。

名字裡的 merge 指的是把多條並行的內層合流成一條輸出。它是五個攤平 operator 裡最寬鬆的一個:不等、不取消、不忽略。適合彼此獨立、順序無所謂的工作,例如同時上傳多個檔案、平行打多支互不相關的 API。它的風險也來自寬鬆——來源吐值很快時內層會無限增生,例如把它接在按鈕點擊上,使用者狂點就會狂發請求,這時候要靠第二個參數 concurrent 或改用其他 operator 來收斂。

相關術語: concurrent (可限制並行數)、inner Observable (全部保持存活)

出處:第 4 段「mergeMap / flatMap:全部同時開,誰也不取消」

flatMap

攤平映射(mergeMap 的舊別名)

mergeMap 的另一個名字,行為完全相同。

這個名字來自函數式程式語言的 flatMap(先 map 再 flatten)。RxJS 早期同時提供兩個名字,導致同一個行為在網路上有兩種寫法,也是初學者混亂的來源之一。RxJS 7 起 flatMap 已被標記為 deprecated,官方文件寫明改名為 mergeMap 並將在下一個主版本移除,所以今天讀到 flatMap 就把它當成 mergeMap 讀,自己寫的時候一律用 mergeMap。

相關術語: mergeMap (同一個東西)

出處:第 4 段「mergeMap / flatMap:全部同時開,誰也不取消」

higher-order Observable

高階串流

值本身又是 Observable 的 Observable,需要靠攤平 operator 把裡層的值取出來。

如果在骨架裡把 mergeMap 換成 map,得到的就是高階串流:subscribe 收到的不是 0、1、2,而是五個 Observable 物件。mergeMap、concatMap、switchMap、exhaustMap 其實都可以拆成「map 產生高階串流」加上「用某種策略攤平」兩步;它們各自等價於 map 後面接 mergeAll、concatAll、switchAll、exhaustAll。理解這個拆法之後,四個 operator 的差別就收斂成一句話:攤平時對已經存在的內層採取什麼策略。

相關術語: inner Observable (它的元素)、map (會產生它)

出處:第 4 段「mergeMap / flatMap:全部同時開,誰也不取消」

concurrent

並行上限參數

mergeMap 的第二個參數,限制同時可以有幾個內層 Observable 在跑。

預設值是無限大,也就是來幾個開幾個。設成 3 表示最多三個同時進行、第四個以後排隊等空位,很適合用來限制對同一個 API 的併發壓力。特別的是 mergeMap(fn, 1) 的行為與 concatMap 完全一致——這也說明四個 operator 不是四種毫無關聯的東西,而是同一個維度(允許幾個同時活著、以及舊的怎麼處理)上的不同取值。

相關術語: mergeMap (它的參數)

出處:第 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
留給下一段 全部平行的直接後果是順序不保證,而且無法用前一個工作的結果去做下一件事。如果需求正好相反——非得等前一個結束才能開始下一個——就必須有人負責排隊。

5. concatMap:排隊,等前一個完成 4:55–5:55

只把 mergeMap 換成 concatMap,0 到 4 一樣全部出現,但是一個一個慢慢出來,之間看得到延遲。因為 concatMap 會等前一個 observable 完成才建立下一個:0 的 observable 延遲五百毫秒結束後,才輪到 1,接著 2、3、4。要「拿前一個結果再做下一件事」時就用它。

console 逐一出現、每個之間隔著五百毫秒,與上一段同時湧出的畫面形成直接對照
5:12 · console 逐一出現、每個之間隔著五百毫秒,與上一段同時湧出的畫面形成直接對照
承上 承上:上一段留下的需求是「必須等前一個工作結束才能開始下一個」,這一段就是把負責排隊的那一個 operator 填進同一個骨架。

推理因為上一段已經確認骨架的變因只有 operator 一個,所以作者只把 concatMap 這個字換上去、其他一行都沒動。console 的結果是 0 到 4 全部都在,一個都沒少,但這次是一個接一個慢慢出現,中間看得到明顯的間隔,最後才是 concatMap completed。值一個都沒少代表沒有人被取消或忽略;出現的節奏被拉開代表它們不是同時跑的。兩件事合起來只有一種解釋:concatMap 會等前一個內層完成才建立下一個。0 的內層跑滿五百毫秒結束後,1 的內層才被建立,接著才輪到 2、3、4,總共花掉大約兩秒半。作者因此給出使用時機:當你要拿前一個 Observable 的結果去做下一件事、而且非等不可的時候用它;mergeMap 則完全不等。

AI 補充這裡要補兩個作者沒提、但實務上會踩到的前提。第一,「等前一個完成」的前提是內層真的會完成。骨架裡的內層是 of(x).pipe(delay(500)),吐一個值就結束,所以隊伍能一直往前推進;如果內層換成 interval 這種永遠不會完成的 Observable,第一個工作會永遠佔著位置,後面的值就永遠排不到,整條串流看起來像卡死。第二,佇列本身沒有上限。concatMap 會把來不及處理的值全部留著,來源吐值的速度只要長期快過處理速度,佇列就會無限增長、記憶體跟著漲,而且使用者看到的延遲會越來越久。所以 concatMap 適合的是「每一筆都不能丟、順序又不能亂」的場景,例如依序送出離線期間累積的操作、或是必須照順序寫入的日誌,而不是綁在使用者連續輸入這種高頻事件上。

術語:complete

concatMap

串接映射

同一時間只跑一個內層 Observable,後面的值排隊等待,前一個完成後才建立下一個。

名字裡的 concat 就是「接在後面」,語意上等同於 mergeMap 把並行數限制成 1。它是四個攤平 operator 裡唯一同時保證「一個都不丟」和「順序不亂」的,代價是總時間等於所有工作時間相加。實務上典型用途是必須照順序完成的寫入操作、排隊送出的離線佇列。要注意它與 mergeMap 在本例的差別只表現在時間軸上,最終值的集合完全相同——console 裡同樣是 0 到 4。

相關術語: mergeMap (等 vs 不等)、concurrent (等於設為 1)

出處:第 5 段「concatMap:排隊,等前一個完成」

complete

完成通知

Observable 宣告自己不會再吐值的結束訊號,subscribe 的第三個 callback 會被呼叫。

完成是 Observable 三種結束方式(完成、錯誤、被取消訂閱)中的一種,也是 concatMap 判斷「可以叫下一位了」的依據。骨架裡的 of(x) 吐完值就自動完成,所以隊伍推得動。console 最後那行 xxx completed 印的是外層串流的完成——外層要等所有內層都完成才算完成,這也是為什麼那行字永遠出現在所有數字之後。要注意被取消訂閱的 Observable 不會發出完成通知,這個區別在下一種策略裡會變得重要。

相關術語: concatMap (等它才前進)、subscribe (第三個回呼)

出處:第 5 段「concatMap:排隊,等前一個完成」

留給下一段 排隊保證了順序也保證了每筆都做到,但它的前提是「每一筆都還有價值」。如果新的值一出現就代表舊的工作已經沒意義了——例如搜尋框裡使用者又多打了一個字——那麼把舊工作做完不只是浪費,還可能讓過期的結果蓋掉新的。這種情況需要的是相反的策略。

6. switchMap:新的來了就砍掉舊的 5:55–6:37

換成 switchMap,console 只印出 4,然後 completed。因為 switchMap 每次拿到新值都會取消掉前一個還沒完成的 observable:1 取消 0、2 取消 1,一路下去,最後只剩 4 活著跑完延遲。

console 只有一個 4,是「前面全部被取消」最直接的證據
6:06 · console 只有一個 4,是「前面全部被取消」最直接的證據
承上 承上:上一段結尾指出「新值出現就代表舊工作失效」的情境需要相反的策略,這一段填進骨架的 switchMap 正是那個相反策略。

推理因為上一段建立了「值一個都不能少」作為對照基準,所以這一段的 console 特別刺眼:只印出一個 4,然後 switchMap completed。作者從這個結果反推:明明五個值都進到了 operator、也都各自建立了內層,卻只有一個活到最後,唯一的解釋是每次新值抵達時,switchMap 會取消掉前一個還沒完成的內層。0 的內層建立後不到一毫秒,1 就到了,0 被取消;1 立刻被 2 取消,2 被 3 取消,3 被 4 取消。4 之後來源就沒有值了,沒有人來取消它,於是只有 4 安穩跑完五百毫秒、印出來。

AI 補充作者用「取消」帶過,這裡補上它的機制與後果。switch 的意思是切換訂閱:新值一到,switchMap 對舊的內層 unsubscribe,然後訂閱新的內層。關鍵是被取消訂閱和自然完成不一樣——被取消的內層不會發出完成通知,所以掛在它上面的 complete 處理不會執行,這是很多人 debug 時的困惑點;要在被取消時也做清理(例如關閉載入動畫),得用 finalize 而不是 complete。另一個實務上的好處是取消會往下傳遞:真實的 HttpClient 請求被取消訂閱時,瀏覽器會直接中止那個 HTTP 連線,不只是丟掉結果而已,所以 switchMap 是唯一能真正省下網路流量的策略。這也解釋了它最經典的用途:搜尋框的即時建議。使用者每多打一個字就發一次請求,舊的關鍵字結果已無意義且可能比新的晚回來、把正確結果蓋掉,switchMap 一次解決了浪費與競態兩個問題。

switchMap

切換映射

每來一個新值就取消訂閱前一個還沒完成的內層 Observable,只保留最新的那一個。

四個攤平 operator 裡最常被使用、也最常被誤用的一個。它適合「只有最新結果有意義」的場景:搜尋建議、路由參數變動後重新取資料、篩選條件改變後重打 API。誤用的典型是把它接在「每一筆都不能掉」的動作上,例如送出訂單或逐筆儲存——使用者連續操作時,前面幾筆會被無聲地取消掉。判斷方式很簡單:問自己「舊的那次做到一半被丟掉,會不會出事」,會出事就不能用 switchMap。

相關術語: unsubscribe (靠它砍掉舊的)、concatMap (砍舊 vs 排隊)

出處:第 6 段「switchMap:新的來了就砍掉舊的」

unsubscribe

取消訂閱

中止一個進行中的訂閱,讓 Observable 停止產生值並執行清理邏輯。

取消訂閱是 Observable 相對於 Promise 最實質的優勢:Promise 一旦發出就無法收回,Observable 可以隨時喊停。它是 switchMap 砍掉舊內層的實際手段,也是 Angular 元件銷毀時避免記憶體洩漏的標準做法。要記住三件事:取消不等於完成、不會觸發 complete 回呼;清理程式碼要寫在 finalize 或 Observable 建構時回傳的 teardown 函式裡;對 HTTP 而言取消會真的中止底層請求。

相關術語: subscribe (相反操作)、complete (兩種不同結束)

出處:第 6 段「switchMap:新的來了就砍掉舊的」

留給下一段 switchMap 的取捨很清楚:永遠保留最新、丟掉舊的。既然有人選最新,就會有相反的需求——已經在做的事不准被打斷、後來的請求寧可不要。最後一個 operator 要回答的就是這一端。

7. exhaustMap:忙的時候,後來的一律忽略 6:37–7:18

換成 exhaustMap,console 只印出 0。因為第一個 observable 還沒完成之前,後面所有的值都被忽略、不會建立新的 observable。它和 switchMap 剛好相反:switchMap 留最新的,exhaustMap 留最早的。

console 只有一個 0,與上一段只有 4 並排看,正好是「留最早」對「留最新」
6:48 · console 只有一個 0,與上一段只有 4 並排看,正好是「留最早」對「留最新」
承上 承上:上一段留下的問題是「有沒有反過來的策略——已經在做的不准被打斷、後來的寧可不要」,這一段的 exhaustMap 就是那一端。

推理因為上一段建立了「只剩最後一個」這個極端,所以作者把 exhaustMap 換上去之後的畫面正好是鏡像:console 只印出一個 0,然後 exhaustMap completed。他反推的邏輯是,0 進來時沒有任何內層在跑,所以順利建立了帶延遲的內層;接下來 1、2、3、4 每一個都想建立新的內層,但都建立不了,因為 exhaustMap 看到已經有一個內層還在進行中,就把這些值直接忽略掉。等到 0 的五百毫秒跑完,來源早就結束了,也就沒有下一個值可以接手。於是四個 operator 在同一個骨架下給出四種可以互相對照的輸出:全部、逐一、只剩最新、只剩最早。

AI 補充補上名字與判準。exhaust 的意思是「把目前這個耗盡」——在現有內層耗盡(完成)之前,新來的值一律丟棄,而且是靜悄悄地丟,不會報錯也不會排隊,這一點和排隊策略最容易混淆:兩者都是同時只跑一個,但一個把後來的存起來、一個把後來的扔掉。最典型的用途是防止重複觸發:送出按鈕連點兩下只送一次、token 過期時多個請求同時要求續期只實際續一次、下拉更新在上一次還沒回來前不重複發。把四個 operator 放在同一張表上就記得住了——都不管別人的是 mergeMap,把別人排在後面的是 concatMap,砍掉舊的留新的是 switchMap,擋掉新的留舊的是 exhaustMap。要注意這四者的差別只有在「前一個還沒結束、下一個就來了」的時候才存在。

exhaustMap

耗盡映射

目前的內層 Observable 還沒完成時,忽略所有新進來的值,不建立新的內層也不排隊。

它是 switchMap 的鏡像:一個留最新、一個留最早。實務上最常見的用途是防止重複觸發——表單送出按鈕、登入請求、token 續期、下拉重新整理,這些動作在前一次還沒回來之前重複執行只會造成重複資料或競態。用它的時候要留意被忽略的值是靜默丟棄的,使用者可能不知道自己那一下沒有生效,介面上通常要搭配 disabled 或載入中的狀態提示。

相關術語: switchMap (相反策略)、concatMap (丟掉 vs 排隊)

出處:第 7 段「exhaustMap:忙的時候,後來的一律忽略」

留給下一段 四種策略都攤在桌上了,但這樣就回到第 1 段那個原始困惑:既然差異這麼明顯,為什麼 Stack Overflow 上不同人用不同 operator 解同一個問題,卻都能跑出正確結果?最後要用一個真實情境把這個矛盾解掉。

8. 真實例子:為何 concatMap 和 switchMap 結果一樣 7:18–9:48

回到最常見的情境:先打 getUser,再用結果打 getUserDetails。用 concatMap 完全正確,console 印出 id 1、age 30。但把它換成 switchMap,結果一模一樣——這正是大家在 Stack Overflow 看到的混亂來源。作者的解釋是:HTTP 請求只會發出一個值就完成,根本沒有「前一個 observable」需要取消,所以兩者在這個情境無法分辨;換成別的情境就會有差。

getUser 與 getUserDetails 的簽章:後者必須吃前者的結果,這是為什麼需要 concatMap 這類「串接」operator
7:48 · getUser 與 getUserDetails 的簽章:後者必須吃前者的結果,這是為什麼需要 concatMap 這類「串接」operator
concatMap 版本的程式碼與 console 的 id 1 / age 30,是「正確作法」的基準畫面
8:25 · concatMap 版本的程式碼與 console 的 id 1 / age 30,是「正確作法」的基準畫面
換成 switchMap 後 console 完全相同,這張圖就是整段的謎題本身
8:55 · 換成 switchMap 後 console 完全相同,這張圖就是整段的謎題本身
承上 承上:上一段留下的矛盾是「差異這麼明顯,為什麼不同人用不同 operator 解同一題都能跑」,這一段用最常見的串接情境把矛盾解掉。

推理因為前面四段的差異都是在人造的高頻來源下逼出來的,所以作者刻意換一個真實情境來檢驗:先呼叫 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

cold Observable

冷串流

每次被訂閱都重新從頭執行一次、各訂閱者拿到各自獨立資料的 Observable。

of、from 以及 Angular 的 HttpClient 回傳的都是冷串流:沒訂閱就什麼都不做,訂閱兩次就執行兩次。這解釋了兩件實務現象——模板裡多寫幾個 async pipe 就多打幾次 API,以及取消訂閱能真的中止請求。相對的熱串流(例如 Subject、DOM 事件)不管有沒有人訂閱都在產生值,訂閱者共享同一份資料流,也因此錯過的值就是錯過了。要把冷變熱可以用 shareReplay。

相關術語: subscribe (訂閱才執行)、of (產生冷串流)

出處:第 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
留給下一段 總結收束。

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

📄 全部影片 · 主題區: 前端資料流