觀察者模式在 Javascript 中的應用

jkest發表於2017-12-14

*觀察者模式(Observer Pattern)*在 Javascript 中應用非常普遍,本文會首先明確觀察者模式在 Javascript 的基本實現方式,然後著眼與當前流行的工具庫 —— RxJS —— 進一步研究觀察者模式在其中的實現原理。


觀察者模式的模式結構

觀察者模式定義物件間的一種一對多依賴關係,使得當每一個被依賴物件狀態發生改變時,其相關依賴物件皆得到通知並被自動更新。

被依賴物件通常被稱為主體(Subject),為了和 RxJS 中的概念一致,將其稱為被觀察物件(Observable),其相關依賴物件被稱為觀察者(Observer),即被觀察物件會主動地向觀察者“推送(push)”訊息,而不是觀察者向被觀察物件“拉取(pull)”訊息,實現的是一種 Push 模式的訊息系統。

從被觀察物件和觀察者的類圖可以明確地看到兩者之間的訊息傳遞關係:

觀察者模式類圖
觀察者模式類圖

被觀察物件通過 subscribe 方法和 unsubscribe 方法新增和刪除一個觀察者,通過 broadcast 方法向觀察者推送訊息,接下來是一個最簡單的觀察者模式虛擬碼:

const clickHandler = (e) => { /* ... */ };
document.body.addEventListener('click', clickHandler);
複製程式碼

document.body 在這裡就是一個被觀察物件, 全域性物件是觀察者,當 click 事件觸發的時候,觀察者會呼叫 clickHandler 方法。


實現一個 RxJS 風格的觀察者模式

本文重點並不是如何使用 RxJS 工具,而是理解其基本的實現思想。所以接下來並不會詳細說明 RxJS 的使用方法,而是自己動手實現一個具有類似功能的簡易觀察者模式,旨在學習如何自己在工作中運用觀察模式。

首先介紹 RxJS 中最基本的使用方法。

第一步,建立一個 Observable 物件:

var observable = Rx.Observable.create(observer => {
  observer.next(1);
  observer.next(2);
  setTimeout(() => {
    observer.next(3);
    observer.complete();
  }, 1000);
});
複製程式碼

第二步,在 observable 上訂閱一個 observer

const observer = {
  next: x => console.log('got value ' + x),
  error: err => console.error('something wrong occurred: ' + err),
  complete: () => console.log('done'),
};
console.log('just before subscribe');
const subscription = observable.subscribe(observer);
console.log('just after subscribe');
複製程式碼

接著就會看到輸出結果:

just before subscribe
got value 1
got value 2
just after subscribe
got value 4
done
複製程式碼

如果在呼叫了 subscribe 之後立即呼叫 unsubscribe ,則該 observer 會被取消訂閱:

subscription.unsubscribe();
複製程式碼

輸出結果為:

just before subscribe
got value 1
got value 2
just after subscribe
複製程式碼

即 1s 之後的執行動作被取消了。

通過上面的程式碼可以得知,RxJS 不僅使用了觀察者模式還結合了迭代器模式的使用,本文重點關注其觀察者模式的實現原理。

一共需要實現兩個類,ObservableSubscriptionobservable 通過自身的 subscribe 方法訂閱 observer 並且返回一個 subscription 物件,subscription 物件呼叫自身的 unsubscribe 方法可以當前 observer 的訂閱,從輸出結果上來看,即取消了 observernexterror 以及 complete 方法的執行。

注意 observer 並不需要實現,observer 是傳入 Observable.prototype.subscribe 的引數。分析完畢之後,本文接下來會在 RxJS 的原始碼基礎上構建一個簡化版本實現。

首先是 Observable.js

module.exports = class Observable {
  constructor(_subscribe) {
    this._subscribe = _subscribe; // 傳入的執行函式
  }
  static create(_subscribe) {
    return new Observable(_subscribe);
  }
  subscribe({ next, error, complete }) { // 解構傳入的 observer
    const sink = new Subscription(next, error, complete);
    this._subscribe(sink);
    return sink;
  }
}
複製程式碼

接下來是 Subscription.js

module.exports = class Subscription {
  constructor(next, error, complete) {
    this._isStopped = false;
    this._next = next;
    this._error = error;
    this._complete = complete;
  }
  next(value) {
    if (!this._isStopped) {
      this._next(value);
    }
  }
  error(value) {
    if (!this._isStopped) {
      this._isStopped = true;
      this._error(value);
      this.unsubscribe();
    }
  }
  complete(value) {
    if (!this._isStopped) {
      this._isStopped = true;
      this._complete(value);
      this.unsubscribe();
    }
  }
  unsubscribe() {
    this._isStopped = true;
  }
}
複製程式碼

subscription 通過 _isStopped 標誌顯示是否呼叫了 unsubscribe 方法,並且將 observernexterrorcomplete 方法進行了進一步的包裝,使得他們可以被取消訂閱。

接下來使用類似呼叫 RxJS 的程式碼進行測試:

const Observable = require('./Observable');
const observable = Observable.create(observer => {
  observer.next(1);
  observer.next(2);
  setTimeout(() => {
    observer.next(3);
    observer.complete();
  }, 1000);
});
const observer = {
  next: x => console.log('got value ' + x),
  error: err => console.error('something wrong occurred: ' + err),
  complete: () => console.log('done'),
};
console.log('just before subscribe');
const subscription = observable.subscribe(observer);
console.log('just after subscribe');
複製程式碼

輸出結果和呼叫 RxJS 一致:

just before subscribe
got value 1
got value 2
just after subscribe
got value 4
done
複製程式碼

如果在呼叫了 subscribe 之後立即呼叫 unsubscribe

subscription.unsubscribe();
複製程式碼

輸出結果為:

just before subscribe
got value 1
got value 2
just after subscribe
複製程式碼

至此實現了一個建議版本的 RxJS 風格的觀察者模式,RxJS 實在是微軟的良心出品,建議每位前端工程師都能掌握。


觀察者模式的實際用途

觀察者模式的價值主要體現在以下兩個方面:

  • 非同步程式設計中廣泛使用。

    觀察者模式實現了非同步物件的執行邏輯與非同步回撥邏輯的完全分離,以上面提到的 document.body.addEventListener 方法為例,接下來使用 RxJS 版本的觀察者模式進行說明,首先是建立 Observable 物件:

    const observable = Rx.Observable.fromEvent(document.body, 'click');
    複製程式碼

    接著是訂閱一個 Observer

    observable.subscribe({
      next: () => {/* .. */}
    })
    複製程式碼

    進行編寫非同步回撥邏輯的時候,只需要關注 subscribe 函式的呼叫,完全不用關注非同步執行邏輯的內部狀態,程式的純淨性也得到了提高。

  • 程式便於擴充套件。

    由於觀察者模式實現了物件執行邏輯與回撥邏輯的鬆耦合,那麼當出現新的 observer 時,就完全不用改變 observable 物件內部實現,而只需要再呼叫一次 Observable.prototype.subscribe 方法新增一個新的 observer,這樣對於多人合作的專案來說,不僅便於分工也便於隨時擴充套件。


觀察者模式釋出/訂閱模式的比較

觀察者模式釋出/訂閱模式的目標是一致的,都是主體主動地向通知觀察者推送訊息,然而觀察者模式中主體和觀察者還是存在一定的耦合性,兩者必須確切的知道對方的存在才能進行訊息的傳遞。

釋出/訂閱模式 更像是一個全域性的觀察者模式釋出/訂閱模式在主體與觀察者之間引入訊息排程中心,有點類似於 Node.js 中的 EventEmitter,主體和觀察者之間完全透明,所有的訊息傳遞過程都通過訊息排程中心完成,也就是說具體的業務邏輯程式碼將會是在訊息排程中心內,而主體和觀察者之間實現了完全的鬆耦合,帶來的問題也很直接:程式易讀性顯著降低。

觀察者模式釋出/訂閱模式密不可分但又存在某種差異,並且各有利弊不能絕對的說孰優孰劣,使用何種模式需要根據實際應用場景進行選擇。


參考資料

相關文章