iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 24
6
Modern Web

30 天精通 RxJS系列 第 24

30 天精通 RxJS(23): Subject, BehaviorSubject, ReplaySubject, AsyncSubject

30 天精通 RxJS(23): Subject, BehaviorSubject, ReplaySubject, AsyncSubject

昨天我們介紹了 Subject 是什麼,今天要講 Subject 一些應用方式,以及 Subject 的另外三種變形。

Subject

昨天我們講到了 Subject 實際上就是 Observer Pattern 的實作,他會在內部管理一份 observer 的清單,並在接收到值時遍歷這份清單並送出值,所以我們可以這樣用 Subject

var subject = new Rx.Subject();

var observerA = {
    next: value => console.log('A next: ' + value),
    error: error => console.log('A error: ' + error),
    complete: () => console.log('A complete!')
}

var observerB = {
    next: value => console.log('B next: ' + value),
    error: error => console.log('B error: ' + error),
    complete: () => console.log('B complete!')
}

subject.subscribe(observerA);
subject.subscribe(observerB);

subject.next(1);
// "A next: 1"
// "B next: 1"
subject.next(2);
// "A next: 2"
// "B next: 2"

JSBin

這裡我們可以直接用 subject 的 next 方法傳送值,所有訂閱的 observer 就會接收到,又因為 Subject 本身是 Observable,所以這樣的使用方式很適合用在某些無法直接使用 Observable 的前端框架中,例如在 React 想對 DOM 的事件做監聽

class MyButton extends React.Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 };
        this.subject = new Rx.Subject();
        
        this.subject
            .mapTo(1)
            .scan((origin, next) => origin + next)
            .subscribe(x => {
                this.setState({ count: x })
            })
    }
    render() {
        return <button onClick={event => this.subject.next(event)}>{this.state.count}</button>
    }
}

JSBin | JSFiddle

從上面的程式碼可以看出來,因為 React 本身 API 的關係,如果我們想要用 React 自訂的事件,我們沒辦法直接使用 Observable 的 creation operator 建立 observable,這時就可以靠 Subject 來做到這件事。

Subject 因為同時是 observer 和 observable,所以應用面很廣除了前面所提的之外,還有上一篇文章講的組播(multicase)特性也會在接下來的文章做更多應用的介紹,這裡先讓我們來看看 Subject 的三個變形。

BehaviorSubject

很多時候我們會希望 Subject 能代表當下的狀態,而不是單存的事件發送,也就是說如果今天有一個新的訂閱,我們希望 Subject 能立即給出最新的值,而不是沒有回應,例如下面這個例子

var subject = new Rx.Subject();

var observerA = {
    next: value => console.log('A next: ' + value),
    error: error => console.log('A error: ' + error),
    complete: () => console.log('A complete!')
}

var observerB = {
    next: value => console.log('B next: ' + value),
    error: error => console.log('B error: ' + error),
    complete: () => console.log('B complete!')
}

subject.subscribe(observerA);

subject.next(1);
// "A next: 1"
subject.next(2);
// "A next: 2"
subject.next(3);
// "A next: 3"

setTimeout(() => {
    subject.subscribe(observerB); // 3 秒後才訂閱,observerB 不會收到任何值。
},3000)

以上面這個例子來說,observerB 訂閱的之後,是不會有任何元素送給 observerB 的,因為在這之後沒有執行任何 subject.next(),但很多時候我們會希望 subject 能夠表達當前的狀態,在一訂閱時就能收到最新的狀態是什麼,而不是訂閱後要等到有變動才能接收到新的狀態,以這個例子來說,我們希望 observerB 訂閱時就能立即收到 3,希望做到這樣的效果就可以用 BehaviorSubject。

BehaviorSubject 跟 Subject 最大的不同就是 BehaviorSubject 是用來呈現當前的值,而不是單純的發送事件。BehaviorSubject 會記住最新一次發送的元素,並把該元素當作目前的值,在使用上 BehaviorSubject 建構式需要傳入一個參數來代表起始的狀態,範例如下

var subject = new Rx.BehaviorSubject(0); // 0 為起始值
var observerA = {
    next: value => console.log('A next: ' + value),
    error: error => console.log('A error: ' + error),
    complete: () => console.log('A complete!')
}

var observerB = {
    next: value => console.log('B next: ' + value),
    error: error => console.log('B error: ' + error),
    complete: () => console.log('B complete!')
}

subject.subscribe(observerA);
// "A next: 0"
subject.next(1);
// "A next: 1"
subject.next(2);
// "A next: 2"
subject.next(3);
// "A next: 3"

setTimeout(() => {
    subject.subscribe(observerB); 
    // "B next: 3"
},3000)

JSBin | JSFiddle

從上面這個範例可以看得出來 BehaviorSubject 在建立時就需要給定一個狀態,並在之後任何一次訂閱,就會先送出最新的狀態。其實這種行為就是一種狀態的表達而非單存的事件,就像是年齡跟生日一樣,年齡是一種狀態而生日就是事件;所以當我們想要用一個 stream 來表達年齡時,就應該用 BehaviorSubject。

ReplaySubject

在某些時候我們會希望 Subject 代表事件,但又能在新訂閱時重新發送最後的幾個元素,這時我們就可以用 ReplaySubject,範例如下

var subject = new Rx.ReplaySubject(2); // 重複發送最後 2 個元素
var observerA = {
    next: value => console.log('A next: ' + value),
    error: error => console.log('A error: ' + error),
    complete: () => console.log('A complete!')
}

var observerB = {
    next: value => console.log('B next: ' + value),
    error: error => console.log('B error: ' + error),
    complete: () => console.log('B complete!')
}

subject.subscribe(observerA);
subject.next(1);
// "A next: 1"
subject.next(2);
// "A next: 2"
subject.next(3);
// "A next: 3"

setTimeout(() => {
    subject.subscribe(observerB);
    // "B next: 2"
    // "B next: 3"
},3000)

JSBin |

可能會有人以為 ReplaySubject(1) 是不是就等同於 BehaviorSubject,其實是不一樣的,BehaviorSubject 在建立時就會有起始值,比如 BehaviorSubject(0) 起始值就是 0,BehaviorSubject 是代表著狀態而 ReplaySubject 只是事件的重放而已。

AsyncSubject

AsyncSubject 是最怪的一個變形,他有點像是 operator last,會在 subject 結束後送出最後一個值,範例如下

var subject = new Rx.AsyncSubject();
var observerA = {
    next: value => console.log('A next: ' + value),
    error: error => console.log('A error: ' + error),
    complete: () => console.log('A complete!')
}

var observerB = {
    next: value => console.log('B next: ' + value),
    error: error => console.log('B error: ' + error),
    complete: () => console.log('B complete!')
}

subject.subscribe(observerA);
subject.next(1);
subject.next(2);
subject.next(3);
subject.complete();
// "A next: 3"
// "A complete!"

setTimeout(() => {
    subject.subscribe(observerB);
    // "B next: 3"
    // "B complete!"
},3000)

JSBin |

從上面的程式碼可以看出來,AsyncSubject 會在 subject 結束後才送出最後一個值,其實這個行為跟 Promise 很像,都是等到事情結束後送出一個值,但實務上我們非常非常少用到 AsyncSubject,絕大部分的時候都是使用 BehaviorSubject 跟 ReplaySubject 或 Subject。

我們把 AsyncSubject 放在大腦的深處就好

今日小結

今天介紹了 Subject 的一些應用方式,以及 BehaviorSubject, ReplaySubject, AsyncSubject 三個變形各自的特性介紹,不知道讀者麼是否有收穫呢? 如果有任何問題,歡迎在下方留言給我!


上一篇
30 天精通 RxJS(22): 什麼是 Subject?
下一篇
30 天精通 RxJS(24): Observable operators - multicast, refCount, publish, share
系列文
30 天精通 RxJS30

2 則留言

2
l7960261
iT邦新手 5 級 ‧ 2017-01-15 09:47:42

BehaviorSubject 建構式需要傳入一個參數來代表起始的狀態

在訂閱當下的 observerA 馬上收到你設定的起始值, 有點困惑這樣的設計用意.

那個數值我們範例是寫死的,但實際上可以是從後端抓的

想像一下假如我們正在做一個股票相關的網站,一開始頁面是空的,使用者可以從右側的選單中加入自己關注的股票。當他點擊某支股票後,我們的程式就會去發 HTTP Request 取得這隻股票的資料,再利用這些資料來建立 webSocket 的連線。

webSocket 連線會持續的接收新的價格,並更新在畫面上,但因為畫面顯示可能一開始是圖表,但可以在選擇加上單純的數字。

所以說圖表顯示跟數字顯示其實是兩個訂閱,共用同一個資料源,也就是說這裡會用到 subject,那哪個 subject 比較適合呢?

這時其實就很適合用 BehaviorSubject 在一開始顯示圖表時,可能 server 還沒有送資料,但我們可以先顯示一開始 HTTP Request 抓到的昨日收盤價,這樣能讓圖表在開始時至少會有一筆資料。

這就是 BehaviorSubject 起始狀態的用處:)

Good! 這樣一講就更清楚明瞭了!

1
kingc
iT邦新手 5 級 ‧ 2017-01-15 21:50:38

很有收获,谢谢你的分享.目前在跟Angular 2纠缠,正好需要掌握相关内容.

我要留言

立即登入留言