- 原文地址:RxJS: Observables, observers and operators introduction
- 原文作者:Todd
- 譯文出自:掘金翻譯計劃
- 譯者:lsvih
- 校對者:sunui,GangsterHyj
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.subscribe
的 subscribe
回撥都將會被我們或者其它的可觀察物件呼叫。這樣我們下面做的事情就有意義了。
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
其實就是攜帶 next
、error
、complete
的物件字面量。
這塊其實很有意思。
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
了嗎?這說明你成功地建立了一個可觀察物件流。
再試試:
希望這篇文章對你來說還算有趣~:)
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃。