原文連結: blog.angularindepth.com/rxjs-how-to…
本文為 RxJS 中文社群 翻譯文章,如需轉載,請註明出處,謝謝合作!
如果你也想和我們一起,翻譯更多優質的 RxJS 文章以奉獻給大家,請點選【這裡】
在我的上篇文章 理解 publish 和 share 操作符中,只是簡單介紹了 refCount 方法。在這篇文章中我們將深入介紹。
refCount 的作用是什麼?
簡單回顧一下, RxJS 多播的基本心智模型包括: 一個源 observable,一個訂閱源 observable 的 subject 和多個訂閱 subject 的觀察者。multicast
操作符封裝了基於 subject 的基礎結構並返回擁有 connect
和 refCount
方法的 ConnectableObservable
。
顧名思義,refCount
返回的 observable 維護訂閱者的引用計數。
當觀察者訂閱有引用計數的 observable 時,引用計數會增加,如果上一個引用計數為零的話,負責多播基礎結構的 subject 會訂閱源 observable 。而當觀察者取消訂閱時,引用計數則會減少,如果引用計數歸零的話,subject 會取消源 observable 的訂閱。
這種引用計數的行為有兩種用途:
- 當所有觀察者都取消訂閱後,自動取消 subject 對源 observable 的訂閱
- 當所有觀察者都取消訂閱後,自動取消 subject 對源 observable 的訂閱,然後當再有觀察者訂閱該引用計數的 observable 時,subject 重新訂閱源 observable
我們來詳細介紹每一種情況,然後建立一些使用 refCount
的通用指南。
使用 refCount 自動取消訂閱
publish
操作符返回 ConnectableObservable
。呼叫 ConnectableObservable
的 connect
方法時,負責多播基礎結構的 subject 會訂閱源 observable 並返回 subscription (訂閱)。subject 會保持對源 observable 的訂閱直到呼叫 subscription 的 unsubscribe
方法。
我們來看下面的示例,觀察者會接收一個值,然後(隱式地)取消對呼叫過 publish
的 observable 的訂閱:
const source = instrument(Observable.interval(100));
const published = source.publish();
const a = published.take(1).subscribe(observer("a"));
const b = published.take(1).subscribe(observer("b"));
const subscription = published.connect();
複製程式碼
本文中的示例都將使用下面的工具函式來讓源 observable 具備日誌功能,以及建立有名稱的觀察者:
function instrument<T>(source: Observable<T>) {
return Observable.create((observer: Observer<T>) => {
console.log("source: subscribing");
const subscription = source
.do(value => console.log(`source: ${value}`))
.subscribe(observer);
return () => {
subscription.unsubscribe();
console.log("source: unsubscribed");
};
}) as Observable<T>;
}
function observer<T>(name: string) {
return {
next: (value: T) => console.log(`observer ${name}: ${value}`),
complete: () => console.log(`observer ${name}: complete`)
};
}
複製程式碼
示例的輸出如下所示:
source: subscribing
source: 0
observer a: 0
observer a: complete
observer b: 0
observer b: complete
source: 1
source: 2
source: 3
...
複製程式碼
兩個觀察者都只接收一個值然後完成,完成的同時取消對呼叫過 publish
的 observable 的訂閱。但是,多播基礎結構仍然保持著對源 observable 的訂閱。
如果不想顯示地執行取消訂閱操作的話,可以使用 refCount
:
const source = instrument(Observable.interval(100));
const counted = source.publish().refCount();
const a = counted.take(1).subscribe(observer("a"));
const b = counted.take(1).subscribe(observer("b"));
複製程式碼
觀察者訂閱使用引用計數的 observable 的話,當引用計數歸零時,負責多播的基礎結構的 subject 會取消源 observable 的訂閱,示例的輸出如下所示:
source: subscribing
source: 0
observer a: 0
observer a: complete
observer b: 0
observer b: complete
source: unsubscribed
複製程式碼
重新訂閱已完成的 observables
當引用計數歸零後,多播的基礎結構除了取消源 observable 的訂閱,當負責引用計數的 observable 再次發生訂閱時,它還會重新訂閱源 observable 。
我們使用下面的示例來看看當使用已完成的源 observable 時會發生什麼:
const source = instrument(Observable.timer(100));
const counted = source.publish().refCount();
const a = counted.subscribe(observer("a"));
setTimeout(() => a.unsubscribe(), 110);
setTimeout(() => counted.subscribe(observer("b")), 120);
複製程式碼
示例中使用 timer
observable 作為源。它會等待指定的毫秒數後發出 next
和 complete
通知。還有兩個觀察者: a
在源 observable 完成後訂閱,在源 observable 完成後取消訂閱;b
在 a
取消訂閱後訂閱。
示例的輸出如下:
source: subscribing
source: 0
observer a: 0
source: unsubscribed
observer a: complete
observer b: complete
複製程式碼
當 b
訂閱時,引用計數為零,所以多播的基礎結構會期望 subject 重新訂閱源 observable 。但是,由於 subject 已經收到了源 observable 的 complete
通知,並且 subject 是無法複用的,所以實際上並沒有進行重新訂閱,b
只能收到 complete
通知。
如果使用 publishBehavior(-1)
來代替 publish()
的話,輸出類似,但會包含 BehaviorSubject
的初始值:
observer a: -1
source: subscribing
source: 0
observer a: 0
source: unsubscribed
observer a: complete
observer b: complete
複製程式碼
同樣的,b
還是隻能收到 complete
通知。
如果使用 publishReplay(1)
來代替 publish()
的話,情況會有些變化,輸出如下:
source: subscribing
source: 0
observer a: 0
source: unsubscribed
observer a: complete
observer b: 0
observer b: complete
複製程式碼
同樣的,這次也沒有重新訂閱源 observable,因為 subject 已經完成了。但是,已完成的 ReplaySubject
將通知重放給後來的訂閱者,所以 b
能收到重放的 next
通知和 complete
通知。
如果使用 publishLast()
來代替 publish()
的話,情況又會有些不同,輸出如下:
source: subscribing
source: 0
source: unsubscribed
observer a: 0
observer a: complete
observer b: 0
observer b: complete
複製程式碼
同樣的,依然沒有重新訂閱源 observable,因為 subject 已經完成了。但是,AsyncSubject
會將最後收到的 next
通知發給它的訂閱者,所以 a
和 b
都收到的是 next
和 complete
通知。
綜上所述,根據示例我們可以發現 publish
以及它的變種:
- 當源 observable 完成時,負責多播基礎結構的 subject 也會完成,而且這會阻止對源 observable 的重新訂閱。
- 當
publish
和publishBehavior
與refCount
一起使用時,後來的訂閱者只會收到complete
通知,這似乎並不是我們想要的效果。 - 當
publishReplay
和publishLast
與refCount
一起使用時,後來的訂閱者會收到預期的通知。
重新訂閱未完成的 observables
我們已經看過了重新訂閱已完成的源 observable 時會發生什麼,現在我們再來看看重新訂閱未完成的源 observable 是怎樣一個情況。
這個示例中將使用 interval
observable 來替代 timer
observable,它會根據指定的時間間隔重複地發出包含自增數字的 next
通知:
const source = instrument(Observable.interval(100));
const counted = source.publish().refCount();
const a = counted.subscribe(observer("a"));
setTimeout(() => a.unsubscribe(), 110);
setTimeout(() => counted.subscribe(observer("b")), 120);
複製程式碼
示例的輸出如下所示:
source: subscribing
source: 0
observer a: 0
source: unsubscribed
source: subscribing
source: 0
observer b: 0
source: 1
observer b: 1
...
複製程式碼
與使用已完成的源 observable 的示例不同的是,負責多播基礎結構的 subject 能夠被重新訂閱,所以源 observable 可以產生新的訂閱。b
所收到的 next
通知便是重新訂閱的證據: 該通知包含數值0,因為重新訂閱已經開啟了全新的 interval
序列。
如果使用 publishBehavior(-1)
來代替 publish()
的話,情況會有所不同,輸出如下所示:
observer a: -1
source: subscribing
source: 0
observer a: 0
source: unsubscribed
observer b: 0
source: subscribing
source: 0
observer b: 0
source: 1
observer b: 1
...
複製程式碼
輸出是類似的,可以清楚地看到重新訂閱開啟了全新的 interval
序列。但是,在收到 interval
的 next
通知前,a
還收到了包含 BehaviorSubject
初始值-1的 next
通知,b
會收到包含 BehaviorSubject
當前值0的 next
通知。
如果使用 publishReplay(1)
來代替 publish()
的話,情況又會有所不同,輸出如下所示:
source: subscribing
source: 0
observer a: 0
source: unsubscribed
observer b: 0
source: subscribing
source: 0
observer b: 0
source: 1
observer b: 1
...
複製程式碼
輸出也是類似的,可以清楚地看到重新訂閱開啟了全新的 interval
序列。但是,b
在收到源 observable 的第一個 next
通知之前會收到重放的 next
通知。
綜上所述,根據示例我們可以發現,當對未完成的源 observable 使用 refCount
時,publish
、publishBehavior
和 publishReplay
的行為都如預期一般,沒有讓人出乎意料之處。
shareReplay 的作用是什麼?
在 RxJS 5.4.0 版本中引入了 shareReplay 操作符。它與 publishReplay().refCount()
十分相似,只是有一個細微的差別。
與 share
類似, shareReplay
傳給 multicast
操作符的也是 subject 的工廠函式。這意味著當重新訂閱源 observable 時,會使用工廠函式來建立出一個新的 subject 。但是,只有當前一個被訂閱 subject 未完成的情況下,工廠函式才會返回新的 subject 。
publishReplay
傳給 multicast
操作符的是 ReplaySubject
例項,而不是工廠函式,這是影響行為不同的原因。
對呼叫了 publishReplay().refCount()
的 observable 進行重新訂閱,subject 會一直重放它的可重放通知。但是,對呼叫了 shareReplay()
的 observable 進行重新訂閱,行為未必如前者一樣,如果 subject 還未完成,會建立一個新的 subject 。所以區別在於,使用呼叫了 shareReplay()
的 observable 的話,當引用計數歸零時,如果 subject 還未完成的話,可重放的通知會被沖洗掉。
不完全使用準則
根據我們看過的這些示例,可以歸納出如下使用準則:
refCount
可以與publish
及其變種一起使用,從而自動地取消源 observable 的訂閱。- 當使用
refCount
來自動取消已完成的源 observable 的訂閱時,publishReplay
和publishLast
的行為會如預期一樣,但是,對於後來的訂閱,publish
和publishBehavior
的行為並沒太大幫助,所以你應該只使用publish
和publishBehavior
來自動取消訂閱。 - 當使用
refCount
來自動取消未完成的源 observable 的訂閱時,publish
、publishBehavior
和publishRelay
的行為都會如預期一樣。 shareReplay()
的行為類似於publishReplay().refCount()
,在對兩者進行選擇時,應該根據在對源 observable 進行重新訂閱時,你是否想要衝洗掉可重放的通知。
上面所描述的 shareReplay
的行為只適用於 RxJS 5.5 之前的版本。在 5.5.0 beta 中,shareReplay
做出了變更: 當引用計數歸零時,操作符不再取消源 observable 的訂閱。
這項變化立即使得引用計數變得多餘,因為只有當源 observable 完成或報錯時,源 observable 的訂閱才會取消訂閱。這項變化也意味著只有在處理錯誤時,shareReplay
和 publishReplay().refCount()
才有所不同:
- 如果源 observable 報錯,
publishReplay().refCount()
返回的 observable 的任何後來訂閱者都將收到錯誤。 - 但是,
shareReplay
返回的 observable 的任何後來訂閱者都將產生一個源 observable 的新訂閱。
===================================結尾分界線=================================
這是多播三連的最後一篇,也應該是年前更新的最後一篇,在這裡提前祝大家春節快樂,闔家歡樂,18年開開心心學 Rx 。
順便預告下,年後回來我們會來兩篇實戰型的文章。