原文鏈接: https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339
本文為 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: 在外部創建生產者
如果底層的生產者是在 訂閱1 外部創建或激活的,那么 Observable 就是“熱的”。
- 共享生產者的引用
- 開始監聽生產者
- 多播(通常情況下2)
如果我們沿用上面的示例并將 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 中創建了新的生產者,它就是“冷的”。如果兩者都做了…。那它到底是什么?我猜是“暖的”。
注釋
1 (注意: 生產者在訂閱內部“激活”,直到未來某個時間點才“創建”出來,這種做法是有些奇怪,但使用代理的話,這也是可能的。) 通常“熱的” Observables 的生產者是在訂閱外部創建和激活的。
2 熱的 Observables 通常是多播的,但是它們可能正在監聽一個只支持一個監聽器的生產者。在這一點上將其稱之為“多播”有點勉強。