RxJS 與元件層:把 props 當成一條 stream

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

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

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

1. Outline

  1. 起點 · Embrace Reactive Programming in React and Vue with RxJS - Fahad Ibnay Heylaal
    從「什麼是 data stream」一路推到「props 也是一條 stream」,中間補完 Observable、RxJS operator 與 Subject,最後用一個 interval 元件示範今天手接的樣板有多痛,並提出 proppy 這種接線工具。

2. YouTuber 的思維推導

Embrace Reactive Programming in React and Vue with RxJS - Fahad Ibnay Heylaal

JavaScript Conferences by GitNation · 26m44s · 字幕 en · vision=on (研討會投影片 talk:marble diagram、operator 的 ASCII 時間軸、左右對照的 React/Vue 程式碼都是「看了才懂」的畫面,講稿裡「this diagram / this code / on the left side there is react」這類指示語超過 10 次。) · YouTube

預估 vs 實際耗時
階段預估實際
fetch 6s 2s
segment 1m23s –
shot 1m00s 4s
analyze 11m15s –
render 5s –

作者的推導是一條「先建立心像,再逼出缺口,最後補上自己的工具」的直線。他先用一個現場就能感受的例子(房間裡的人數會變)把「值隨時間變化」變成可見的東西,再用 marble diagram 給這件事一套記號;有了記號,就需要一個能承載它的型別,於是 Observable 登場,並且是用「跟 Promise 差在哪」來定義的——多值、lazy、可取消。接著他把 Observable 從概念落到程式(constructor 產值、subscribe 收值),再擴大到 RxJS 提供的三類 API,並用三個前端日常場景(即時、取消、離線重試)說明為什麼值得學。

到這裡 RxJS 這一側已經完整,但還缺一個洞:constructor 內部產的值,外面送不進去——Subject 補上這個洞,資料流才能形成循環。第二幕轉向元件:既然 React/Vue 收到新 props 就重繪,那就把 props 想成一條 stream,元件永遠 stateless、邏輯全留在 RxJS 那側。他刻意把 interval 範例用今天的寫法實作一次,讓 componentDidMount / setState / componentWillUnmount 的樣板攤在螢幕上,痛點自己說話。

最後他檢視既有方案(redux-observable 等)指出兩個限制——被綁在全域 store、以及只能往下產生 stream 卻接不住上游 props——這兩個限制正好定義了他的 proppy 要做什麼:attach 負責接,withStream 負責讓 props 進來也是 stream。收尾他沒有推銷,而是反過來提醒 Rx 的學習曲線,把選擇權交回聽眾。

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

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

  1. 開場:這場 talk 的路線圖 → 路線的第一站是最基礎的問題:所謂的 data stream 到底是什麼,又要怎麼把它畫出來?
  2. data stream 與 marble diagram → 圖畫得出來了,但圖不能執行。下一步的問題是:這條時間軸在 JavaScript 裡要用什麼型別表示?
  3. Observable:和 Promise 差在哪 → 型別的性質講完了,但還沒看過一行程式。下一段要問:這個 Observable 實際寫出來長什麼樣,值又是從哪裡冒出來的?
  4. Observable 的程式長相與 subscribe → 單一個 Observable 的生產與消費都清楚了,但真實應用不會只有一條 stream。下一段要看承載這一切的函式庫本身提供了什麼。
  5. RxJS 是什麼:Rx 的三類 API → 工具箱列完了,但還沒回答「為什麼是我需要它」。下一段換成前端日常場景來回答。
  6. 前端為什麼需要它 → 動機建立了,但 operator 到目前為止只是名字。下一段用一條具體的數字流把它跑一次。
  7. operator 實例與 pipe:順便瘦身 bundle → 到這裡 RxJS 這一側幾乎完整了,只剩一個第 4 段埋下的限制沒解決:值只能在 constructor 裡產生,外面送不進去。
  8. Subject:讓外部把值送進 stream → 資料流的兩個方向都齊了。接下來把這個循環真的接到元件上:如果 props 本身就是一條 stream 會怎樣?
  9. 把 props 當成 stream → 概念圖很漂亮,但還沒有一行真正能跑的接線程式。下一段用一個最小的 interval 範例,把「產生 stream」與「接上元件」各做一次。
  10. interval 範例,與今天手接的痛 → 樣板的形狀左右一致,這件事本身就是線索:既然形狀固定,它就應該可以被抽成一個共用的東西。下一段追問:那個東西該長什麼樣,現成方案為什麼還不夠?
  11. 理想的接法、三個好處,與既有方案的限制 → 規格書寫好了:要能接、還要能把進來的 props 當 stream 接住。下一段就是照這份規格做出來的東西。
  12. proppy:attach、withObservable、withStream → 工具講完了,剩下最後一個問題:這一整套值不值得你導入?
  13. 結語:先有 use case 再用 Rx

3. 逐段說明

Embrace Reactive Programming in React and Vue with RxJS - Fahad Ibnay Heylaal

1. 開場:這場 talk 的路線圖 0:17–1:56

講者 Fahad 在 traffics 用 React + RxJS 上線,先問現場有多少人在 production 用 React/Vue(很多)、用 RxJS(很少),點出這個落差就是這場 talk 要補的。他預告四件事:什麼是 data stream、RxJS 為什麼有價值、如何把 stream 接進 component 層、以及離開這個房間之後怎麼在 production 用。

承上 前情提要裡的「你已經在用 React 或 Vue」這個背景:作者刻意先確認這件事,才有後面「把 RxJS 接到你已經在用的東西上」的立足點。

推理作者一開場先做現場調查,因為他要建立的不是「RxJS 很好」而是「你們之間有個落差」:舉手用 React/Vue 的很多,用 RxJS 的很少。這個落差就是整場的動機——不是叫大家換框架,而是把 RxJS 放進大家已經有的元件層。他接著把 talk 拆成四段路線:什麼是 data stream、RxJS 為什麼有價值、怎麼接進元件、離開這個房間後怎麼在 production 用。

AI 補充他的身分決定了這場 talk 的可信度來源:在 Travix 用 React + RxJS 上線,所以講的不是玩具。值得注意的是他一開始就把「呈現層是誰」設定成不重要的變數——React、Vue 都行——這個設定在後半段會變成一個實際可驗證的主張(同一份邏輯,只換 import 的套件名)。

留給下一段 路線的第一站是最基礎的問題:所謂的 data stream 到底是什麼,又要怎麼把它畫出來?

2. data stream 與 marble diagram 1:56–3:52

引用 André Staltz 的定義:reactive programming 就是用非同步 data stream 寫程式。凡是能對時間軸畫出來的東西都是 stream,他用「這個房間裡的人數」當例子:100 → 朋友離場變 99 → 回來又變 100。RxJS 社群用 marble diagram 這種 ASCII 時間軸來畫:字母是發出的值、X 是錯誤、豎線是 stream 結束,而且時間只會往前。

marble diagram 的圖例:a b c d 是發出的值、X 是 error、| 是 complete、---> 是時間軸,後面所有圖都照這套讀
3:35 · marble diagram 的圖例:a b c d 是發出的值、X 是 error、| 是 complete、---> 是時間軸,後面所有圖都照這套讀
把「這個房間的人數」畫成 marble diagram:第 1 分鐘 100、第 5 分鐘 99、第 20 分鐘回到 100,最後一條 | 收尾,是「值隨時間變化」被視覺化的那一刻
3:48 · 把「這個房間的人數」畫成 marble diagram:第 1 分鐘 100、第 5 分鐘 99、第 20 分鐘回到 100,最後一條 | 收尾,是「值隨時間變化」被視覺化的那一刻
承上 上一段留下的第一站:data stream 是什麼、怎麼畫出來。

推理因為要讓「stream」從抽象詞變成可操作的東西,作者先借 André Staltz 的定義收斂範圍——reactive programming 就是用非同步 data stream 寫程式——再用現場就能驗證的例子撐開它:這個房間裡的人數。100 個人,朋友離場變 99,回來又變 100。凡是能對著時間軸畫出來的東西都是 stream。有了直覺還不夠,還需要一套共同記號,所以他引入 RxJS 社群的 marble diagram:字母是發出的值、X 是錯誤、豎線是結束、箭頭是只能往前的時間軸。

AI 補充marble diagram 不只是教學插圖,它是 RxJS 世界的真正通用語:官方文件每個 operator 都用它、測試框架甚至能直接把 ASCII 的 marble 字串當成測試輸入(marble testing)。所以這一段看似在講比喻,其實是在教你之後查文件的閱讀能力。另外請留意圖上那條豎線:作者說「stream 很可能永遠不結束」——會不會結束是後面判斷「要不要取消訂閱」的關鍵,畫面上這條線在不在,決定了你要不要自己收尾。

Reactive programming

反應式程式設計

用非同步 data stream 來寫程式:把「隨時間變化的值」當成一等公民,而不是靠事件回呼互相通知。

作者引的是 André Staltz 那篇廣為流傳的〈The introduction to Reactive Programming you've been missing〉。這個定義的重點在「非同步」與「stream」兩個字:非同步排除了同步計算,stream 排除了單一值。它不是某個函式庫的名字,RxJS 只是它在 JavaScript 的一種實作。

相關術語: Data stream (操作的對象)、RxJS (一種實作)

出處:第 2 段「data stream 與 marble diagram」

Data stream

資料流

任何可以沿著時間軸畫出來的值序列,例如房間人數、滑鼠位置、WebSocket 訊息。

判斷一樣東西是不是 stream 的簡單方法就是作者的方法:能不能對它畫一條時間軸並在上面標點。這個定義刻意寬鬆,因為它要涵蓋的東西差異很大——一次性的 HTTP 回應是只有一個值就結束的 stream,滑鼠移動則是永不結束的 stream。

相關術語: Marble diagram (用來畫它)

出處:第 2 段「data stream 與 marble diagram」

Marble diagram

彈珠圖

RxJS 社群用來畫 stream 的 ASCII 時間軸:字母是值、X 是錯誤、| 是完成、---> 是時間方向。

彈珠圖之所以重要,是因為 RxJS 上百個 operator 幾乎都是用它來定義的——看懂圖比背 API 名稱有效率得多。它還有可執行的版本:RxJS 的 TestScheduler 支援 marble testing,直接用 '--a--b--|' 這種字串描述輸入與期望輸出。圖上時間只往前,這不是美術限制,而是在強調 stream 不能倒帶重看(要重播得另外用 ReplaySubject 之類的東西)。

相關術語: Data stream (它的畫法)

出處:第 2 段「data stream 與 marble diagram」

留給下一段 圖畫得出來了,但圖不能執行。下一步的問題是:這條時間軸在 JavaScript 裡要用什麼型別表示?

3. Observable:和 Promise 差在哪 3:52–5:27

要把 marble diagram 寫成程式就需要 Observable——一個正在 TC39 標準化的 JavaScript 新型別。和 Promise 逐條對照:Promise 只 resolve/reject 一次就結束,Observable 可以隨時間發出 0 到多個值、可以永不結束;Observable 是 lazy 的,沒 subscribe 就什麼都不發生,Promise 一建立就 eager 執行;最關鍵的是 Observable 可以取消,Promise 不行——元件已經 unmount 但請求還在跑,就是這個差別在救你。

Promise 與 Observable 的逐項對照表,這張是整場「為什麼不是用 Promise 就好」的論證核心
4:30 · Promise 與 Observable 的逐項對照表,這張是整場「為什麼不是用 Promise 就好」的論證核心
承上 上一段留下的問題:畫得出來的時間軸,在 JavaScript 裡用什麼型別表示?

推理因為聽眾手上已經有一個處理非同步的工具(現場問「誰知道 Promise」時全場舉手),作者選擇用對照來定義 Observable,而不是從零解釋。投影片列出五點:它是 JavaScript 的新型別、用來描述 push-based 的資料來源、可以持續發出多個值、是 lazy 的、可以被取消、而且是可組合的。對照 Promise 就清楚了:Promise 只 resolve 或 reject 一次就結束,Observable 可以發 0 到多個值甚至永不結束;Promise 一建立就開始跑,Observable 沒人 subscribe 就什麼都不做;最關鍵的是 Promise 取消不了,Observable 可以——他當場給的場景是「請求還在飛,元件卻已經 unmount 了」。

AI 補充「lazy」與「可取消」其實是同一件事的兩面:因為工作是在 subscribe 那一刻才啟動,這份工作才有一個明確的擁有者(subscription),也才有東西可以被取消。Promise 沒有這個擁有者,它一出生就在跑,所以 AbortController 得另外從外面補一個取消通道進去。投影片上還有一個講者沒展開的詞:push-based。它是指值由來源主動推給你,對照的是你主動去要的 pull-based(例如 iterator、async iterator)——這個分類決定了背壓(誰控制速度)要由誰負責。

Observable

可觀察物件

代表一條可以隨時間推出多個值的 stream,lazy、可取消、可組合。

把它想成「值的函式版本」:Promise 是一個已經在跑的計算,Observable 是一份還沒開始執行的計算說明書,subscribe 才是按下開始。同一個 Observable 被 subscribe 兩次,預設會各自執行一次(cold),這點常讓從 Promise 過來的人踩坑;要共享同一次執行得用 share 之類的 operator 把它變成 hot。

相關術語: Promise (對照組)、Observer (由它產生值)

出處:第 3 段「Observable:和 Promise 差在哪」

Promise

承諾

JavaScript 內建的非同步型別:只會 resolve 或 reject 一次,建立後立即執行,且無法取消。

作者用它當對照組是有道理的:Promise 是聽眾唯一都有的共同背景。三個差異裡最實務的是取消——這正是 fetch 需要搭配 AbortController 的原因。反過來說,如果你的非同步工作本來就只有一個結果、也不需要中途喊停,Promise 仍然是比較簡單的選擇,用 Observable 反而是殺雞用牛刀。

相關術語: Observable (對照組)

出處:第 3 段「Observable:和 Promise 差在哪」

TC39

ECMAScript 標準委員會

負責推進 JavaScript 語言標準的委員會,Observable 曾以提案形式送進這個流程。

TC39 的提案要走 Stage 0 到 Stage 4 五個階段,進到 Stage 4 才會寫進 ECMAScript 標準。作者演講當時(2018)Observable 提案還被視為「正在標準化中」,但它此後就停在早期階段沒有再前進——所以直到今天,你要用 Observable 仍然得靠 RxJS 這類函式庫,而不是語言內建。

相關術語: Observable (提案標的)

出處:第 3 段「Observable:和 Promise 差在哪」

確定錯誤/已過時 4:00
「it's a new type in JavaScript currently it's being standardized by the tc39」
這句在 2018 年說得過去,今天已經不成立:TC39 的 proposal-observable 長年停在早期階段、實質停擺,Observable 至今沒有進入 ECMAScript 標準。後來真正有進展的是另一條路——WHATWG 把 Observable 寫進 DOM 規格(搭配 EventTarget 的 when()),走的是瀏覽器 API 而不是語言核心。實務結論不變:今天要用 Observable,還是得靠 RxJS。
依據: tc39/proposal-observable 自 2018 年後未再推進;WHATWG DOM 於 2024 年納入 Observable 規格
留給下一段 型別的性質講完了,但還沒看過一行程式。下一段要問:這個 Observable 實際寫出來長什麼樣,值又是從哪裡冒出來的?

4. Observable 的程式長相與 subscribe 5:27–7:02

假設瀏覽器已原生支援,new Observable(observer => …) 裡的函式拿到 observer 物件:observable 是「可以被訂閱、拿值的東西」,observer 是「產生值的東西」,而且值只能在 constructor 內產生,外面的人動不了。observer 有三個方法:next 發值、error 送錯誤、complete 結束。社群慣例是變數名結尾加 $ 提醒自己這是 stream。subscribe 時對應傳三個 handler:值、錯誤、完成(完成最多只會觸發一次)。

new Observable 的程式碼:observer.next / error / complete 三個方法寫在哪裡,是理解 Subject 之前的必要畫面
5:35 · new Observable 的程式碼:observer.next / error / complete 三個方法寫在哪裡,是理解 Subject 之前的必要畫面
subscribe 的三個 callback(值/error/complete)對位,看到才知道 error 之後 stream 就停在那裡、complete 最多只觸發一次
6:48 · subscribe 的三個 callback(值/error/complete)對位,看到才知道 error 之後 stream 就停在那裡、complete 最多只觸發一次
承上 上一段留下的問題:Observable 寫出來長什麼樣,值從哪裡冒出來?

推理因為要回答「值從哪來」,作者把 constructor 攤開:new Observable(function (observer) { … }),傳進去的函式拿到一個 observer 物件,值就是在這裡面用 observer.next(1)、next(2)、next(3) 推出去的,需要報錯就 observer.error(),要收尾就 observer.complete()。他順手切開兩個常被混用的名詞:observable 是「可以被訂閱、拿到值的那一端」,observer 是「產生值的那一端」。他也強調一個限制——值只能在 constructor 裡產生,外面的人插不進來。收值端則是 subscribe,依序傳三個 handler:值、錯誤、完成,其中完成最多只會觸發一次。最後補上社群慣例:變數名結尾加 $ 提醒自己這是 stream。

AI 補充請記住「值只能在 constructor 裡產生」這個限制,它不是實作細節而是刻意的封裝:因為外面動不了,Observable 才能保證每次 subscribe 都重跑同一份說明書。這個限制稍後會變成一個真實的障礙——UI 事件是從外面來的。另外投影片上被註解掉的那行 observer.error('oh crap') 值得留意:next 可以叫很多次,但 error 和 complete 只要叫了一次,這條 stream 就終止,之後的 next 一律被丟棄。這也是為什麼 subscribe 的第二、第三個 handler 天生互斥。

術語:$ suffix convention

Observer

觀察者

產生值的那一端,提供 next / error / complete 三個方法把值與終止訊號推進 stream。

Observer 與 Observable 的方向剛好相反:Observable 面向消費者(你 subscribe 它),Observer 面向生產者(你呼叫它)。subscribe 時傳的那三個 callback,其實就是在建立一個 observer。三個方法的關係是:next 可以叫任意多次,error 與 complete 各自只要叫一次就終止整條 stream。

相關術語: Observable (值送進它)、subscribe (另一端)

出處:第 4 段「Observable 的程式長相與 subscribe」

subscribe

訂閱

啟動一個 Observable 並開始接收值的動作,回傳一個可以用來取消的 subscription。

subscribe 是整個 RxJS 裡唯一「真的會發生事情」的動作——在此之前不管串了多少 operator,都只是在描述。它的回傳值也很關鍵:那是一個 subscription 物件,你之後要靠它 unsubscribe。忘了收好這個回傳值,就是後面那段記憶體洩漏的來源。

相關術語: Observable (訂閱對象)、Observer (由三個 callback 組成)

出處:第 4 段「Observable 的程式長相與 subscribe」

$ suffix convention

錢字號命名慣例

社群慣例:代表 stream 的變數名結尾加上 $(例如 numbers$),提醒自己這是一條 stream 而不是一個值。

這個慣例(Finnish notation)純粹是給人看的,執行時毫無意義,但在 RxJS 程式裡非常實用:numbers 和 numbers$ 的用法完全不同,前者可以直接算,後者只能 pipe 或 subscribe。作者後面每一張投影片都遵守它,看到 $ 就知道那行還沒有值。

相關術語: Observable (標記它)

出處:第 4 段「Observable 的程式長相與 subscribe」

留給下一段 單一個 Observable 的生產與消費都清楚了,但真實應用不會只有一條 stream。下一段要看承載這一切的函式庫本身提供了什麼。

5. RxJS 是什麼:Rx 的三類 API 7:02–8:52

Observable 目前還不能直接用,所以要靠實作:zen-observable 是 TC39 提案的最小實作,RxJS 5 以上則是貼近提案又遠不止於此的完整版。Rx = Reactive Extensions,是跨平台的(.NET、Java 都有),RxJS 是它的 JavaScript 版。RxJS 給的東西分三類:建立 stream(constructor、interval、Node 風格 callback…)、合併 stream(merge、concat,把多條併成一條再 subscribe 一次)、以及 operator(map、filter、reduce 這些函數式工具,只是現在作用在非同步的 stream 上)。

承上 上一段留下的線索:單一 Observable 會寫了,但真實應用需要更多——函式庫本身提供什麼?

推理因為上一段的 new Observable 其實還不能在瀏覽器直接用,作者先交代實作選項:zen-observable 是 TC39 提案的最小實作,RxJS 5 以上則貼近提案但遠不止於此。接著他退一步解釋名字:Rx = Reactive Extensions,是一套跨平台的概念,.NET、Java 都有,RxJS 只是它的 JavaScript 版本。然後把 RxJS 提供的東西整理成三類:建立 stream(constructor、interval、Node 風格 callback…)、合併 stream(merge、concat,把多條併成一條之後只要 subscribe 一次)、以及 operator——如果你熟悉 map / filter / reduce,同一組工具現在作用在非同步的 stream 上。

AI 補充「多條併成一條再 subscribe 一次」這句話值得停一下:它是 RxJS 相對於回呼地獄的主要優勢——原本散在各處的多個非同步來源,可以先在宣告層合併成單一條 stream,錯誤處理與取消也就只需要做一次,而不是每個 callback 各做一份。至於 operator 與陣列方法的類比很好用但有一個重要差別:陣列的 map 立刻算完,stream 的 map 只是宣告「以後每個進來的值都這樣處理」,真正執行要等到 subscribe。

術語:Rx (Reactive Extensions)

RxJS

Reactive Extensions for JavaScript

Rx 的 JavaScript 實作,提供 Observable、額外型別,以及大量建立/合併/轉換 stream 的函式。

RxJS 5 是一次大改寫,也是作者說「貼近 TC39 提案」的版本;5.5 引入 pipeable operator(下一段會看到),6 之後匯入路徑固定成 rxjs 與 rxjs/operators。它在 Angular 裡是內建依賴,這也是很多人第一次遇到它的場合;在 React/Vue 生態則一直是選配——這正是這場 talk 想改變的事。

相關術語: Rx (Reactive Extensions) (JS 版實作)、Observable (核心型別)

出處:第 5 段「RxJS 是什麼:Rx 的三類 API」

Rx (Reactive Extensions)

反應式擴充

微軟開源的一套跨語言 API 設計:用 observable 描述非同步資料流,並提供一致的 operator 名稱。

Rx 最早出自 .NET,之後被移植到 Java(RxJava)、Swift(RxSwift)、JavaScript 等。跨語言一致是它真正的資產:同一個 operator 名稱與同一張 marble diagram 在各語言意義相同,reactivex.io 上的文件因此可以共用。

相關術語: RxJS (JS 版)

出處:第 5 段「RxJS 是什麼:Rx 的三類 API」

Operator

運算子

接收一個 Observable、回傳一個新 Observable 的函式,例如 map、filter、merge。

operator 是純函式:它不改動原本的 stream,而是回傳一條新的,所以可以安心串接。它們大致分兩類——creation operator(如 interval、from,從無到有造一條 stream)與 pipeable operator(如 map、filter,把一條變成另一條)。RxJS 有上百個 operator,但實務上八成的程式只會用到十幾個。

相關術語: Observable (進出的型別)、Marble diagram (用它定義行為)

出處:第 5 段「RxJS 是什麼:Rx 的三類 API」

留給下一段 工具箱列完了,但還沒回答「為什麼是我需要它」。下一段換成前端日常場景來回答。

6. 前端為什麼需要它 8:52–10:24

三個前端天天遇到的場景:即時應用(WebSocket 訊息一直進來,要不重整頁面就更新畫面)、取消(元件已 unmount 但請求還在飛,subscription 一結束就沒有 side effect 了)、離線與重試(斷線後要一直嘗試重連、回來再重抓,這種 retry 邏輯可以宣告一次就交給 RxJS 跑)。他形容這是「心安」——邏輯集中在一個地方宣告,不用散在各處。

承上 上一段留下的問題:工具箱有了,但為什麼是前端工程師需要它?

推理因為抽象的好處說服不了人,作者換成三個前端天天遇到的場景。第一是即時應用:WebSocket 訊息不斷進來,畫面要在不重整的情況下反應。第二是取消:元件已經因為使用者切走而 unmount,但它發出的請求還在飛——上一段提到的「subscribe 才啟動、可以取消」在這裡兌現,訂閱一結束就不再有 side effect。第三是離線與重試:斷線後要持續嘗試重連、回來後重新抓資料,這種 retry 邏輯可以在一個地方宣告一次,交給 RxJS 執行。他用「心安」來形容這種把邏輯集中宣告的感覺。

AI 補充這三個場景其實是同一件事的三種面貌:它們都不是「拿到一個值」,而是「一段延續中的關係」。Promise 天生描述前者,所以遇到後者就得靠額外的旗標、計數器與清理函式;Observable 因為本身就有結束與取消的概念,這些狀態才有地方放。作者這裡也悄悄示範了宣告式的價值——retry(5) 這種寫法把「重試策略」變成程式碼裡一個看得見的名詞,而不是散落在 catch 區塊裡的控制流程。

Cancellation

取消

停止一條進行中的 stream 並釋放它的資源,在 RxJS 裡就是對 subscription 呼叫 unsubscribe。

取消真正處理的是「不再需要的工作還在產生副作用」這個問題:更新已卸載元件的 state、覆寫使用者剛切換過去的新資料、白白佔著網路連線。RxJS 的取消是往上游傳遞的——你取消最下游的訂閱,整條鏈上的來源都會收到收尾通知,這是它和手動加 if (cancelled) 判斷最大的差別。

相關術語: subscribe (取消它的結果)、Memory leak (沒做會導致)

出處:第 6 段「前端為什麼需要它」

Retry

重試

stream 失敗時自動重新訂閱來源的策略,可以在宣告時就指定次數或延遲。

RxJS 提供 retry(n) 與更靈活的 retryWhen(後者在新版改以 retry({ delay }) 表達),可以做出「指數退避」這種常見策略。重點在於它是重新訂閱整條來源——因為 Observable 是 lazy 的說明書,重跑一次是自然的動作;Promise 沒有這個性質,重試得自己重新呼叫一次函式。

相關術語: Observable (靠它可重跑)

出處:第 6 段「前端為什麼需要它」

留給下一段 動機建立了,但 operator 到目前為止只是名字。下一段用一條具體的數字流把它跑一次。

7. operator 實例與 pipe:順便瘦身 bundle 10:24–12:31

用 1~5 這條非同步數字流示範:先 filter 只留奇數得到 1、3、5,再 map 乘以 10 得到 10、30、50——和陣列的 filter/map 一樣,只是對象是 stream。程式面用 RxJS 5.5 引入的 pipe:不再一次載入全部 operator,只 import 用得到的函式,bundle 可以從幾百 KB 掉到三四 KB。pipe 接受一串 operator 函式,subscribe 最後那條結果流即可。

filter → map 兩層 marble diagram 疊在一起,是「operator 就是 stream 進 stream 出」最直觀的一張
10:45 · filter → map 兩層 marble diagram 疊在一起,是「operator 就是 stream 進 stream 出」最直觀的一張
pipe 的程式寫法與 import 方式,這張同時解釋了 bundle size 為什麼會變小
11:40 · pipe 的程式寫法與 import 方式,這張同時解釋了 bundle size 為什麼會變小
承上 上一段留下的線索:動機清楚了,該把 operator 真的跑一次。

推理因為 operator 需要看得到才算懂,作者把它畫進上一段建立的 marble diagram 記號裡:一條 numbers 流是 1、2、3、4、5,套上 filter(n => n % 2 === 1) 得到 1、3、5,再套 map(n => n * 10) 得到 10、30、50,三條時間軸上下疊著看,operator 就是「stream 進、stream 出」。程式面他用 RxJS 5.5 引入的 pipe:從 rxjs/operators 只 import 用得到的 filter 與 map,串進 numbers$.pipe(…),最後 subscribe 結果流。他強調這帶來的附加好處是打包體積——不再把整包 operator 拉進 bundle。

AI 補充pipe 的意義比「語法比較好看」更深:舊寫法是把 operator 掛在 Observable.prototype 上(numbers$.filter(...)),因此必須先 import 有副作用的 patch 檔,打包工具無從判斷哪些用得到;改成獨立函式後,未使用的 operator 才可能被 tree shaking 移除。順帶一提,投影片上 filter 的條件寫 n % 2 === 1 只對正數成立——負奇數的餘數是 -1——這在示範裡無所謂,但搬進真實程式時要小心。

pipe

管道

Observable 的方法,把一串 operator 函式依序套用,回傳最後的結果 stream。

pipe 就是函數式的組合(compose):numbers$.pipe(a, b, c) 等於 c(b(a(numbers$))),只是讀起來是由上往下的順序。RxJS 5.5 引入它來取代把 operator 掛在 prototype 上的舊寫法,6 之後舊寫法正式退場。它本身不執行任何東西——沒有 subscribe,pipe 再長也只是一份說明書。

相關術語: Operator (串接它們)、Tree shaking (使它可行)

出處:第 7 段「operator 實例與 pipe:順便瘦身 bundle」

Tree shaking

搖樹優化

打包工具靜態分析 import,把沒有被用到的程式碼從最終 bundle 移除。

它依賴 ES module 的靜態 import:因為 import { map } from 'rxjs/operators' 在編譯期就能確定用了什麼,沒被引用的 operator 才能安全刪掉。相對地,舊的 prototype patch 寫法會產生副作用,打包工具不敢移除。這也是為什麼 pipe 的改動同時是 API 改善與體積改善。

相關術語: pipe (受益於它)

出處:第 7 段「operator 實例與 pipe:順便瘦身 bundle」

見仁見智 11:40
「so your bundle size from let's say a few hundred kilobytes becomes like only three or four kilobytes」
數字要打折看。省下來的是「沒用到的 operator」,不是整個 RxJS——Observable、Subject、Subscription 這些核心結構仍然在 bundle 裡,實際大小取決於用了哪些 operator,一般落在數十 KB 等級(gzip 後約十幾 KB)。「幾百 KB 掉到三四 KB」是把最壞情況和最好情況放在一起比的說法。
依據: RxJS 6+ 只 import 少量 operator 的實測 bundle 一般在 10–40 KB(min)之譜,非 3–4 KB
留給下一段 到這裡 RxJS 這一側幾乎完整了,只剩一個第 4 段埋下的限制沒解決:值只能在 constructor 裡產生,外面送不進去。

8. Subject:讓外部把值送進 stream 12:31–14:03

前面的 Observable 有個限制:值只能在 constructor 裡產生,外面送不進去。但按鈕元件就是要把事件往回送、讓 stream 發出新值再當 props 流回來,形成單向循環。Subject 就是解法:它同時實作了 Observable 和 Observer,所以既有 subscribe,也有 next/error/complete。範例裡先 subscribe,再從外面呼叫三次 next,console 就依序印出 1、2、3;沒有 complete,它就一直等著下一個值。

observable ↔ button component 的循環示意圖,說明「為什麼需要一個能從外面餵值的東西」
12:55 · observable ↔ button component 的循環示意圖,說明「為什麼需要一個能從外面餵值的東西」
Subject 的程式碼:subscribe 與 next 同時掛在同一個物件上,是它「兩種身分」的證據
13:30 · Subject 的程式碼:subscribe 與 next 同時掛在同一個物件上,是它「兩種身分」的證據
承上 上一段最後點出的限制:值只能在 constructor 裡產生,外面送不進去。

推理因為那個限制在 UI 上會立刻卡住——按鈕被點、輸入框被打字,這些值天生來自外部——作者引入 Subject 來補洞。他先畫出需要的形狀:observable 把 props 給元件,元件把事件送回來,形成一個循環;能同時做這兩件事的東西就是 Subject。定義很乾脆:Subject 同時是 Observable 也是 Observer,所以它既有 subscribe,也有 next / error / complete。程式示範是先 new Subject()、先 subscribe(此時還是空的 stream),再從外面連叫三次 next(1)、next(2)、next(3),console 依序印出 1、2、3;因為沒有 complete,它就一直開著等下一個值。投影片同時列出 BehaviorSubject:帶初始值的 Subject。

AI 補充「先 subscribe 再 next」的順序不是巧合,而是必要的:一般的 Subject 不保存歷史,訂閱之前發出的值就永遠錯過了。這正是投影片上 BehaviorSubject 存在的理由——它記得最後一個值,新訂閱者一接上就先拿到當前狀態,這在 UI 上幾乎總是你要的(元件掛載時就該看到現在的值,而不是等下一次變化)。作者沒有展開,但這個差別是實務上最常見的 Subject 選型錯誤。

Subject

主體

同時實作 Observable 與 Observer 的型別:可以被 subscribe,也可以從外部呼叫 next 推值進去。

Subject 是 RxJS 裡唯一可以「從外面餵值」的入口,因此也是 stream 世界與事件世界的接縫。代價是它天生是 hot 的、共享同一份執行,而且沒有 lazy 的保護——值送出去就送出去了,沒人訂閱就消失。實務建議是把 Subject 藏在模組內部,對外只暴露 asObservable(),避免任何人都能往裡面塞值。

相關術語: Observable (它是其中一半)、Observer (它是另一半)、BehaviorSubject (帶初始值的版本)

出處:第 8 段「Subject:讓外部把值送進 stream」

BehaviorSubject

行為主體

帶初始值的 Subject,會保存最後一個值,新訂閱者一訂閱就立刻收到它。

投影片上與 Subject 並列,但沒有口頭展開。它是把 stream 當狀態容器用的關鍵:元件在任何時間點掛載,都能馬上拿到「現在的值」而不是空白。可以說 BehaviorSubject 就是一個最小的 store——這也是為什麼很多人用它取代小型的狀態管理函式庫。

相關術語: Subject (它的變形)

出處:第 8 段「Subject:讓外部把值送進 stream」

留給下一段 資料流的兩個方向都齊了。接下來把這個循環真的接到元件上:如果 props 本身就是一條 stream 會怎樣?

9. 把 props 當成 stream 14:03–17:12

轉到 component 層。他刻意用兩套 rendering library:React、Vue、Preact 都只是呈現層,邏輯應該全留在 RxJS 這一側。這些函式庫本來就是反應式的——收到新 props 就重繪——所以只要把 props 想成一條 stream,元件就能永遠是 stateless。單向是 observable → 元件;要接住 click、input 這些事件就再加 Subject:使用者打字 → handler 把值送進 Subject → Subject 非同步處理後把新 props 送回元件,變成受控輸入。連「打字時去伺服器檢查是否重複」這種非同步驗證錯誤,也能一起串成 props 流回去。

observable → component 的單向資料流圖,建立「props 是一條 stream」的心像
15:40 · observable → component 的單向資料流圖,建立「props 是一條 stream」的心像
加上 Subject 之後的雙向循環圖(input → subject → props → input),是整場 talk 的架構圖
16:25 · 加上 Subject 之後的雙向循環圖(input → subject → props → input),是整場 talk 的架構圖
承上 上一段留下的提議:資料流的兩個方向都齊了,把這個循環接到元件上會怎樣?

推理因為要接的是元件,作者先說明為什麼他刻意同時用 React 和 Vue:這些函式庫只是呈現層,邏輯應該全留在 RxJS 那側,所以哪一套並不重要。他接著點出一個關鍵觀察——這些函式庫本來就是反應式的:收到新 props 就重繪。那麼只要把 props 想成一條 stream,元件就可以永遠 stateless。投影片先畫單向:RxJS 的 Observable 把 props 推給 React 元件。但單向不夠,click、input 這些事件要能回來,於是把左邊換成 Subject:使用者在輸入框打字 → handleChange() 把值送回 Subject → Subject 非同步處理後把 inputValue 當 props 送回元件,形成受控輸入。他還補了一個延伸:打字時去伺服器檢查是否重複這種非同步驗證,錯誤訊息也能一起串成 props 流回去。

AI 補充這一段是整場的樞紐,值得把它翻譯成一句話:狀態不再住在元件裡,而是住在 stream 裡,元件只是這條 stream 的最後一個訂閱者。這麼做的直接後果是元件變成純函式——同樣的 props 永遠畫出同樣的畫面,測試時不必模擬生命週期。第二張圖的三條箭頭也是刻意的:inputValue 下去、handleChange 上來、inputValue 再下去——它畫的正是「受控輸入」的完整迴路,值繞了一圈才回到畫面,所以中間才有位置插入 debounce、驗證這些非同步處理。

術語:Stateless component

Props

屬性

父層傳給元件的輸入資料;在這場 talk 裡被重新想像成一條隨時間推送的 stream。

React 與 Vue 都把 props 視為唯讀輸入,收到新的就重繪——這個既有性質正是作者論證的支點:不需要改框架,只要把 props 的來源換成 stream。這個想法在 React 生態不算新(recompose 的 mapPropsStream 就是),但作者要的是不綁定特定框架的版本。

相關術語: Stateless component (它的唯一輸入)

出處:第 9 段「把 props 當成 stream」

Stateless component

無狀態元件

只依 props 決定畫面、自己不持有任何內部狀態的元件。

無狀態帶來三件事:可預測(同樣 props 同樣輸出)、好測試(不必模擬生命週期)、以及好重用(不綁任何資料來源)。注意它與「函式元件」不是同義詞——函式元件用了 useState 一樣有狀態;這裡指的是狀態完全外移到 stream。

相關術語: Props (唯一輸入)、Higher-order component (靠它接上 stream)

出處:第 9 段「把 props 當成 stream」

Controlled input

受控輸入

輸入框的值由外部狀態決定、變更透過事件送回去更新,值繞一圈才回到畫面。

受控輸入在 React 是標準做法,但通常那一圈是繞到元件自己的 state。作者把那一圈拉長,繞到 Subject 再回來——因為變長了,中間才有地方塞 debounceTime、去伺服器查重複、產生驗證錯誤這些非同步步驟,而元件本身完全不用知道。

相關術語: Subject (事件送回它)、Props (值由它回來)

出處:第 9 段「把 props 當成 stream」

留給下一段 概念圖很漂亮,但還沒有一行真正能跑的接線程式。下一段用一個最小的 interval 範例,把「產生 stream」與「接上元件」各做一次。

10. interval 範例,與今天手接的痛 17:12–19:45

具體做一次:interval(1000) 每秒吐一個遞增整數,但元件要的是 props 物件,所以用 map 把 n 轉成 { interval: n },prop stream 就成形了。問題出在「接」這一段——用今天的 React/Vue 寫法,你得先宣告 internal state、在 mount 時 subscribe、在值進來時 setState、render 再讀 state,最後還要在 unmount 時取消訂閱以免記憶體洩漏。他直說:要記的事太多了。

interval + map 產生 props stream 的程式,示範「stream 的形狀要配合元件的介面」
17:55 · interval + map 產生 props stream 的程式,示範「stream 的形狀要配合元件的介面」
React 與 Vue 手動 subscribe / setState / unsubscribe 的完整樣板,看到量才知道痛點有多大
19:00 · React 與 Vue 手動 subscribe / setState / unsubscribe 的完整樣板,看到量才知道痛點有多大
承上 上一段留下的要求:用最小的範例,把「產生 stream」與「接上元件」各做一次。

推理因為要具體,作者選了最小的例子:一個接收 interval prop、每秒加一的元件。產生 stream 這半很短——interval(1000) 每秒吐一個遞增整數,但元件要的是物件,所以 pipe 一個 map(n => ({ interval: n })) 把它轉成 { interval: 1 }、{ interval: 2 }…,props$ 就成形了。真正的問題出在「接」這半:他把 React 與 Vue 的寫法並排放上螢幕——宣告 state、在 componentDidMount / beforeMount 裡 subscribe、在值進來時 setState、render 讀 state、最後在 componentWillUnmount / beforeDestroy 裡 unsubscribe 以免記憶體洩漏。兩側都是整整一屏。他的評語很直接:要記的事太多了。

AI 補充這張並排投影片是整場最有說服力的一張,因為它讓「痛」變成可以用眼睛測量的東西:左右兩邊的差異只有語法,樣板的形狀一模一樣——這正暗示了它可以被抽出來。值得一提的是最後那個 unsubscribe:它不是最佳實踐建議,而是必要動作。interval 是永不結束的 stream(回到第 2 段那條沒有豎線的時間軸),元件消失後它仍然每秒推值、仍然抓著 setState 的參照,記憶體洩漏就是這樣來的。

術語:Subscription

interval

間隔計時流

RxJS 的 creation operator,每隔指定毫秒推出一個遞增整數,且永不自行結束。

它是最容易上手的 stream 來源,也是最容易示範取消的:因為它永遠不會 complete,不 unsubscribe 就會一直跑下去。相近的 timer 可以指定首次延遲,而 interval 的第一個值要等一個完整週期才出現——想要立刻先有一個值,通常會再串 startWith。

相關術語: Operator (creation 類)、Memory leak (不取消會造成)

出處:第 10 段「interval 範例,與今天手接的痛」

Subscription

訂閱物件

subscribe 的回傳值,代表一次進行中的執行,呼叫 unsubscribe 即可終止它。

把它想成資源的把手:有借(subscribe)就要有還(unsubscribe)。多個 subscription 可以 add 進同一個父 subscription 一起取消,這是元件卸載時常見的收尾寫法。RxJS 後來也提供 takeUntil(destroy$) 這種宣告式的收尾方式,把取消寫進 pipe 裡而不是生命週期方法裡。

相關術語: subscribe (它的回傳值)、Cancellation (由它執行)

出處:第 10 段「interval 範例,與今天手接的痛」

Memory leak

記憶體洩漏

元件已卸載但訂閱還活著,來源持續推值並抓住已無用的參照,記憶體無法回收。

在 stream 的情境裡它幾乎總是同一個原因:忘了 unsubscribe 一條永不結束的 stream。症狀除了記憶體成長,還有更早被發現的那種——對已卸載元件呼叫 setState 的警告。會自行 complete 的 stream(例如一次性的 HTTP 請求)不會有這個問題,這也是為什麼判斷「這條 stream 會不會結束」很重要。

相關術語: Subscription (沒收好造成)、interval (典型來源)

出處:第 10 段「interval 範例,與今天手接的痛」

見仁見智 19:10
「you also need to be able to like cancel that subscription so that there is no memory leak」
痛點仍在,但今天的樣板小得多。這是 2018 年 class component 與 Vue 2 Options API 的寫法;現在 React 用 useEffect 訂閱並在回傳的清理函式裡 unsubscribe(或直接用 useSyncExternalStore),Vue 3 用 onScopeDispose 或 VueUse 的 useObservable,都只需要幾行。取消訂閱這件事本身沒有消失,只是不再散在三個生命週期方法裡。
依據: React 16.8(2019)hooks、Vue 3(2020)Composition API 之後的慣用寫法
留給下一段 樣板的形狀左右一致,這件事本身就是線索:既然形狀固定,它就應該可以被抽成一個共用的東西。下一段追問:那個東西該長什麼樣,現成方案為什麼還不夠?

11. 理想的接法、三個好處,與既有方案的限制 19:45–22:49

他要的是:左邊 stateless 元件、右邊 prop stream,中間一個 higher-order component 把兩者接起來,其他都別讓我知道。這樣換來三個好處:程式碼變得函數式、由上往下讀就懂;邏輯與呈現徹底分離,行為都留在 RxJS 那側;元件無狀態因此好測試。既有方案 redux-observable、recycle、vuerx、vue-streams 他都用過,但兩個限制沒解決:走 Redux 等於把自己綁在一個全應用的 store 上,而他只想要「一條 stream 加一個元件」;而且多數方案只能產生新的 observable 往下傳,沒辦法把父層傳進來的 props 也當成 stream 接住。

左 React 右 Vue 的 stateless 元件對照,兩邊幾乎一樣,支撐「呈現層可以互換」的主張
20:00 · 左 React 右 Vue 的 stateless 元件對照,兩邊幾乎一樣,支撐「呈現層可以互換」的主張
「中間缺一個 magic HOC」的示意圖,是下一段 proppy 要填的那個洞
20:25 · 「中間缺一個 magic HOC」的示意圖,是下一段 proppy 要填的那個洞
承上 上一段留下的線索:樣板的形狀左右一致,所以它應該可以被抽出來——那個東西該長什麼樣?

推理因為形狀固定,作者直接畫出他要的介面:左邊一個 stateless 元件、右邊一條 props$,中間一個 higher-order component 把兩者接起來,寫成一行 link(props$)(Interval),其他都別讓我知道。他接著說明這樣換來三個好處:程式碼變得函數式、由上往下讀就懂,不必在內部狀態之間來回追;邏輯與呈現徹底分離,行為全留在 RxJS 那側;元件無狀態,因此容易測試。然後他誠實地檢視既有方案——redux-observable(用 epic 把 dispatch 的 action 當成 stream 處理)、recycle、vuerx、vue-streams——他都用過,但有兩個限制沒被解決:走 Redux 等於把自己綁在一個全應用的 store 上,而他要的只是「一條 stream 加一個元件」;而且多數方案只能往下產生新的 observable,沒辦法把父層傳進來的 props 也當成 stream 接住。

AI 補充請注意這兩個限制不是抱怨,而是規格書:它們正好定義了下一段那個工具必須做到什麼。第二個限制尤其關鍵,值得翻成具體場景——如果上游 props 不是 stream,那麼「prop 變了要取消前一個請求」這件事就只能回到生命週期比對舊值新值,也就回到了上一段那堆樣板。至於第一個限制,它其實是「元件層級的狀態」與「應用層級的狀態」之爭:Redux 假設你想要後者,而作者主張很多時候你只需要前者。

Higher-order component

高階元件(HOC)

接收一個元件、回傳一個加了額外能力的新元件的函式。

HOC 是 React 生態在 hooks 之前共用邏輯的主要手法(connect、withRouter 都是),優點是被包的元件完全不必知道自己被包了,所以能保持 stateless;缺點是包多層之後除錯時的元件樹會很深。在這場 talk 裡它扮演的角色很單純:唯一知道 RxJS 的那一層。

相關術語: Stateless component (包住它)、Props (注入它們)

出處:第 11 段「理想的接法、三個好處,與既有方案的限制」

redux-observable

Redux 的 RxJS 中介層

把 Redux 派發的 action 當成一條 stream,用 RxJS 處理副作用後再派發新 action 的中介層。

它的核心概念是 epic:一個接收 action$ 並回傳 action$ 的函式,形狀正是「stream 進、stream 出」。作者的批評不是它做得不好,而是它的粒度——它假設你已經採用 Redux 的全應用單一 store,而他想要的是元件層級的 stream。

相關術語: Epic (核心概念)、Higher-order component (另一種接法)

出處:第 11 段「理想的接法、三個好處,與既有方案的限制」

Epic

史詩函式

redux-observable 的核心:接收 action stream、回傳新的 action stream 的函式。

epic 是理解「stream in, stream out」最好的例子——它不直接改 state,只是宣告「看到這種 action 就產生那種 action」。作者在下一段提出的 withStream 走的是同一個形狀,差別在於進出的不是 action 而是 props。

相關術語: redux-observable (屬於它)

出處:第 11 段「理想的接法、三個好處,與既有方案的限制」

留給下一段 規格書寫好了:要能接、還要能把進來的 props 當 stream 接住。下一段就是照這份規格做出來的東西。

12. proppy:attach、withObservable、withStream 22:49–25:42

他的解法:withObservable 把來源 observable 轉成 props,attach 這個 HOC 把 composition 接到 stateless 元件上。左右兩份程式碼唯一的差別是 import 的套件名(proppy-react vs proppy-vue)。再進一步是 withStream:它讓你把「傳進來的 props」當成 stream 收,再回傳一條新的 stream——路由參數 product ID 從 1 變成 2 時,可以取消前一個請求、直接發新的,新 props 立刻流回元件。此外也顧到 SSR(先在伺服器端把 static props 算好再輸出 HTML)與全應用設定(API endpoint、主題,甚至 Redux store 都能當成一個 composition 取用)。

attach + withObservable 的 React/Vue 並排程式,證明「只有 import 那行不一樣」
23:15 · attach + withObservable 的 React/Vue 並排程式,證明「只有 import 那行不一樣」
withStream 的程式:incoming props 進來是 stream、回傳的也是 stream,這是他說的 stream in / stream out
24:25 · withStream 的程式:incoming props 進來是 stream、回傳的也是 stream,這是他說的 stream in / stream out
承上 上一段寫下的規格書:要能把 stream 接上元件,還要能把進來的 props 也當成 stream 接住。

推理因為規格已經明確,作者直接給出他的實作 proppy。第一層對應「能接」:withObservable 把來源 observable 轉成 props,attach 這個 HOC 把這份 composition 接到 stateless 元件上。他請大家找出左右兩邊的差異——只有 import 的套件名不同(proppy-react vs proppy-vue),這是他前面「呈現層可以互換」主張的直接證據。第二層對應「能接住上游」:proppy-rx 的 withStream 讓你把 incomingProps$ 當成 stream 收進來、再回傳一條新的 props$,也就是投影片標題那句 stream in => stream out。他用路由參數舉例:product ID 從 1 變成 2 時,可以取消前一個請求、直接發新的,新 props 立刻流回元件。最後補兩個實務缺口:SSR(先在伺服器端把 static props 算好再輸出 HTML)與全應用設定(API endpoint、主題,甚至 Redux store 都能當成一個 composition 取用)。

AI 補充值得指出的是,最後那個「Redux store 也能當成 composition」其實回應了上一段對 Redux 的批評——他不是反對全域 store,而是反對被強迫接受它;在這個設計裡全域 store 只是眾多來源的一種。另外 SSR 那段是誠實的:全面 stream 化之後,伺服器端渲染確實會變麻煩,因為 HTML 必須在某個時間點定格,而 stream 沒有天然的定格點——所以才需要一個專門的機制先把 static props 算完。這是採用這套做法前最該先確認的一項成本。

proppy

(函式庫名)

作者做的函式庫:用 composition 產生 props,再用 attach 把它接到 React 或 Vue 的無狀態元件上。

它拆成幾個套件:proppy 是核心與通用 composition(withObservable、withProps…),proppy-react / proppy-vue 是各框架的 attach,proppy-rx 是 RxJS 專用的 withStream。設計上刻意不綁 RxJS——核心不依賴它,要用才裝 proppy-rx。要提醒的是這是 2018 年的個人專案,導入前值得先確認它現在的維護狀態。

相關術語: withStream (它的 RxJS 部分)、Higher-order component (attach 是一個)

出處:第 12 段「proppy:attach、withObservable、withStream」

withStream

串流組合器

proppy-rx 提供的 composition:接收 incoming props 的 stream,回傳一條新的 props stream。

它的簽名 (incomingProps$) => props$ 就是重點:因為輸入也是 stream,「上游 prop 改變」變成 stream 上的一個事件,可以直接用 switchMap 這類 operator 取消前一個進行中的請求。這正是作者說既有方案缺少的那一半,也是 epic 的 (action$) => action$ 在 props 世界的對應物。

相關術語: proppy (屬於它)、Epic (同樣形狀)

出處:第 12 段「proppy:attach、withObservable、withStream」

Server-side rendering (SSR)

伺服器端渲染

在伺服器上先把元件渲染成 HTML 再送給瀏覽器,前端接手後續互動。

SSR 與 stream 天生有張力:HTML 必須在某一刻定稿,但 stream 沒有天然的終點。所以這類函式庫都得提供一個「先把值算完」的機制(proppy 是 static props),概念上等同於 Next.js 的資料預取階段。若你的應用需要 SEO 或首屏速度,這是評估任何 stream 化方案時必須先問的問題。

相關術語: proppy (由它支援)

出處:第 12 段「proppy:attach、withObservable、withStream」

見仁見智 25:08
「so this package also takes care of like generating some static props on the server site」
功能敘述沒問題,但採用建議需要更新:proppy 是 2018 年前後的專案,之後幾乎沒有新版本,社群使用者也很少。今天要達成同樣目的,較常見的做法是直接在 React 用 useSyncExternalStore 或 useObservable(observable-hooks),Vue 3 用 VueUse 的 useObservable / from。真正該帶走的是這一段的設計想法,而不是這個套件本身。
依據: proppy 自 2018–2019 年後未再有明顯發布;React 18 提供 useSyncExternalStore(2022)、VueUse 提供 RxJS 整合
留給下一段 工具講完了,剩下最後一個問題:這一整套值不值得你導入?

13. 結語:先有 use case 再用 Rx 25:42–26:44

收尾很誠實:Rx 很重、學習曲線很陡,不要因為別人在用就用。真的被非同步資料流折磨時,它會讓困難的事變得非常容易;但沒想清楚就用,它也會讓簡單的事變得很難。

承上 上一段留下的最後一個問題:這一整套值不值得導入?

推理因為前面花了二十幾分鐘證明「可以做到」,作者在最後刻意拉回「該不該做」。他的回答沒有迴避成本:Rx 很重、學習曲線很陡,不要因為別人在用就用。判準是有沒有真實的使用情境——如果你真的在跟非同步資料流搏鬥,它會讓困難的事變得非常容易;但沒想清楚就用,它也會讓簡單的事變得很難。

AI 補充這句忠告可以直接翻成一組檢查清單:你的畫面是否需要合併多個非同步來源?是否需要在事件之間取消先前的工作?是否有 debounce、retry、重播這類時間相關的需求?三題都答不出來,那麼 useState 加 fetch 就夠了。反過來說,一旦你發現自己在手寫「這個請求過期了要忽略」「這個訂閱要記得清掉」的旗標,那就是 stream 已經在你的程式裡了,只是還沒有名字。

留給下一段 總結收束。

4. 總結

整支影片是一條沒有跳步的直線。作者先用「房間裡的人數會變」把「值隨時間變化」變成看得見的東西,再用 marble diagram 給它一套記號;記號需要型別來承載,於是 Observable 登場,並且刻意用「跟 Promise 差在哪」來定義——多值、lazy、可取消,其中 lazy 與可取消其實是同一件事的兩面。有了型別就進到程式:constructor 裡用 observer.next 產值、外面用 subscribe 收值,同時埋下一個限制——值只能在 constructor 裡產生。接著他把鏡頭拉遠介紹 RxJS 提供的三類 API,並用即時、取消、離線重試三個前端日常場景說明為什麼值得學,再用 filter → map 的 marble 圖與 pipe 寫法把 operator 跑過一次。到這裡 RxJS 這一側完整了,只剩那個限制沒解——Subject 補上它,因為它同時是 Observable 與 Observer,外面的事件才送得進來,資料流才成為循環。第二幕轉向元件:既然 React/Vue 收到新 props 就重繪,那把 props 想成 stream,元件就能永遠 stateless、邏輯全留在 RxJS 那側。他用最小的 interval 範例把「產生 stream」與「接上元件」各做一次,讓 componentDidMount/setState/componentWillUnmount 的樣板攤在螢幕上——左右兩邊形狀一致,這本身就說明它可以被抽走。最後他檢視既有方案的兩個限制(被綁在全域 store、接不住上游 props),這兩個限制正好是他的 proppy 的規格書:attach 負責接,withStream 負責讓進來的 props 也是 stream。收尾他反過來提醒 Rx 的學習曲線,把「該不該用」的決定權交還給你。

勘誤總整理

確定錯誤/已過時見仁見智(取決於版本或情境)

段落原話(transcript 逐字)說明
3. Observable:和 Promise 差在哪
4:00
「it's a new type in JavaScript currently it's being standardized by the tc39」這句在 2018 年說得過去,今天已經不成立:TC39 的 proposal-observable 長年停在早期階段、實質停擺,Observable 至今沒有進入 ECMAScript 標準。後來真正有進展的是另一條路——WHATWG 把 Observable 寫進 DOM 規格(搭配 EventTarget 的 when()),走的是瀏覽器 API 而不是語言核心。實務結論不變:今天要用 Observable,還是得靠 RxJS。
依據: tc39/proposal-observable 自 2018 年後未再推進;WHATWG DOM 於 2024 年納入 Observable 規格
7. operator 實例與 pipe:順便瘦身 bundle
11:40
「so your bundle size from let's say a few hundred kilobytes becomes like only three or four kilobytes」數字要打折看。省下來的是「沒用到的 operator」,不是整個 RxJS——Observable、Subject、Subscription 這些核心結構仍然在 bundle 裡,實際大小取決於用了哪些 operator,一般落在數十 KB 等級(gzip 後約十幾 KB)。「幾百 KB 掉到三四 KB」是把最壞情況和最好情況放在一起比的說法。
依據: RxJS 6+ 只 import 少量 operator 的實測 bundle 一般在 10–40 KB(min)之譜,非 3–4 KB
10. interval 範例,與今天手接的痛
19:10
「you also need to be able to like cancel that subscription so that there is no memory leak」痛點仍在,但今天的樣板小得多。這是 2018 年 class component 與 Vue 2 Options API 的寫法;現在 React 用 useEffect 訂閱並在回傳的清理函式裡 unsubscribe(或直接用 useSyncExternalStore),Vue 3 用 onScopeDispose 或 VueUse 的 useObservable,都只需要幾行。取消訂閱這件事本身沒有消失,只是不再散在三個生命週期方法裡。
依據: React 16.8(2019)hooks、Vue 3(2020)Composition API 之後的慣用寫法
12. proppy:attach、withObservable、withStream
25:08
「so this package also takes care of like generating some static props on the server site」功能敘述沒問題,但採用建議需要更新:proppy 是 2018 年前後的專案,之後幾乎沒有新版本,社群使用者也很少。今天要達成同樣目的,較常見的做法是直接在 React 用 useSyncExternalStore 或 useObservable(observable-hooks),Vue 3 用 VueUse 的 useObservable / from。真正該帶走的是這一段的設計想法,而不是這個套件本身。
依據: proppy 自 2018–2019 年後未再有明顯發布;React 18 提供 useSyncExternalStore(2022)、VueUse 提供 RxJS 整合

5. 推薦三個下一步

1. 往下挖深:搞懂 flattening operator

第 12 段的「prop 一變就取消前一個請求」全靠 switchMap,但影片只示範了 map 與 filter。switchMap/mergeMap/concatMap/exhaustMap 的取捨是 RxJS 從會用到用對的分水嶺,也是取消真正發生的地方。

YouTube 搜尋:RxJS switchMap vs mergeMap vs concatMap RxJS flattening operators explained switchMap cancel previous request

2. 往旁邊對照:signals 與現代反應式方案

影片停在 2018 年的 HOC 解法。之後前端的反應式主流換成了 signals(Solid、Angular signals、Vue 的 ref)與 React 的 useSyncExternalStore,同樣解決「狀態外移」但心智模型完全不同,對照著看才知道 stream 的長處與代價各在哪。

YouTube 搜尋:signals vs observables frontend Angular signals vs RxJS useSyncExternalStore observable React

3. 往上應用:用 RxJS 做一個真實的搜尋輸入框

第 9 段的受控輸入加非同步驗證只有概念圖。把 debounceTime + distinctUntilChanged + switchMap 串起來做一個會取消前一次查詢的搜尋框,是最短的一條把整場觀念落地的路。

YouTube 搜尋:RxJS typeahead debounceTime switchMap RxJS autocomplete search tutorial React RxJS search input example

📄 全部影片 · 主題區: 歷史參考(已過時)