RxJS 簡介:可觀察物件、觀察者與操作符

lsvih發表於2017-06-05

RxJS 簡介:可觀察物件、觀察者與操作符

對於響應式程式設計來說,RxJS 是一個不可思議的工具。今天我們將深入探討什麼是 Observable(可觀察物件)和 observer(觀察者),然後瞭解如何建立自己的 operator(操作符)。

如果你之前用過 RxJS,想了解它的內部工作原理,以及 Observable、operator 是如何運作的,這篇文章將很適合你閱讀。

什麼是 Observable(可觀察物件)?

可觀察物件其實就是一個比較特別的函式,它接受一個“觀察者”(observer)物件作為引數(在這個觀察者物件中有 “next”、“error”、“complete”等方法),以及它會返回一種解除與觀察者關係的邏輯。例如我們自己實現的時候會使用一個簡單的 “unsubscribe” 函式來實現退訂功能(即解除與觀察者繫結關係的邏輯)。而在 RxJS 中, 它是一個包含 unsubsribe 方法的訂閱物件(Subscription)。

可觀察物件會建立觀察者物件(稍後我們將詳細介紹它),並將它和我們希望獲取資料值的“東西”連線起來。這個“東西”就是生產者(producer),它可能來自於 click 或者 input 之類的 DOM 事件,是資料值的來源。當然,它也可以是一些更復雜的情況,比如通過 HTTP 與伺服器交流的事件。

我們稍後將要自己寫一個可觀察物件,以便更好地理解它!在此之前,讓我們先看看一個訂閱物件的例子:

const node = document.querySelector('input[type=text]');

const input$ = Rx.Observable.fromEvent(node, 'input');

input$.subscribe({
  next: (event) => console.log(`你剛剛輸入了 ${event.target.value}!`),
  error: (err) => console.log(`Oops... ${err}`),
  complete: () => console.log(`完成!`)
});複製程式碼

這個例子使用了一個 <input type="text"> 節點,並將其傳入 Rx.Observable.fromEvent() 中。當我們觸發指定的事件名時,它將會返回一個輸入的 Event 的可觀察物件。(因此我們在 console.log 中用 ${event.target.value} 可以獲取輸入值)

當輸入事件被觸發的時候,可觀察物件會將它的值傳給觀察者。

什麼是 Observer(觀察者)?

觀察者相當容易理解。在前面的例子中,我們傳入 .subscribe() 中的物件字面量就是觀察者(訂閱物件將會呼叫我們的可觀察物件)。

.subscribe(next, error, complete) 也是一種合法的語法,但是我們現在研究的是物件字面量的情況。

當一個可觀察物件產生資料值的時候,它會通知觀察者,當新的值被成功捕獲的時候呼叫 .next(),發生錯誤的時候呼叫 .error()

當我們訂閱一個可觀察物件的時候,它會持續不斷地將值傳遞給觀察者,直到發生以下兩件事:一種是生產者告知沒有更多的值需要傳遞了,這種情況它會呼叫觀察者的 .complete() ;一種是我們(“消費者”)對之後的值不再感興趣,決定取消訂閱(unsubsribe)。

如果我們想要對可觀察物件傳來的值進行組成構建(compose),那麼在值傳達最終的 .subscribe() 程式碼塊之前,需要經過一連串的可觀察物件(也就是操作符)處理。這個一連串的“鏈”也就是我們所說的可觀察物件序列。鏈中的每個操作符都會返回一個新的可觀察物件,讓我們的序列能夠持續進行下去——這也就是我們所熟知的“流”。

什麼是 Operator(操作符)?

我們前面提到,可觀察物件能夠進行鏈式呼叫,也就是說我們可以像這樣寫程式碼:

const input$ = Rx.Observable.fromEvent(node, 'input')
  .map(event => event.target.value)
  .filter(value => value.length >= 2)
  .subscribe(value => {
    // use the `value`
  });複製程式碼

這段程式碼做了下面一系列事情:

  • 我們先假定使用者輸入了一個“a”
  • 可觀察物件將會對這個輸入事件作出反應,將值傳給下一個觀察者
  • “a”被傳給了訂閱了我們初始可觀察物件的 .map()
  • .map() 會返回一個 event.target.value 的新可觀察物件,然後呼叫它觀察者物件中的 .next()
  • .next() 將會呼叫訂閱了 .map().filter(),並將 .map() 處理後的值傳遞給它
  • .filter() 將會返回另一個可觀察物件,.filter() 過濾後留下 .length 大於等於 2 的值,並將其傳給 .next()
  • 我們通過 .subscribe() 獲得了最終的資料值

這短短的幾行程式碼做了這麼多的事!如果你還覺得弄不清,只需要記住:

每當返回一個新的可觀察物件,都會有一個新的觀察者掛載到前一個可觀察物件上,這樣就能通過觀察者的“流”進行傳值,對觀察者生產的值進行處理,然後呼叫 .next() 方法將處理後的值傳遞給下一個觀察者。

簡單來說,操作符將會不斷地依次返回新的可觀察物件,讓我們的流能夠持續進行。作為使用者而言,我們不需要關心什麼時候、什麼情況下需要建立與使用可觀察物件與觀察者,我們只需要用我們的訂閱物件進行鏈式呼叫就行了。

建立我們自己的 Observable(可觀察物件)

現在,讓我們開始寫自己的可觀察物件的實現吧。儘管它不會像 Rx 的實現那麼高階,但我們還是對完善它充滿信心。

Observable 構造器

首先,我們需要建立一個 Observable 建構函式,此建構函式接受且僅接受 subscribe 函式作為其唯一的引數。每個 Observable 例項都儲存 subscribe 屬性,稍後可以由觀察者物件呼叫它:

function Observable(subscribe) {
  this.subscribe = subscribe;
}複製程式碼

每個分配給 this.subscribesubscribe 回撥都將會被我們或者其它的可觀察物件呼叫。這樣我們下面做的事情就有意義了。

Observer 示例

在深入探討實際情況之前,我們先看一看基礎的例子。

現在我們已經配好了可觀察物件函式,可以呼叫我們的觀察者,將 1 這個值傳給它並訂閱它:

const one$ = new Observable((observer) => {
  observer.next(1);
  observer.complete();
});

one$.subscribe({
  next: (value) => console.log(value) // 1
});複製程式碼

我們訂閱了 Observable 例項,將我們的 observer(物件字面量)傳入構造器中(之後它會被分配給 this.subscribe)。

Observable.fromEvent

現在我們已經完成了建立自己的 Observable 的基礎步驟。下一步是為 Observable 新增 static 方法:

Observable.fromEvent = (element, name) => {

};複製程式碼

我們將像使用 RxJS 一樣使用我們的 Observable:

const node = document.querySelector('input');

const input$ = Observable.fromEvent(node, 'input');複製程式碼

這意味著我們需要返回一個新的 Observable,然後將函式作為引數傳遞給它:

Observable.fromEvent = (element, name) => {
  return new Observable((observer) => {

  });
};複製程式碼

這段程式碼將我們的函式傳入了構造器中的 this.subscribe。接下來,我們需要將事件監聽設定好:

Observable.fromEvent = (element, name) => {
  return new Observable((observer) => {
    element.addEventListener(name, (event) => {}, false);
  });
};複製程式碼

那麼這個 observer 引數是什麼呢?它又是從哪裡來的呢?

這個 observer 其實就是攜帶 nexterrorcomplete 的物件字面量。

這塊其實很有意思。observer.subscribe() 被呼叫之前都不會被傳遞,因此 addEventListener 在 Observable 被“訂閱”之前都不會被執行。

一旦呼叫 subscribe,也就會呼叫 Observable 構造器內的 this.subscribe 。它將會呼叫我們傳入 new Observable(callback) 的 callback,同時也會依次將值傳給我們的觀察者。這樣,當 Observable 做完一件事的時候,它就會用更新過的值呼叫我們觀察者中的 .next() 方法。

那麼之後呢?我們已經得到了初始化好的事件監聽器,但是還沒有呼叫 .next()。下面完成它:

Observable.fromEvent = (element, name) => {
  return new Observable((observer) => {
    element.addEventListener(name, (event) => {
      observer.next(event);
    }, false);
  });
};複製程式碼

我們都知道,可觀察物件在被銷燬前需要一個“處理後事”的函式,在我們這個例子中,我們需要移除事件監聽:

Observable.fromEvent = (element, name) => {
  return new Observable((observer) => {
    const callback = (event) => observer.next(event);
    element.addEventListener(name, callback, false);
    return () => element.removeEventListener(name, callback, false);
  });
};複製程式碼

因為這個 Observable 還在處理 DOM API 和事件,因此我們還不會去呼叫 .complete()。這樣在技術上就有無限的可用性。

試一試吧!下面是我們已經寫好的完整程式碼:

const node = document.querySelector('input');
const p = document.querySelector('p');

function Observable(subscribe) {
  this.subscribe = subscribe;
}

Observable.fromEvent = (element, name) => {
  return new Observable((observer) => {
    const callback = (event) => observer.next(event);
    element.addEventListener(name, callback, false);
    return () => element.removeEventListener(name, callback, false);
  });
};

const input$ = Observable.fromEvent(node, 'input');

const unsubscribe = input$.subscribe({
  next: (event) => {
    p.innerHTML = event.target.value;
  }
});

// 5 秒之後自動取消訂閱
setTimeout(unsubscribe, 5000);複製程式碼

線上示例:

創造我們自己的 Operator(操作符)

在我們理解了可觀察物件與觀察者物件的概念之後,我們可以更輕鬆地去創造我們自己的操作符了。我們在 Observable 物件原型中加上一個新的方法:

Observable.prototype.map=function(mapFn){

};複製程式碼

這個方法將會像 JavaScript 中的 Array.prototype.map 一樣使用,不過它可以對任何值用:

const input$ = Observable.fromEvent(node, 'input')
    .map(event => event.target.value);複製程式碼

所以我們要取得回撥函式,並呼叫它,返回我們期望得到的資料。在這之前,我們需要拿到流中最新的資料值。

下面該做什麼就比較明瞭了,我們要得到呼叫了這個 .map() 操作符的 Observable 例項的引用入口。我們是在原型鏈上程式設計,因此可以直接這麼做:

Observable.prototype.map = function (mapFn) {
  const input = this;
};複製程式碼

找找樂子吧!現在我們可以在返回的 Obeservable 中呼叫 subscribe:

Observable.prototype.map = function (mapFn) {
  const input = this;
  return new Observable((observer) => {
      return input.subscribe();
  });
};複製程式碼

我們要返回 input.subscribe() ,因為在我們退訂的時候,非訂閱物件將會順著鏈一直轉下去,解除每個 Observable 的訂閱。

這個訂閱物件將允許我們把之前 Observable.fromEvent 傳來的值傳遞下去,因為它返回了構造器中含有 subscribe 原型的新的 Observable 物件。我們可以輕鬆地訂閱它對資料值做出的任何更新!最後,完成通過 map 呼叫我們的 mapFn() 的功能:

Observable.prototype.map = function (mapFn) {
  const input = this;
  return new Observable((observer) => {
    return input.subscribe({
      next: (value) => observer.next(mapFn(value)),
      error: (err) => observer.error(err),
      complete: () => observer.complete()
    });
  });
};複製程式碼

現在我們可以進行鏈式呼叫了!

const input$ = Observable.fromEvent(node, 'input')
  .map(event => event.target.value);

input$.subscribe({
  next: (value) => {
    p.innerHTML = value;
  }
});複製程式碼

注意到最後一個 .subscribe() 不再和之前一樣傳入 Event 物件,而是傳入了一個 value 了嗎?這說明你成功地建立了一個可觀察物件流。

再試試:

希望這篇文章對你來說還算有趣~:)


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃

相關文章