RxJS 系列之四 - Subject 詳解

semlinker發表於2017-03-30

RxJS 系列目錄

Observer Pattern

觀察者模式定義

觀察者模式又叫釋出訂閱模式(Publish/Subscribe),它定義了一種一對多的關係,讓多個觀察者物件同時監聽某一個主題物件,這個主題物件的狀態發生變化時就會通知所有的觀察者物件,使得它們能夠自動更新自己。

我們可以使用日常生活中,期刊訂閱的例子來形象地解釋一下上面的概念。期刊訂閱包含兩個主要的角色:期刊出版方和訂閱者,他們之間的關係如下:

  • 期刊出版方 - 負責期刊的出版和發行工作
  • 訂閱者 - 只需執行訂閱操作,新版的期刊釋出後,就會主動收到通知,如果取消訂閱,以後就不會再收到通知

在觀察者模式中也有兩個主要角色:Subject (主題) 和 Observer (觀察者) 。它們分別對應例子中的期刊出版方和訂閱者。接下來我們來看張圖,從而加深對上面概念的理解。

RxJS 系列之四 - Subject 詳解

觀察者模式結構

RxJS 系列之四 - Subject 詳解

觀察者模式實戰

Subject 類定義

class Subject {

    constructor() {
        this.observerCollection = [];
    }

    addObserver(observer) { // 新增觀察者
        this.observerCollection.push(observer);
    }

    deleteObserver(observer) { // 移除觀察者
        let index = this.observerCollection.indexOf(observer);
        if(index >= 0) this.observerCollection.splice(index, 1);
    }

    notifyObservers() { // 通知觀察者
        this.observerCollection.forEach((observer)=>observer.notify());
    }
}複製程式碼

Observer 類定義

class Observer {
    constructor(name) {
        this.name = name;
    }

    notify() {
        console.log(`${this.name} has been notified.`);
    }
}複製程式碼

使用示例

let subject = new Subject(); // 建立主題物件

let observer1 = new Observer('semlinker'); // 建立觀察者A - 'semlinker'
let observer2 = new Observer('lolo'); // 建立觀察者B - 'lolo'

subject.addObserver(observer1); // 註冊觀察者A
subject.addObserver(observer2); // 註冊觀察者B

subject.notifyObservers(); // 通知觀察者

subject.deleteObserver(observer1); // 移除觀察者A

subject.notifyObservers(); // 驗證是否成功移除複製程式碼

以上程式碼成功執行後控制檯的輸出結果:

semlinker has been notified.
lolo has been notified.
lolo has been notified.複製程式碼

Observable subscribe

在介紹 RxJS - Subject 之前,我們先來看個示例:

const interval$ = Rx.Observable.interval(1000).take(3);

interval$.subscribe({
  next: value => console.log('Observer A get value: ' + value);
});

setTimeout(() => {
  interval$.subscribe({
      next: value => console.log('Observer B get value: ' + value);
  });
}, 1000);複製程式碼

以上程式碼執行後,控制檯的輸出結果:

Observer A get value: 0
Observer A get value: 1
Observer B get value: 0
Observer A get value: 2
Observer B get value: 1
Observer B get value: 2複製程式碼

通過以上示例,我們可以得出以下結論:

  • Observable 物件可以被重複訂閱
  • Observable 物件每次被訂閱後,都會重新執行

上面的示例,我們可以簡單地認為兩次呼叫普通的函式,具體參考以下程式碼:

function interval() {
  setInterval(() => console.log('..'), 1000);
}

interval();

setTimeout(() => {
  interval();
}, 1000);複製程式碼

Observable 物件的預設行為,適用於大部分場景。但有些時候,我們會希望在第二次訂閱的時候,不會從頭開始接收 Observable 發出的值,而是從第一次訂閱當前正在處理的值開始傳送,我們把這種處理方式成為組播 (multicast),那我們要怎麼實現呢 ?回想一下我們剛才介紹過觀察者模式,你腦海中是不是已經想到方案了。沒錯,我們可以通過自定義 Subject 來實現上述功能。

自定義 Subject

Subject 類定義

class Subject {   
    constructor() {
        this.observers = [];
    }

    addObserver(observer) { 
        this.observers.push(observer);
    }

    next(value) {  
        this.observers.forEach(o => o.next(value));    
    }

    error(error){ 
        this.observers.forEach(o => o.error(error));
    }

    complete() {
        this.observers.forEach(o => o.complete());
    }
}複製程式碼

使用示例

const interval$ = Rx.Observable.interval(1000).take(3);
let subject = new Subject();

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

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

subject.addObserver(observerA); // 新增觀察者A
interval$.subscribe(subject); // 訂閱interval$物件
setTimeout(() => {
   subject.addObserver(observerB); // 新增觀察者B
}, 1000);複製程式碼

以上程式碼執行後,控制檯的輸出結果:

Observer A get value: 0
Observer A get value: 1
Observer B get value: 1
Observer A get value: 2
Observer B get value: 2
Observer A complete!
Observer B complete!複製程式碼

通過自定義 Subject,我們實現了前面提到的功能。接下來我們進入正題 - RxJS Subject。

RxJS Subject

首先我們通過 RxJS Subject 來重寫一下上面的示例:

const interval$ = Rx.Observable.interval(1000).take(3);
let subject = new Rx.Subject();

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

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

subject.subscribe(observerA); // 新增觀察者A
interval$.subscribe(subject); // 訂閱interval$物件
setTimeout(() => {
   subject.subscribe(observerB); // 新增觀察者B
}, 1000);複製程式碼

RxJS Subject 原始碼片段

/**
 * Suject繼承於Observable 
 */
export class Subject extends Observable {
    constructor() {
        super();
        this.observers = []; // 觀察者列表
        this.closed = false;
        this.isStopped = false;
        this.hasError = false;
        this.thrownError = null;
    }

    next(value) {
        if (this.closed) {
            throw new ObjectUnsubscribedError();
        }
        if (!this.isStopped) {
            const { observers } = this;
            const len = observers.length;
            const copy = observers.slice();
            for (let i = 0; i < len; i++) { // 迴圈呼叫觀察者next方法,通知觀察者
                copy[i].next(value);
            }
        }
    }

    error(err) {
        if (this.closed) {
            throw new ObjectUnsubscribedError();
        }
        this.hasError = true;
        this.thrownError = err;
        this.isStopped = true;
        const { observers } = this;
        const len = observers.length;
        const copy = observers.slice();
        for (let i = 0; i < len; i++) { // 迴圈呼叫觀察者error方法
            copy[i].error(err);
        }
        this.observers.length = 0;
    }

    complete() {
        if (this.closed) {
            throw new ObjectUnsubscribedError();
        }
        this.isStopped = true;
        const { observers } = this;
        const len = observers.length;
        const copy = observers.slice();
        for (let i = 0; i < len; i++) { // 迴圈呼叫觀察者complete方法
            copy[i].complete();
        }
        this.observers.length = 0; // 清空內部觀察者列表
    }
}複製程式碼

通過 RxJS Subject 示例和原始碼片段,對於 Subject 我們可以得出以下結論:

  • Subject 既是 Observable 物件,又是 Observer 物件
  • 當有新訊息時,Subject 會對內部的 observers 列表進行組播 (multicast)

Angular 2 RxJS Subject 應用

在 Angular 2 中,我們可以利用 RxJS Subject 來實現元件通訊,具體示例如下:

message.service.ts

import { Injectable } from '@angular/core';
import {Observable} from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class MessageService {
    private subject = new Subject<any>();

    sendMessage(message: string) {
        this.subject.next({ text: message });
    }

    clearMessage() {
        this.subject.next();
    }

    getMessage(): Observable<any> {
        return this.subject.asObservable();
    }
}複製程式碼

home.component.ts

import { Component } from '@angular/core';

import { MessageService } from '../_services/index';

@Component({
    moduleId: module.id,
    templateUrl: 'home.component.html'
})

export class HomeComponent {
    constructor(private messageService: MessageService) {}

    sendMessage(): void { // 傳送訊息
        this.messageService.sendMessage('Message from Home Component to App Component!');
    }

    clearMessage(): void { // 清除訊息
        this.messageService.clearMessage();
    }
}複製程式碼

app.component.ts

import { Component, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';

import { MessageService } from './_services/index';

@Component({
    moduleId: module.id,
    selector: 'app',
    templateUrl: 'app.component.html'
})

export class AppComponent implements OnDestroy {
    message: any;
    subscription: Subscription;

    constructor(private messageService: MessageService) {
        this.subscription = this.messageService.getMessage()
              .subscribe(message => { this.message = message; });
    }

    ngOnDestroy() {
        this.subscription.unsubscribe();
    }
}複製程式碼

以上示例實現的功能是元件之間訊息通訊,即 HomeComponent 子元件,向 AppComponent 父元件傳送訊息。程式碼執行後,瀏覽器的顯示結果如下:

RxJS 系列之四 - Subject 詳解

Plunker 示例

Subject 存在的問題

因為 Subject 在訂閱時,是把 observer 存放到觀察者列表中,並在接收到新值的時候,遍歷觀察者列表並呼叫觀察者上的 next 方法,具體如下:

next(value) {
        if (this.closed) {
            throw new ObjectUnsubscribedError();
        }
        if (!this.isStopped) {
            const { observers } = this;
            const len = observers.length;
            const copy = observers.slice();
            for (let i = 0; i < len; i++) { // 迴圈呼叫觀察者next方法,通知觀察者
                copy[i].next(value);
            }
        }
}複製程式碼

這樣會有一個大問題,如果某個 observer 在執行時出現異常,卻沒進行異常處理,就會影響到其它的訂閱者,具體示例如下:

const source = Rx.Observable.interval(1000);
const subject = new Rx.Subject();

const example = subject.map(x => {
    if (x === 1) {
        throw new Error('oops');
    }
    return x;
});
subject.subscribe(x => console.log('A', x));
example.subscribe(x => console.log('B', x));
subject.subscribe(x => console.log('C', x));

source.subscribe(subject);複製程式碼

以上程式碼執行後,控制檯的輸出結果:

A 0
B 0
C 0
A 1
Rx.min.js:74 Uncaught Error: oops複製程式碼

JSBin - Subject Problem Demo

在程式碼執行前,大家會認為觀察者B 會在接收到 1 值時丟擲異常,觀察者 A 和 C 仍會正常執行。但實際上,在當前的 RxJS 版本中若觀察者 B 報錯,觀察者 A 和 C 也會停止執行。那麼應該如何解決這個問題呢?目前最簡單的方式就是為所有的觀察者新增異常處理,更新後的程式碼如下:

const source = Rx.Observable.interval(1000);
const subject = new Rx.Subject();

const example = subject.map(x => {
    if (x === 1) {
        throw new Error('oops');
    }
    return x;
});

subject.subscribe(
    x => console.log('A', x),
    error => console.log('A Error:' + error)
);

example.subscribe(
    x => console.log('B', x),
    error => console.log('B Error:' + error)
);

subject.subscribe(
    x => console.log('C', x),
    error => console.log('C Error:' + error)
);

source.subscribe(subject);複製程式碼

JSBin - RxJS Subject Problem Solved Demo

RxJS Subject & Observable

Subject 其實是觀察者模式的實現,所以當觀察者訂閱 Subject 物件時,Subject 物件會把訂閱者新增到觀察者列表中,每當有 subject 物件接收到新值時,它就會遍歷觀察者列表,依次呼叫觀察者內部的 next() 方法,把值一一送出。

Subject 之所以具有 Observable 中的所有方法,是因為 Subject 類繼承了 Observable 類,在 Subject 類中有五個重要的方法:

  • next - 每當 Subject 物件接收到新值的時候,next 方法會被呼叫
  • error - 執行中出現異常,error 方法會被呼叫
  • complete - Subject 訂閱的 Observable 物件結束後,complete 方法會被呼叫
  • subscribe - 新增觀察者
  • unsubscribe - 取消訂閱 (設定終止識別符號、清空觀察者列表)

BehaviorSubject

BehaviorSubject 定義

BehaviorSubject 原始碼片段

export class BehaviorSubject extends Subject {
    constructor(_value) { // 設定初始值
        super();
        this._value = _value;
    }
    get value() { // 獲取當前值
        return this.getValue();
    }
    _subscribe(subscriber) {
        const subscription = super._subscribe(subscriber);
        if (subscription && !subscription.closed) {
            subscriber.next(this._value); // 為新的訂閱者傳送當前最新的值
        }
        return subscription;
    }
    getValue() {
        if (this.hasError) {
            throw this.thrownError;
        }
        else if (this.closed) {
            throw new ObjectUnsubscribedError();
        }
        else {
            return this._value;
        }
    }
    next(value) { // 呼叫父類Subject的next方法,同時更新當前值
        super.next(this._value = value);
    }
}複製程式碼

BehaviorSubject 應用

有些時候我們會希望 Subject 能儲存當前的最新狀態,而不是單純的進行事件傳送,也就是說每當新增一個觀察者的時候,我們希望 Subject 能夠立即發出當前最新的值,而不是沒有任何響應。具體我們先看一下示例:

var subject = new Rx.Subject();

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

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

subject.subscribe(observerA);

subject.next(1);
subject.next(2);
subject.next(3);

setTimeout(() => {
  subject.subscribe(observerB); // 1秒後訂閱
}, 1000);複製程式碼

以上程式碼執行後,控制檯的輸出結果:

Observer A get value: 1
Observer A get value: 2
Observer A get value: 3複製程式碼

通過輸出結果,我們發現在 observerB 訂閱 Subject 物件後,它再也沒有收到任何值了。因為 Subject 物件沒有再呼叫 next() 方法。但很多時候我們會希望 Subject 物件能夠儲存當前的狀態,當新增訂閱者的時候,自動把當前最新的值傳送給訂閱者。要實現這個功能,我們就需要使用 BehaviorSubject。

BehaviorSubject 跟 Subject 最大的不同就是 BehaviorSubject 是用來儲存當前最新的值,而不是單純的傳送事件。BehaviorSubject 會記住最近一次傳送的值,並把該值作為當前值儲存在內部的屬性中。接下來我們來使用 BehaviorSubject 重新一下上面的示例:

var subject = new Rx.BehaviorSubject(0); // 設定初始值

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

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

subject.subscribe(observerA);

subject.next(1);
subject.next(2);
subject.next(3);

setTimeout(() => {
  subject.subscribe(observerB); // 1秒後訂閱
}, 1000);複製程式碼

以上程式碼執行後,控制檯的輸出結果:

Observer A get value: 0
Observer A get value: 1
Observer A get value: 2
Observer A get value: 3
Observer B get value: 3複製程式碼

JSBin - BehaviorSubject

ReplaySubject

ReplaySubject 定義

ReplaySubject 原始碼片段

export class ReplaySubject extends Subject {
    constructor(bufferSize = Number.POSITIVE_INFINITY, 
                windowTime = Number.POSITIVE_INFINITY, 
                scheduler) {
        super();
        this.scheduler = scheduler;
        this._events = []; // ReplayEvent物件列表
        this._bufferSize = bufferSize < 1 ? 1 : bufferSize; // 設定緩衝區大小
        this._windowTime = windowTime < 1 ? 1 : windowTime;
    }

    next(value) {
        const now = this._getNow();
        this._events.push(new ReplayEvent(now, value));
        this._trimBufferThenGetEvents();
        super.next(value);
    }

  _subscribe(subscriber) {
        const _events = this._trimBufferThenGetEvents(); // 過濾ReplayEvent物件列表
        let subscription;
        if (this.closed) {
            throw new ObjectUnsubscribedError();
        }
        ...
        else {
            this.observers.push(subscriber);
            subscription = new SubjectSubscription(this, subscriber);
        }
          ...
        const len = _events.length;
        // 重新傳送設定的最後bufferSize個值
        for (let i = 0; i < len && !subscriber.closed; i++) {
            subscriber.next(_events[i].value);
        }
        ...
        return subscription;
    }
}

class ReplayEvent {
    constructor(time, value) {
        this.time = time;
        this.value = value;
    }
}複製程式碼

ReplaySubject 應用

有些時候我們希望在 Subject 新增訂閱者後,能向新增的訂閱者重新傳送最後幾個值,這時我們就可以使用 ReplaySubject ,具體示例如下:

var subject = new Rx.ReplaySubject(2); // 重新傳送最後2個值

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

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

subject.subscribe(observerA);

subject.next(1);
subject.next(2);
subject.next(3);

setTimeout(() => {
  subject.subscribe(observerB); // 1秒後訂閱
}, 1000);複製程式碼

以上程式碼執行後,控制檯的輸出結果:

Observer A get value: 1
Observer A get value: 2
Observer A get value: 3
Observer B get value: 2
Observer B get value: 3複製程式碼

可能會有人認為 ReplaySubject(1) 是不是等同於 BehaviorSubject,其實它們是不一樣的。在建立BehaviorSubject 物件時,是設定初始值,它用於表示 Subject 物件當前的狀態,而 ReplaySubject 只是事件的重放。

JSBin - ReplaySubject

AsyncSubject

AsyncSubject 定義

AsyncSubject 原始碼片段

export class AsyncSubject extends Subject {
    constructor() {
        super(...arguments);
        this.value = null;
        this.hasNext = false;
        this.hasCompleted = false; // 標識是否已完成
    }
    _subscribe(subscriber) {
        if (this.hasError) {
            subscriber.error(this.thrownError);
            return Subscription.EMPTY;
        }
        else if (this.hasCompleted && this.hasNext) { // 等到完成後,才發出最後的值
            subscriber.next(this.value);
            subscriber.complete();
            return Subscription.EMPTY;
        }
        return super._subscribe(subscriber);
    }
    next(value) {
        if (!this.hasCompleted) { // 若未完成,儲存當前的值
            this.value = value;
            this.hasNext = true;
        }
    }
}複製程式碼

AsyncSubject 應用

AsyncSubject 類似於 last 操作符,它會在 Subject 結束後發出最後一個值,具體示例如下:

var subject = new Rx.AsyncSubject();

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

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

  subject.subscribe(observerA);

  subject.next(1);
  subject.next(2);
  subject.next(3);

  subject.complete();

  setTimeout(() => {
    subject.subscribe(observerB); // 1秒後訂閱
  }, 1000);複製程式碼

以上程式碼執行後,控制檯的輸出結果:

Observer A get value: 3
Observer A complete!
Observer B get value: 3
Observer B complete!複製程式碼

JSBin - AsyncSubject

參考資源

相關文章