原文連結: blog.angularindepth.com/rxjs-unders…
本文為 RxJS 中文社群 翻譯文章,如需轉載,請註明出處,謝謝合作!
如果你也想和我們一起,翻譯更多優質的 RxJS 文章以奉獻給大家,請點選【這裡】
照片取自 Unsplash,作者 Kimberly Farmer 。
我經常會被問及 publish
操作符的相關問題:
publish 和 share 之間的區別是什麼?
如何匯入 refCount 操作符?
何時使用 AsyncSubject?
我們來解答這些問題,並讓你瞭解到更多內容,首先從基礎入手。
多播的心智模型
多播是一個術語,它用來描述由單個 observable 發出的每個通知會被多個觀察者所接收的情況。一個 observable 是否具備多播的能力取決於它是熱的還是冷的。
熱的和冷的 observable 的特徵在於 observable 通知的生產者是在哪建立的。在 Ben Lesh 的 熱的 Vs 冷的 Observables 一文中,他詳細討論了兩者間的差異,這些差異可以歸納如下:
- 如果通知的生產者是觀察者訂閱 observable 時建立的,那麼 observable 就是冷的。例如,
timer
observable 就是冷的,每次訂閱時都會建立一個新的定時器。 - 如果通知的生產者不是每次觀察者訂閱 observable 時建立的,那麼 observable 就是熱的。例如,使用
fromEvent
建立的 observable 就是熱的,產生事件的元素存在於 DOM 之中,它不是觀察者訂閱時所建立的。
冷的 observables 是單播的,每個觀察者所接收到的通知都是來自不同的生產者,生產者是觀察者訂閱時所建立的。
熱的 observables 是多播的,每個觀察者所接收到的通知都是來自同一個生產者。
有些時候,需要冷的 observable 具有多播的行為,RxJS 引入了 Subject
類使之成為可能。
Subject 即是 observable,又是 observer (觀察者)。通過使用觀察者來訂閱 subject,然後 subject 再訂閱冷的 observable,可以讓冷的 observable 變成熱的。這是 RxJS 引入 subjects 的主要用途,在 Ben Lesh 的 關於 RxJS 中的 Subject 一文中,他指出:
多播是 RxJS 中 Subjects 的主要用法。
我們來看下面的示例:
import { Observable } from "rxjs/Observable";
import { Subject } from "rxjs/Subject";
import "rxjs/add/observable/defer";
import "rxjs/add/observable/of";
const source = Observable.defer(() => Observable.of(
Math.floor(Math.random() * 100)
));
function observer(name: string) {
return {
next: (value: number) => console.log(`observer ${name}: ${value}`),
complete: () => console.log(`observer ${name}: complete`)
};
}
const subject = new Subject<number>();
subject.subscribe(observer("a"));
subject.subscribe(observer("b"));
source.subscribe(subject);
複製程式碼
示例中的 source
是冷的。每次觀察者訂閱 source
時,傳給 defer
的工廠函式會建立一個發出隨機數後完成的 observable 。
要讓 source
變成多播的,需要觀察者訂閱 subject,然後 subject 再訂閱 source
。source
只會看到一個訂閱 ( subscription ),它也只生成一個包含隨機數的 next
通知和一個 complete
通知。Subject 會將這些通知傳送給它的觀察者,輸出如下所示:
observer a: 42
observer b: 42
observer a: complete
observer b: complete
複製程式碼
此示例可以作為 RxJS 多播的基本心智模型: 一個源 observable,一個訂閱源 observable 的 subject 和多個訂閱 subject 的觀察者。
multicast 操作符和 ConnectableObservable
RxJS 引入了 multicast
操作符,它可以應用於 observable ,使其變成熱的。此操作符封裝了 subject 用於多播 observable 時所涉及的基礎結構。
在看 multicast
操作符之前,我們使用一個簡單實現的 multicast
函式來替代上面示例中的 subject :
function multicast<T>(source: Observable<T>) {
const subject = new Subject<T>();
source.subscribe(subject);
return subject;
}
const m = multicast(source);
m.subscribe(observer("a"));
m.subscribe(observer("b"));
複製程式碼
程式碼改變後,示例的輸出如下:
observer a: complete
observer b: complete
複製程式碼
這並不是我們想要的結果。在函式內部訂閱 subject 使得 subject 在被觀察者訂閱之前就已經收到了 next
和 complete
通知,所以觀察者只能收到 complete
通知。
這是可避免的,任何連線多播基礎結構的函式的呼叫者需要能夠在 subject 訂閱源 observable 時進行控制。RxJS 的 multicast
操作符通過返回一個特殊的 observable 型別 ConnectableObservable 來實現的。
ConnectableObservable 封裝了多播的基礎結構,但它不會立即訂閱源 observable ,只有當它的 connect
方法呼叫時,它才會訂閱源 observable 。
我們來使用 multicast
操作符:
import { Observable } from "rxjs/Observable";
import { Subject } from "rxjs/Subject";
import "rxjs/add/observable/defer";
import "rxjs/add/observable/of";
import "rxjs/add/operator/multicast";
const source = Observable.defer(() => Observable.of(
Math.floor(Math.random() * 100)
));
function observer(name: string) {
return {
next: (value: number) => console.log(`observer ${name}: ${value}`),
complete: () => console.log(`observer ${name}: complete`)
};
}
const m = source.multicast(new Subject<number>());
m.subscribe(observer("a"));
m.subscribe(observer("b"));
m.connect();
複製程式碼
程式碼改變後,現在觀察者可以收到 next
通知了:
observer a: 54
observer b: 54
observer a: complete
observer b: complete
複製程式碼
呼叫 connect
時,傳入 multicast
操作符的 subject 會訂閱源 observable,而 subject 的觀察者會收到多播通知,這正符合 RxJS 多播的基本心智模型。
ConnectableObservable 還有另外一個方法 refCount
,它可以用來確定源 observable 何時產生了訂閱。
refCount
看上去就像是操作符,也就是說,它是在 observable 上呼叫的方法並且返回另一個 observable,但是它只是 ConnectableObservable
的方法而且不需要匯入。顧名思義,refCount
返回 observable, 它負責維護已產生的訂閱的引用計數。
當觀察者訂閱負責引用計數的 observable 時,引用計數會增加,如果前一個引用計數為0的話,負責多播基礎結構的 subject 會訂閱源 observable 。當觀察者取消訂閱時,引用計數會減少,如果引用計數歸零的話,subject 會取消對源 observable 的訂閱。
我們來使用 refCount
:
const m = source.multicast(new Subject<number>()).refCount();
m.subscribe(observer("a"));
m.subscribe(observer("b"));
複製程式碼
程式碼改變後,輸出如下所示:
observer a: 42
observer a: complete
observer b: complete
複製程式碼
只有第一個觀察者收到了 next
通知。我們來看看原因。
示例中的源 observable 會立即發出通知。也就是說,一旦訂閱了,源 observable 就會發出 next
和 complete
通知,complete
通知導致在第二個觀察者訂閱之前第一個就取消了訂閱。當第一個取消訂閱時,引用計數會歸零,所以負責多播基礎結構的 subject 也會取消源 observable 的訂閱。
當第二個觀察者訂閱時,subject 會再次訂閱源 observable,但由於 subject 已經收到了 complete
通知,所以它無法被重用。
向 multicast
傳入 subject 的工廠函式可以解決此問題:
const m = source.multicast(() => new Subject<number>()).refCount();
m.subscribe(observer("a"));
m.subscribe(observer("b"));
複製程式碼
程式碼改變後,每次源 observable 被訂閱時,都會建立一個新的 subject,輸出如下所示:
observer a: 42
observer a: complete
observer b: 54
observer b: complete
複製程式碼
因為源 observable 會立即發出通知,所以觀察者收到的通知是分開的。將 source
進行修改,以便延遲通知:
import { Observable } from "rxjs/Observable";
import { Subject } from "rxjs/Subject";
import "rxjs/add/observable/defer";
import "rxjs/add/observable/of";
import "rxjs/add/operator/delay";
import "rxjs/add/operator/multicast";
const source = Observable.defer(() => Observable.of(
Math.floor(Math.random() * 100)
)).delay(0);
複製程式碼
觀察者依然會收到多播通知,輸出如下所示:
observer a: 42
observer b: 42
observer a: complete
observer b: complete
複製程式碼
總結一下,上述示例展示了 multicast
操作符的以下特點:
- 封裝了多播的基礎結構以符合多播的心智模型;
- 提供了
connect
方法以用於確定源 observable 何時產生了訂閱; - 提供了
refCount
方法以用於自動管理源 observable 的訂閱; - 如果使用
refCount
,必須傳入Subject
的工廠函式,而不是Subject
例項;
接下來我們來看 publish
和 share
操作符,以及 publish
的變種,看看它們是如何在 multicast
操作符所提供的基礎之上建立的。
publish 操作符
我們通過下面的示例來看看 publish
操作符:
import { Observable } from "rxjs/Observable";
import "rxjs/add/observable/defer";
import "rxjs/add/observable/of";
import "rxjs/add/operator/delay";
import "rxjs/add/operator/publish";
function random() {
return Math.floor(Math.random() * 100);
}
const source = Observable.concat(
Observable.defer(() => Observable.of(random())),
Observable.defer(() => Observable.of(random())).delay(1)
);
function observer(name: string) {
return {
next: (value: number) => console.log(`observer ${name}: ${value}`),
complete: () => console.log(`observer ${name}: complete`)
};
}
const p = source.publish();
p.subscribe(observer("a"));
p.connect();
p.subscribe(observer("b"));
setTimeout(() => p.subscribe(observer("c")), 10);
複製程式碼
示例中的源 observable 會立即發出一個隨機數,經過短暫的延遲後發出另一個隨機數,然後完成。這個示例可以讓我們看到訂閱者在 connect
呼叫前、connect
呼叫後以及呼叫過 publish 的 observable 完成後訂閱分別會發生什麼。
publish
操作符是對 multicast
操作符進行了一層薄薄的封裝。它會呼叫 multicast
並傳入 Subject
。
示例的輸出如下所示:
observer a: 42
observer a: 54
observer b: 54
observer a: complete
observer b: complete
observer c: complete
複製程式碼
觀察者收到的通知可歸納如下:
a
是在connect
呼叫前訂閱的,所以它能收到兩個next
通知和complete
通知。b
是在connect
呼叫後訂閱的,此時第一個立即傳送的next
通知已經發出過了,所以它只能收到第二個next
通知和complete
通知。c
是在源 observable 完成後訂閱的,所以它只能收到complete
通知。
使用 refCount
來代替 connect
:
const p = source.publish().refCount();
p.subscribe(observer("a"));
p.subscribe(observer("b"));
setTimeout(() => p.subscribe(observer("c")), 10);
複製程式碼
示例的輸出如下所示:
observer a: 42
observer a: 54
observer b: 54
observer a: complete
observer b: complete
observer c: complete
複製程式碼
輸出跟使用 connect
時的類似。這是為什麼?
b
沒有收到第一個 next
通知是因為源 observable 的第一個 next
通知是立即發出的,所以只有 a
能收到。
c
是在呼叫過 publish 的 observable 完成後訂閱的,所以訂閱的引用計數已經是0,此時將會再生成一個訂閱。但是,publish
傳給 multicast
的是 subject,而不是工廠函式,因為 subjects 無法被複用,所以 c
只能收到 complete
通知。
publish
和 multicast
操作符都接受一個可選的 selector
函式,如果指定了此函式,操作符的行為將會有很大的不同。這將在另一篇文章 multicast 操作符的祕密中詳細介紹。
特殊型別的 subjects
publish
操作符有幾個變種,它們都以一種類似的方式對 multicast
進行了包裝,傳入的是 subjects,而不是工廠函式。但是,它們傳入的是不同型別的 subjects 。
publish
變種使用的特殊型別的 subjects 包括:
BehaviorSubject
ReplaySubject
AsyncSubject
關於如何使用這些特殊型別的 subjects 的答案是: 每個變種都與一個特殊型別的 subject 相關聯,當你需要的行為類似於某個 publish
變種時,就使用相對應的 subject 。我們來看看這些變種的行為是怎樣的。
publishBehavior 操作符
publishBehavior
傳給 multicast
的是 BehaviorSubject
,而不是 Subject
。BehaviorSubject
類似於 Subject
,但如果 subject 的訂閱發生在源 observable 發出 next
通知之前,那麼 subject 會發出包含初始值的 next
通知。
我們更改下示例,給生成隨機數的源 observable 加上短暫的延遲,這樣它就不會立即發出隨機數:
const delayed = Observable.timer(1).switchMapTo(source);
const p = delayed.publishBehavior(-1);
p.subscribe(observer("a"));
p.connect();
p.subscribe(observer("b"));
setTimeout(() => p.subscribe(observer("c")), 10);
複製程式碼
示例的輸出如下所示:
observer a: -1
observer b: -1
observer a: 42
observer b: 42
observer a: 54
observer b: 54
observer a: complete
observer b: complete
observer c: complete
複製程式碼
觀察者收到的通知可歸納如下:
a
是在connect
呼叫前訂閱的,所以它能收到帶有 subject 的初始值的next
通知、源 observable 的兩個next
通知和complete
通知。b
是在connect
呼叫後但在 subject 收到源 observable 的第一個next
通知前訂閱的,所以它能收到帶有 subject 的初始值的next
通知、源 observable 的兩個next
通知和complete
通知。c
是在源 observable 完成後訂閱的,所以它只能收到complete
通知。
publishReplay 操作符
publishReplay
傳給 multicast
的是 ReplaySubject
,而不是 Subject
。顧名思義,每當觀察者訂閱時,ReplaySubject
會重放指定數量的 next
通知。
const p = source.publishReplay(1);
p.subscribe(observer("a"));
p.connect();
p.subscribe(observer("b"));
setTimeout(() => p.subscribe(observer("c")), 10);
複製程式碼
使用了 publishReplay
,示例的輸出如下所示:
observer a: 42
observer b: 42
observer a: 54
observer b: 54
observer a: complete
observer b: complete
observer c: 54
observer c: complete
複製程式碼
觀察者收到的通知可歸納如下:
a
是在connect
呼叫前訂閱的,此時 subject 還沒有收到next
通知,所以a
能收到源 observable 的兩個next
通知和complete
通知。b
是在connect
呼叫後訂閱的,此時 subject 已經收到了源 observable 的第一個next
通知,所以b
能收到重放的next
通知、源 observable 的第二個next
通知和complete
通知。c
是在源 observable 完成後訂閱的,所以它能收到重放的next
通知和complete
通知。
來看看 c
的行為,很明顯,不同於 publish
操作符,publishReplay
操作符適合使用 refCount
方法,因為觀察者在源 observable 完成後訂閱依然能收到任意數量的重放的 next
通知。
publishLast 操作符
publishLast
傳給 multicast
的是 AsyncSubject
,而不是 Subject
。AsyncSubject
是最特別的特殊型別 subjects 。只有當它完成時,才會發出 next
通知 (如果有 next
通知的話) 和 complete
通知,這個 next
通知是源 observable 中的最後一個 next
通知。
const p = source.publishLast();
p.subscribe(observer("a"));
p.connect();
p.subscribe(observer("b"));
setTimeout(() => p.subscribe(observer("c")), 10);
複製程式碼
使用了 publishLast
,示例的輸出如下所示:
observer a: 54
observer b: 54
observer a: complete
observer b: complete
observer c: 54
observer c: complete
複製程式碼
觀察者收到的通知可歸納如下:
a
和b
都是在源 observable 完成前訂閱的,但直到源 observable 完成它們才能收到通知,它們能收到帶有第二個隨機數的next
通知和complete
通知。c
是在源 observable 完成後訂閱的,它能收到帶有第二個隨機數的next
通知和complete
通知。
與 publishReplay
類似,publishLast
操作符適合使用 refCount
方法,因為觀察者在源 observable 完成後訂閱依然能收到任意數量的重放的 next
通知。
share 操作符
share
操作符類似於使用 publish().refCount()
。但是,share
傳給 multicast
的是工廠函式,這意味著在引用計數為0之後發生訂閱的話,會建立一個新的 Subject
來訂閱源 observable 。
const s = source.share();
s.subscribe(observer("a"));
s.subscribe(observer("b"));
setTimeout(() => s.subscribe(observer("c")), 10);
複製程式碼
使用了 share
,示例的輸出如下所示:
observer a: 42
observer a: 54
observer b: 54
observer a: complete
observer b: complete
observer c: 6
observer c: 9
observer c: complete
複製程式碼
觀察者收到的通知可歸納如下:
a
訂閱後立即收到第一個next
通知,隨後是第二個next
通知和complete
通知。b
只能收到第二個next
通知和complete
通知。c
是在源 observable 完成後訂閱的,會建立一個新的 subject 來訂閱源 observable,它會立即收到第一個next
通知,隨後是第二個next
通知和complete
通知。
在上面這些示例中,我們介紹了 publish
和 share
操作符,當源 observable 完成時,a
和 b
會自動取消訂閱。如果源 observable 報錯,它們也同樣會自動取消訂閱。publish
和 share
操作符還有另外一個不同點:
- 如果源 observable 報錯,由
publish
返回的 observable 的任何將來的訂閱者都將收到error
通知。 - 但是,由
share
返回的 observable 的任何將來的訂閱者會生成源 observable 的一個新訂閱,因為錯誤會自動取消任何訂閱者的訂閱,將其引用計數歸零。
就這樣了,本文到此結束。我們介紹了六個操作符,但它們全是通過一種類似的方式來實現的,它們全都符合同一個基本的心智模型: 一個源 observable、一個訂閱源 observable 的 subject 和多個訂閱 subject 的觀察者。
本文只是簡略地介紹了 refCount
方法。想要深入瞭解,請參見 RxJS: 如何使用 refCount。