[譯] RxJS: 如何使用 refCount

SangKa發表於2018-02-06

原文連結: blog.angularindepth.com/rxjs-how-to…

本文為 RxJS 中文社群 翻譯文章,如需轉載,請註明出處,謝謝合作!

如果你也想和我們一起,翻譯更多優質的 RxJS 文章以奉獻給大家,請點選【這裡】

在我的上篇文章 理解 publish 和 share 操作符中,只是簡單介紹了 refCount 方法。在這篇文章中我們將深入介紹。

refCount 的作用是什麼?

簡單回顧一下, RxJS 多播的基本心智模型包括: 一個源 observable,一個訂閱源 observable 的 subject 和多個訂閱 subject 的觀察者。multicast 操作符封裝了基於 subject 的基礎結構並返回擁有 connectrefCount 方法的 ConnectableObservable

顧名思義,refCount 返回的 observable 維護訂閱者的引用計數。

當觀察者訂閱有引用計數的 observable 時,引用計數會增加,如果上一個引用計數為零的話,負責多播基礎結構的 subject 會訂閱源 observable 。而當觀察者取消訂閱時,引用計數則會減少,如果引用計數歸零的話,subject 會取消源 observable 的訂閱。

這種引用計數的行為有兩種用途:

  • 當所有觀察者都取消訂閱後,自動取消 subject 對源 observable 的訂閱
  • 當所有觀察者都取消訂閱後,自動取消 subject 對源 observable 的訂閱,然後當再有觀察者訂閱該引用計數的 observable 時,subject 重新訂閱源 observable

我們來詳細介紹每一種情況,然後建立一些使用 refCount 的通用指南。

使用 refCount 自動取消訂閱

publish 操作符返回 ConnectableObservable 。呼叫 ConnectableObservableconnect 方法時,負責多播基礎結構的 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 作為源。它會等待指定的毫秒數後發出 nextcomplete 通知。還有兩個觀察者: a 在源 observable 完成後訂閱,在源 observable 完成後取消訂閱;ba 取消訂閱後訂閱。

示例的輸出如下:

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 通知發給它的訂閱者,所以 ab 都收到的是 nextcomplete 通知。

綜上所述,根據示例我們可以發現 publish 以及它的變種:

  • 當源 observable 完成時,負責多播基礎結構的 subject 也會完成,而且這會阻止對源 observable 的重新訂閱。
  • publishpublishBehaviorrefCount 一起使用時,後來的訂閱者只會收到 complete 通知,這似乎並不是我們想要的效果。
  • publishReplaypublishLastrefCount 一起使用時,後來的訂閱者會收到預期的通知。

重新訂閱未完成的 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 序列。但是,在收到 intervalnext 通知前,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 時,publishpublishBehaviorpublishReplay 的行為都如預期一般,沒有讓人出乎意料之處。

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 的訂閱時,publishReplaypublishLast 的行為會如預期一樣,但是,對於後來的訂閱,publishpublishBehavior 的行為並沒太大幫助,所以你應該只使用 publishpublishBehavior 來自動取消訂閱。
  • 當使用 refCount 來自動取消未完成的源 observable 的訂閱時,publishpublishBehaviorpublishRelay 的行為都會如預期一樣。
  • shareReplay() 的行為類似於 publishReplay().refCount(),在對兩者進行選擇時,應該根據在對源 observable 進行重新訂閱時,你是否想要衝洗掉可重放的通知。

上面所描述的 shareReplay 的行為只適用於 RxJS 5.5 之前的版本。在 5.5.0 beta 中,shareReplay 做出了變更: 當引用計數歸零時,操作符不再取消源 observable 的訂閱。

這項變化立即使得引用計數變得多餘,因為只有當源 observable 完成或報錯時,源 observable 的訂閱才會取消訂閱。這項變化也意味著只有在處理錯誤時,shareReplaypublishReplay().refCount() 才有所不同:

  • 如果源 observable 報錯,publishReplay().refCount() 返回的 observable 的任何後來訂閱者都將收到錯誤。
  • 但是,shareReplay 返回的 observable 的任何後來訂閱者都將產生一個源 observable 的新訂閱。

===================================結尾分界線=================================

這是多播三連的最後一篇,也應該是年前更新的最後一篇,在這裡提前祝大家春節快樂,闔家歡樂,18年開開心心學 Rx 。

順便預告下,年後回來我們會來兩篇實戰型的文章。

相關文章