RxJS Observable

在介紹 Observable 之前,我們要先了解兩個設計模式:
Observer Pattern ——(觀察者模式)
Iterator Pattern ——(迭代器模式)
這兩個模式是 Observable 的基礎,下面我們先來介紹一下 Observer Pattern。
Observer Pattern
觀察者模式定義
觀察者模式是軟件設計模式的一種。在此種模式中,一個目標對象管理所有相依於它的觀察者對象,並且在它本身的狀態改變時主動發出通知。這通常透過呼叫各觀察者所提供的方法來實現。此種模式通常被用來實時事件處理系統。 — 維基百科
觀察者模式又叫發佈訂閱模式(Publish/Subscribe),它定義了一種一對多的關係,讓多個觀察者對象同時監聽某一個主題對象,這個主題對象的狀態發生變化時就會通知所有的觀察者對象,使得它們能夠自動更新自己。
我們可以使用日常生活中,期刊訂閱的例子來形象地解釋一下上面的概念。期刊訂閱包含兩個主要的角色:期刊出版方和訂閱者,他們之間的關係如下:
期刊出版方 - 負責期刊的出版和發行工作
訂閱者 - 只需執行訂閱操作,新版的期刊發佈後,就會主動收到通知,如果取消訂閱,以後就不會再收到通知
在觀察者模式中也有兩個主要角色:Subject (主題) 和 Observer (觀察者) 。它們分別對應例子中的期刊出版方和訂閱者。接下來我們來看張圖,從而加深對上面概念的理解。
觀察者模式優缺點
觀察者模式的優點:
支持簡單的廣播通信,自動通知所有已經訂閱過的對象
目標對象與觀察者之間的抽象耦合關係能夠單獨擴展以及重用
觀察者模式的缺點:
如果一個被觀察者對象有很多的直接和間接的觀察者的話,將所有的觀察者都通知到會花費很多時間
如果在觀察者和觀察目標之間有循環依賴的話,觀察目標會觸發它們之間進行循環調用,可能導致系統崩潰
觀察者模式的應用
在前端領域,觀察者模式被廣泛地使用。最常見的例子就是爲 DOM 對象添加事件監聽,具體示例如下:
確認
function clickHandler(event) {
console.log(‘用戶已點擊確認按鈕!’);
}
document.getElementById(“btn”).addEventListener(‘click’, clickHandler);
上面代碼中,我們通過 addEventListener API 監聽 button 對象上的點擊事件,當用戶點擊按鈕時,會自動執行我們的 clickHandler 函數。
觀察者模式實戰
Subject 類定義:
class Subject {
constructor() {
this.observerCollection = [];
}
registerObserver(observer) {
this.observerCollection.push(observer);
}
unregisterObserver(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.registerObserver(observer1); // 註冊觀察者A
subject.registerObserver(observer2); // 註冊觀察者B
subject.notifyObservers(); // 通知觀察者
subject.unregisterObserver(observer1); // 移除觀察者A
subject.notifyObservers(); // 驗證是否成功移除
以上代碼成功運行後控制檯的輸出結果:
semlinker has been notified. # 輸出一次
2(unknown) lolo has been notified. # 輸出兩次
需要注意的是,在觀察者模式中,通常情況下調用註冊觀察者後,會返回一個函數,用於移除監聽,有興趣的讀者,可以自己嘗試一下。(備註:在 Angular 1.x 中調用 on() 方法後,就會返回一個函數,用於移除監聽)
Iterator Pattern
迭代器模式定義
迭代器(Iterator)模式,又叫做遊標(Cursor)模式。它提供一種方法順序訪問一個聚合對象中的各個元素,而又不需要暴露該對象的內部表示。迭代器模式可以把迭代的過程從業務邏輯中分離出來,在使用迭代器模式之後,即使不關心對象的內部構造,也可以按順序訪問其中的每個元素。
迭代器模式的優缺點
迭代器模式的優點:
簡化了遍歷方式,對於對象集合的遍歷,還是比較麻煩的,對於數組或者有序列表,我們尚可以通過遊標取得,但用戶需要在對集合瞭解的前提下,自行遍歷對象,但是對於 hash 表來說,用戶遍歷起來就比較麻煩。而引入迭代器方法後,用戶用起來就簡單的多了。
封裝性良好,用戶只需要得到迭代器就可以遍歷,而不用去關心遍歷算法。
迭代器模式的缺點:
遍歷過程是一個單向且不可逆的遍歷
ECMAScript 迭代器
在 ECMAScript 中 Iterator 最早其實是要採用類似 Python 的 Iterator 規範,就是 Iterator 在沒有元素之後,執行 next會直接拋出錯誤;但後來經過一段時間討論後,決定採更 functional 的做法,改成在取得最後一個元素之後執行 next 永遠都回傳 { done: true, value: undefined }
一個迭代器對象 ,知道如何每次訪問集合中的一項, 並記錄它的當前在序列中所在的位置。在 JavaScript 中迭代器是一個對象,它提供了一個 next() 方法,返回序列中的下一項。這個方法返回包含 done 和 value 兩個屬性的對象。對象的取值如下:
在最後一個元素前:{ done: false, value: elementValue }
在最後一個元素後:{ done: true, value: undefined }
詳細信息可以參考 - 可迭代協議和迭代器協議
ES 5 迭代器
接下來我們來創建一個 makeIterator 函數,該函數的參數類型是數組,當調用該函數後,返回一個包含 next() 方法的 Iterator 對象, 其中 next() 方法是用來獲取容器對象中下一個元素。具體示例如下:
function makeIterator(array){
var nextIndex = 0;
return {
next: function(){
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{done: true};
}
}
}
一旦初始化, next() 方法可以用來依次訪問可迭代對象中的元素:
var it = makeIterator([‘yo’, ‘ya’]);
console.log(it.next().value); // ‘yo’
console.log(it.next().value); // ‘ya’
console.log(it.next().done); // true
ES 6 迭代器
在 ES 6 中我們可以通過 Symbol.iterator 來創建可迭代對象的內部迭代器,具體示例如下:
let arr = [‘a’, ‘b’, ‘c’];
let iter = arrSymbol.iterator;
調用 next() 方法來獲取數組中的元素:

iter.next(){ value: ‘a’, done: false }
iter.next(){ value: ‘b’, done: false }
iter.next(){ value: ‘c’, done: false }
iter.next(){ value: undefined, done: true }
ES 6 中可迭代的對象:
Arrays
Strings
Maps
Sets
DOM data structures (work in progress)
Observable
RxJS 是基於觀察者模式和迭代器模式以函數式編程思維來實現的。RxJS 中含有兩個基本概念:Observables 與 Observer。Observables 作爲被觀察者,是一個值或事件的流集合;而 Observer 則作爲觀察者,根據 Observables 進行處理。
Observables 與 Observer 之間的訂閱發佈關係(觀察者模式) 如下:
訂閱:Observer 通過 Observable 提供的 subscribe() 方法訂閱 Observable。
發佈:Observable 通過回調 next 方法向 Observer 發佈事件。
自定義 Observable
如果你想真正瞭解 Observable,最好的方式就是自己寫一個。其實 Observable 就是一個函數,它接受一個 Observer 作爲參數然後返回另一個函數。
它的基本特徵:
是一個函數
接受一個 Observer 對象 (包含 next、error、complete 方法的對象) 作爲參數
返回一個 unsubscribe 函數,用於取消訂閱
它的作用:
作爲生產者與觀察者之間的橋樑,並返回一種方法來解除生產者與觀察者之間的聯繫,其中觀察者用於處理時間序列上數據流。接下來我們來看一下 Observable 的基礎實現:
DataSource - 數據源
class DataSource {
constructor() {
let i = 0;
this._id = setInterval(() => this.emit(i++), 200); // 創建定時器
}
emit(n) {
const limit = 10; // 設置數據上限值
if (this.ondata) {
this.ondata(n);
}
if (n === limit) {
if (this.oncomplete) {
this.oncomplete();
}
this.destroy();
}
}
destroy() {
// 清除定時器
clearInterval(this._id);
}
}
myObservable
function myObservable(observer) {
let datasource = new DataSource(); // 創建數據源
datasource.ondata = (e) => observer.next(e); // 處理數據流
datasource.onerror = (err) => observer.error(err); // 處理異常
datasource.oncomplete = () => observer.complete(); // 處理數據流終止
return () => {
// 返回一個函數用於,銷燬數據源
datasource.destroy();
};
}
使用示例:
const unsub = myObservable({
next(x) { console.log(x); },
error(err) { console.error(err); },
complete() { console.log(‘done’)}
});
/*** 移除註釋,可以測試取消訂閱*/
// setTimeout(unsub, 500);
具體運行結果,可以查看線上示例。
SafeObserver - 更好的 Observer
上面的示例中,我們使用一個包含了 next、error、complete 方法的普通 JavaScript 對象來定義觀察者。一個普通的 JavaScript 對象只是一個開始,在 RxJS 5 裏面,爲開發者提供了一些保障機制,來保證一個更安全的觀察者。以下是一些比較重要的原則:
傳入的 Observer 對象可以不實現所有規定的方法 (next、error、complete 方法)
在 complete 或者 error 觸發之後再調用 next 方法是沒用的
調用 unsubscribe 方法後,任何方法都不能再被調用了
complete 和 error 觸發後,unsubscribe 也會自動調用
當 next、complete和error 出現異常時,unsubscribe 也會自動調用以保證資源不會浪費
next、complete和error是可選的。按需處理即可,不必全部處理
爲了完成上述目標,我們得把傳入的匿名 Observer 對象封裝在一個 SafeObserver 裏以提供上述保障。SafeObserver 的具體實現如下:
class SafeObserver {
constructor(destination) {
this.destination = destination;
}
next(value) {
// 尚未取消訂閱,且包含next方法
if (!this.isUnsubscribed && this.destination.next) {
try {
this.destination.next(value);
} catch (err) {
// 出現異常時,取消訂閱釋放資源,再拋出異常
this.unsubscribe();
throw err;
}
}
}
error(err) {
// 尚未取消訂閱,且包含error方法
if (!this.isUnsubscribed && this.destination.error) {
try {
this.destination.error(err);
} catch (e2) {
// 出現異常時,取消訂閱釋放資源,再拋出異常
this.unsubscribe();
throw e2;
}
this.unsubscribe();
}
}
complete() {
// 尚未取消訂閱,且包含complete方法
if (!this.isUnsubscribed && this.destination.complete) {
try {
this.destination.complete();
} catch (err) {
// 出現異常時,取消訂閱釋放資源,再拋出異常
this.unsubscribe();
throw err;
}
this.unsubscribe();
}
}
unsubscribe() {
// 用於取消訂閱
this.isUnsubscribed = true;
if (this.unsub) {
this.unsub();
}
}
}
myObservable - 使用 SafeObserver
function myObservable(observer) {
const safeObserver = new SafeObserver(observer); // 創建SafeObserver對象
const datasource = new DataSource(); // 創建數據源
datasource.ondata = (e) => safeObserver.next(e);
datasource.onerror = (err) => safeObserver.error(err);
datasource.oncomplete = () => safeObserver.complete();
safeObserver.unsub = () => {
// 爲SafeObserver對象添加unsub方法
datasource.destroy();
};
// 綁定this上下文,並返回unsubscribe方法
return safeObserver.unsubscribe.bind(safeObserver);
}
使用示例:
const unsub = myObservable({
next(x) { console.log(x); },
error(err) { console.error(err); },
complete() { console.log(‘done’)}
});
具體運行結果,可以查看線上示例。
Operators - 也是函數
Operator 是一個函數,它接收一個 Observable 對象,然後返回一個新的 Observable 對象。當我們訂閱新返回的 Observable 對象時,它內部會自動訂閱前一個 Observable 對象。接下來我們來實現常用的 map 操作符:
Observable 實現:
class Observable {
constructor(_subscribe) {
this._subscribe = _subscribe;
}
subscribe(observer) {
const safeObserver = new SafeObserver(observer);
safeObserver.unsub = this._subscribe(safeObserver);
return safeObserver.unsubscribe.bind(safeObserver);
}
}
map 操作符實現:
function map(source, project) {
return new Observable((observer) => {
const mapObserver = {
next: (x) => observer.next(project(x)),
error: (err) => observer.error(err),
complete: () => observer.complete()
};
return source.subscribe(mapObserver);
});
}
具體運行結果,可以查看線上示例。
改進 Observable - 支持 Operator 鏈式調用
如果把 Operator 都寫成如上那種獨立的函數,我們鏈式代碼會逐漸變醜:
map(map(myObservable, (x) => x + 1), (x) => x + 2);
對於上面的代碼,想象一下有 5、6 個嵌套着的 Operator,再加上更多、更復雜的參數,基本上就沒法兒看了。
你也可以試下 Texas Toland 提議的簡單版管道實現,合併壓縮一個數組的Operator並生成一個最終的Observable,不過這意味着要寫更復雜的 Operator,上代碼:JSBin。其實寫完後你會發現,代碼也不怎麼漂亮:
pipe(myObservable, map(x => x + 1), map(x => x + 2));
理想情況下,我們想將代碼用更自然的方式鏈起來:
myObservable.map(x => x + 1).map(x => x + 2);
幸運的是,我們已經有了這樣一個 Observable 類,我們可以基於 prototype 在不增加複雜度的情況下支持多 Operators 的鏈式結構,下面我們採用prototype方式再次實現一下 Observable:
Observable.prototype.map = function (project) {
return new Observable((observer) => {
const mapObserver = {
next: (x) => observer.next(project(x)),
error: (err) => observer.error(err),
complete: () => observer.complete()
};
return this.subscribe(mapObserver);
});
};
現在我們終於有了一個還不錯的實現。這樣實現還有其他好處,例如:可以寫子類繼承 Observable 類,然後在子類中重寫某些內容以優化程序。
接下來我們來總結一下該部分的內容:Observable 就是函數,它接受 Observer 作爲參數,又返回一個函數。如果你也寫了一個函數,接收一個 Observer 作爲參數,又返回一個函數,那麼,它是異步的、還是同步的 ?其實都不是,它就只是一個函數。任何函數的行爲都依賴於它的具體實現,所以當你處理一個 Observable 時,就把它當成一個普通函數,裏面沒有什麼黑魔法。當你要構建 Operator 鏈時,你需要做的其實就是生成一個函數將一堆 Observers 鏈接在一起,然後讓真正的數據依次穿過它們。
Hot Observable vs Cold Observable
Hot Observable
Hot Observable 無論有沒有 Subscriber 訂閱,事件始終都會發生。當 Hot Observable 有多個訂閱者時,Hot Observable 與訂閱者們的關係是一對多的關係,可以與多個訂閱者共享信息。
const socket = new WebSocket(‘ws://someurl’);
const source = new Observable((observer) => {
socket.addEventListener(‘message’, (e) => observer.next(e));
});
Cold Observable
Cold Observable 只有 Subscriber 訂閱時,纔開始執行發射數據流的代碼。並且 Cold Observable 和 Subscriber 只能是一對一的關係,當有多個不同的訂閱者時,消息是重新完整發送的。也就是說對 Cold Observable 而言,有多個 Subscriber 的時候,他們各自的事件是獨立的。
const source = new Observable((observer) => {
const socket = new WebSocket(‘ws://someurl’);
socket.addEventListener(‘message’, (e) => observer.next(e));
return () => socket.close();
});
一個 Observable 是 Hot 還是 Cold,都是相對於生產者而言的,如果每次訂閱的時候,外部的生產者已經創建好了,那就是 Hot Observable,反之,如果每次訂閱的時候都會產生一個新的生產者,那就是 Cold Observable。
Pull vs Push
Pull 和 Push 是數據生產者和數據的消費者兩種不同的交流方式。
什麼是Pull?
在 “拉” 體系中,數據的消費者決定何時從數據生產者那裏獲取數據,而生產者自身並不會意識到什麼時候數據將會被髮送給消費者。
每一個 JavaScript 函數都是一個 “拉” 體系,函數是數據的生產者,調用函數的代碼通過 ‘’拉出” 一個單一的返回值來消費該數據。
const add = (a, b) => a + b;
let sum = add(3, 4);
ES6介紹了 iterator迭代器 和 Generator生成器 — 另一種 “拉” 體系,調用 iterator.next() 的代碼是消費者,可從中拉取多個值。
什麼是Push?
在 “推” 體系中,數據的生產者決定何時發送數據給消費者,消費者不會在接收數據之前意識到它將要接收這個數據。
Promise(承諾) 是當今 JS 中最常見的 “推” 體系,一個Promise (數據的生產者)發送一個 resolved value (成功狀態的值)來執行一個回調(數據消費者),但是不同於函數的地方的是:Promise 決定着何時數據才被推送至這個回調函數。
RxJS 引入了 Observables (可觀察對象),一個全新的 “推” 體系。一個可觀察對象是一個產生多值的生產者,當產生新數據的時候,會主動 “推送給” Observer (觀察者)。
生產者
消費者
pull拉
被請求的時候產生數據
決定何時請求數據
push推
按自己的節奏生產數據
對接收的數據進行處理
接下來我們來看張圖,從而加深對上面概念的理解:
Observable vs Promise
Observable(可觀察對象)是基於推送(Push)運行時執行(lazy)的多值集合。
MagicQ
單值
多值
拉取(Pull)
函數
遍歷器
推送(Push)
Promise
Observable
Promise
返回單個值
不可取消的
Observable
隨着時間的推移發出多個值
可以取消的
支持 map、filter、reduce 等操作符
延遲執行,當訂閱的時候纔會開始執行
延遲計算 & 漸進式取值
延遲計算
所有的 Observable 對象一定會等到訂閱後,纔開始執行,如果沒有訂閱就不會執行。
import { from } from “rxjs”;
import { map } from “rxjs/operators”;
const source$ = from([1, 2, 3, 4, 5]);
const example$ = source.pipe(map(x=>x+1));example.pipe(map(x => x + 1)); 上面的示例中,因爲 example 對象還未被訂閱,所以不會進行運算。這跟數組不一樣,具體如下:
const source = [1,2,3,4,5];
const example = source.map(x => x + 1);
以上代碼運行後,example 中就包含已運算後的值。
漸進式取值
數組中的操作符如:filter、map 每次都會完整執行並返回一個新的數組,纔會繼續下一步運算。具體示例如下:
const source = [1,2,3,4,5];
const example = source
.filter(x => x % 2 === 0) // [2, 4]
.map(x => x + 1) // [3, 5]
關於數組中的 map、filter 的詳細信息,可以閱讀 - RxJS Functional Programming
爲了更好地理解數組操作符的運算過程,我們可以查看 Array Compute。
雖然 Observable 運算符每次都會返回一個新的 Observable 對象,但每個元素都是漸進式獲取的,且每個元素都會經過操作符鏈的運算後才輸出,而不會像數組那樣,每個階段都得完整運算。具體示例如下:
import { from } from “rxjs”;
import { filter, map } from “rxjs/operators”;
const source$ = from([1, 2, 3, 4, 5]);
const example$ = source.pipe(filter(x=>xmap(x=>x+1));example.pipe( filter(x => x % 2 === 0), map(x => x + 1) ); example.subscribe(console.log);
以上代碼的輸出結果:
3
5
爲了更好地理解 Observable 操作符的運算過程,我們可以參考 Observable Compute。

發佈了30 篇原創文章 · 獲贊 3 · 訪問量 9133
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章