原文連結: medium.com/@benlesh/ho…
本文為 RxJS 中文社群 翻譯文章,如需轉載,請註明出處,謝謝合作!
如果你也想和我們一起,翻譯更多優質的 RxJS 文章以奉獻給大家,請點選【這裡】
TL;DR: 當不想一遍又一遍地建立生產者( producer )時,你需要熱的 Observable 。
冷的是指 Observable 建立了生產者
// 冷的
var cold = new Observable((observer) => {
var producer = new Producer();
// observer 會監聽 producer
});複製程式碼
熱的是指 Observable 複用生產者
// 熱的
var producer = new Producer();
var hot = new Observable((observer) => {
// observer 會監聽 producer
});複製程式碼
深入瞭解發生了什麼...
我最新的文章通過構建 Observable 來學習 Observable 主要是為了說明 Observable 只是函式。這篇文章的目標是為了揭開 Observable 自身的神祕面紗,但它並沒有真正深入到 Observable 讓初學者最容易困惑的問題: “熱”與“冷”的概念。
Observables 只是函式而已!
Observables 是將觀察者和生產者聯絡起來的函式。 僅此而已。它們並不一定要建立生產者,它們只需建立觀察者來監聽生產者,並且通常會返回一個拆卸機制來刪除該監聽器。
什麼是“生產者”?
生產者是 Observable 值的來源。它可以是 Web Socket、DOM 事件、迭代器或在陣列中迴圈的某種東西。基本上,這是你用來獲取值的任何東西,並將它們傳遞給 observe.next(value)
。
冷的 Observables: 在內部建立生產者
如果底層的生產者是在訂閱期間建立並啟用的,那麼 Observable 就是“冷的”。這意味著,如果 Observables 是函式,而生產者是通過呼叫該函式建立並啟用的。
- 建立生產者
- 啟用生產者
- 開始監聽生產者
- 單播
下面的示例 Observable 是“冷的”,因為它在訂閱函式(在訂閱該 Observable 時呼叫)中建立並監聽了 WebSocket :
const source = new Observable((observer) => {
const socket = new WebSocket('ws://someurl');
socket.addEventListener('message', (e) => observer.next(e));
return () => socket.close();
});複製程式碼
所以任何 source
的訂閱都會得到自己的 WebSocket 例項,當取消訂閱時,它會關閉 socket 。這意味著 source
是真正的單播,因為生產者只會傳送給一個觀察者。這是用來闡述概念的基礎 JSBin 示例。
熱的 Observables: 在外部建立生產者
如果底層的生產者是在 訂閱¹ 外部建立或啟用的,那麼 Observable 就是“熱的”。
- 共享生產者的引用
- 開始監聽生產者
- 多播(通常情況下²)
如果我們沿用上面的示例並將 WebSocket 的建立移至 Observable 的外部,那麼 Observable 就會變成“熱的”:
const socket = new WebSocket('ws://someurl');
const source = new Observable((observer) => {
socket.addEventListener('message', (e) => observer.next(e));
});複製程式碼
現在任何 source
的訂閱都會共享同一個 WebSocket 例項。它實際上會多播給所有訂閱者。但還有個小問題: 我們不再使用 Observable 來執行拆卸 socket 的邏輯。這意味著像錯誤和完成這樣的通知不再會為我們來關閉 socket ,取消訂閱也一樣。所以我們真正想要的其實是使“冷的” Observable 變成“熱的”。這是用來展示基礎概念的 JSBin 示例。
為什麼要變成“熱的” Observable ?
從上面展示冷的 Observable 的第一個示例中,你可以發現所有冷的 Observables 可能都會些問題。就拿一件事來說,如果你不止一次訂閱了 Observable ,而這個 Observable 本身建立一些稀缺的資源,比如 WebSocket 連線,你不想一遍又一遍地建立這個 WebSocket 連線。實際上真的很容易建立了一個 Observable 的多個訂閱而卻沒有意識到。假如說你想要在 WebSocket 訂閱外部過濾所有的“奇數”和“偶數”值。在此場景下最終你會建立兩個訂閱:
source.filter(x => x % 2 === 0)
.subscribe(x => console.log('even', x));
source.filter(x => x % 2 === 1)
.subscribe(x => console.log('odd', x));複製程式碼
Rx Subjects
在將“冷的” Observable 變成“熱的”之前,我們需要介紹一個新的型別: Rx Subject 。它有如下特性:
- 它是 Observable 。它的結構類似 Observable 並擁有 Observable 的所有操作符。
- 它是 Observer 。它作為 Observer 的鴨子型別。當作為 Observable 被訂閱時,將作為 Observer 發出
next
的任何值。 - 它是多播的。所有通過
subscribe()
傳遞給它的 Observers 都會被新增到內部的觀察者列表。 - 當它完成時,就是完成了。Subjects 在取消訂閱、完成或發生錯誤後無法被複用。
- 它通過自身傳遞值。需要重申下第2點。如果
next
值給它,值會從它 observable 那面出來。
Rx 中的 Subject 之所以叫做 “Subject” 是因為上面的第3點。在 GoF (譯註: 大名鼎鼎的《設計模式》一書) 的觀察者模式中,“Subjects” 通常是有 addObserver
的類。在這裡,我們的 addObserver
方法就是 subscribe
。這是用來展示 Rx Subject 的基礎行為的 JSBin 示例。
將冷的 Observable 變成熱的
瞭解了上面的 Rx Subject 後,我們可以使用一些功能性的程式將任何“冷的” Observable 變成“熱的”:
function makeHot(cold) {
const subject = new Subject();
cold.subscribe(subject);
return new Observable((observer) => subject.subscribe(observer));
}複製程式碼
我們的新方法 makeHot
接收任何冷的 Observable 並通過建立由所得到的 Observable 共享的 Subject 將其變成熱的。這是用來演示 JSBin 示例。
還有一點問題,就是沒有追蹤源的訂閱,所以當想要拆卸時該如何做呢?我們可以新增一些引用計數來解決這個問題:
function makeHotRefCounted(cold) {
const subject = new Subject();
const mainSub = cold.subscribe(subject);
let refs = 0;
return new Observable((observer) => {
refs++;
let sub = subject.subscribe(observer);
return () => {
refs--;
if (refs === 0) mainSub.unsubscribe();
sub.unsubscribe();
};
});
}複製程式碼
現在我們有了一個熱的 Observable ,當它的所有訂閱結束時,我們用來引用計數的 refs
會變成0,我們將取消冷的源 Observable 的訂閱。這是用來演示的 JSBin 示例。
在 RxJS 中, 使用 publish()
或 share()
你可能不應該使用上面提及的任何 makeHot
函式,而是應該使用像 publish()
和 share()
這樣的操作符。將冷的 Observable 變成熱的有很多種方式和手段,在 Rx 中有高效和簡潔的方式來完成此任務。關於 Rx 中可以做此事的各種操作符可以寫上一整篇文章,但這不是文字的目標。本文的目標是鞏固概念,什麼是“熱的”和“冷的” Observable 以及它們的真正意義。
在 RxJS 5 中,操作符 share()
會產生一個熱的,引用計數的 Observable ,它可以在失敗時重試,或在成功時重複執行。因為 Subjects 一旦發生錯誤、完成或取消訂閱,便無法複用,所以 share()
操作符會重複利用已完結的 Subjects,以使結果 Observable 啟用重新訂閱。
這是 JSBin 示例,演示了在 RxJS 5 中使用 share()
將源 Observable 變熱,以及它可以重試。
“暖的” Observable
鑑於上述一切,人們能夠看到 Observable 是怎樣的,它只是一個函式,實際上可以同時是“熱的”和“冷的”。或許它觀察了兩個生產者?一個是它建立的而另一個是它複用的?這可能不是個好主意,但有極少數情況下可能是必要的。例如,多路複用的 WebSocket 必須共享 socket ,但同時傳送自己的訂閱並過濾出資料流。
“熱的”和“冷的”都關乎於生產者
如果在 Observable 中複用了生產者的共享引用,它就是“熱的”,如果在 Observable 中建立了新的生產者,它就是“冷的”。如果兩者都做了…。那它到底是什麼?我猜是“暖的”。
註釋
¹ (注意: 生產者在訂閱內部“啟用”,直到未來某個時間點才“建立”出來,這種做法是有些奇怪,但使用代理的話,這也是可能的。) 通常“熱的” Observables 的生產者是在訂閱外部建立和啟用的。
² 熱的 Observables 通常是多播的,但是它們可能正在監聽一個只支援一個監聽器的生產者。在這一點上將其稱之為“多播”有點勉強。