如何理解 RxJS?

在 Angular 2 中,我們遇到了一個新的概念 —— RxJS。

對很多人而言,這可能是一個比較難以理解的地方。所謂的難以理解並不是說 API 有多複雜,而是對於 RxJS 本身的理念就無從下手。

所以,這裏簡單地對 RxJS 進行一些介紹。

函數響應式編程(FRP)

FRP 早在上世紀 90 年代就已經被提出,但由於早期的編譯器和運行時能力有限,大部分編程實踐中往往採用的是人遷就機器的理念,即 命令式編程 ,或者叫廣義的面向過程。(這裏指包括面向對象在內的以指定步驟的方式來編程的方式)

而另一類編程方式,叫做 聲明式編程 。在聲明式編程中,並不會爲一個操作指定步驟,而只是單純的給出我們的意圖,而函數響應式編程就是聲明式編程中的一個重要實踐。

其實,聲明式編程本身並不特別,例如我們的 HTML 就是一個很常見的聲明式編程。

比如我們有下面的 HTML:

<ul class="list">
  <li class="item">1</li>
  <li class="item">2</li></ul>

對應到命令式編程中,我們大概會得到下面的代碼:

const ul = document.createElement('ul')
ul.className = 'list'
const lis = [1, 2].map(n => {
li = document.createElement('li')
li.className = 'item'
li.textContent = n.toString()
})
lis.forEach(li => ul.appendChild(li))

通過比較,我們可以很直觀的發現在一些特定場景中聲明式編程會比命令式編程要簡潔很多。

除了 HTML 之外,XML、CSS、SQL 和 Prolog 等也都是聲明式編程語言。而對於其它的通用編程語言而言,雖然語言本身大多屬於命令式語言,但在特定場景下依然可以使用聲明式編程的實踐。

Reactive Extensions

巨硬大法好!

Reactive Extensions 是微軟(研究院)提出的一個函數響應式編程抽象,最早用於 .Net 中(位於 System.Reactive 命名空間下但不在標準庫中),之後也被大量移植到其它語言,比如我們這裏到 RxJS 就是 Rx 的 JavaScript 移植版本。

雖然語言不同,但 Rx 的核心思想以及主要 API 仍然是通用的,所以我們這裏的內容同樣適用於 RxWhatever。

爲了解釋函數響應式,我們先來簡單看一看函數式編程。例如在 lodash 中,我們可以使用方法鏈的方式來實現複雜操作:

interface Person { name: string, age: number }
declare const people: Person[]
_(people).filter(person => person.age >= 18)
.forEach(person => console.log(`${person.name} is an adult.`))

對於一般的函數式編程而言,我們會對數據的某種集合容器(Array、Map、Set 等等)進行組合與變換,而在 Rx 中,我們處理的是一類特殊的數據集合叫做 Observable,可以看作一個事件流。

現在,由於每一個數據項都是一個事件,我們並不能在一開始就獲得所有的事件,並且事件具體在什麼時候產生也無從得知。

一個很簡單的例子,我們仍然需要處理上面的 person,但 person 不是同時獲取的,而是通過用戶交互動態創建的。當然,我們也可以把所有操作都寫在回調函數裏,但那樣會造成極高的耦合度,破壞代碼質量。

所以,我們可以把這裏 person 的創建做成一個事件流,並不去關心在什麼地方會用到,以及怎樣用到。

class SomeComponent {
people: Subject<Person>
onCreate(person: Person) { people.next(person) }
}

而對於真正的調用方,只要獲取對應的 Observable,然後進行後續操作。

const people$ = getPeopleObservableSomehow()
people$.filter(person => person.age >= 18)
.subscribe(person => console.log(`${person.name} is an adult.`))

如果我們還有其它的數據源,比如初始數據從服務端獲取,之後的數據才通過交互產生,我們可以進行一次組合:

const initialPeople = getPeopleSomehow()
const people$ = getPeopleObservableSomehow()
const allPeople$ = Observable.from(initialPeople).mergeMap(people$) //組合
allPeople$.filter(person => person.age >= 18).subscribe(person => console.log(`${person.name} is an adult.`))

這樣,可以讓我們的代碼具有較高的複用性,同時又能極大地降低了耦合性,提高代碼質量。

此外,函數響應式編程和對應的命令式編程對應的一大區別就是,函數響應式編程是 Push-based,而命令式編程(通常)是 Pull-based。也就是說,在函數響應式編程中我們並不會有取值這個操作(不會通過賦值來獲取數據)。

與 Promise 的聯繫

我們已經知道,Promise 的一大特性就是組合與變換,那麼 Promise 和 Observable 之間有什麼聯繫呢?

事實上,我們確實也可以把 Observable 看成一個有可變數據量的 Promise,而 Promise 只能包含一個數據。

如果看成狀態機的話,那麼 Promise 具有 3 個狀態:pending、resolved、rejected(如果 Cancelable Promise 正式通過,那麼還會增加一個狀態)。而 Observable 有 N + 3 個狀態:idle、pending、resolved_0、resolved_1 … resolved_N、completed 和 error。

因此,相比於 Promise 這個有限狀態機而言,Observable 既可能是有限狀態機,也可能是無限狀態機(N 爲無窮)。並且 Observable 還具有可訂閱性,對於 Cold Observable 而言,只有訂閱後纔開始起作用,而 Promise 一經產生便開始起作用。

Observable 可以分爲 Cold Observable 和 Hot Observable,Cold Observable 只有被訂閱後纔開始產生事件,比如封裝了 Http 的 Observable,而 Hot Observable 始終產生事件,比如封裝了某元素 Click 事件的 Observable。

此外,由於 Promise 僅有一個數據,故數據被獲取即爲 Promise 完成,僅需要一個狀態。而對於 Observable,由於可以有任意多個數據,因此需要一個額外的狀態來表示完成,一經完成後便不能再產生數據。

在當前的 RxJS 實現中,我們可以通過 .toPromise 運算符來將 Observable 轉換爲 Promise。

const aPromise = anObservable.toPromise()

如何使用循環語法

我們知道,通過回調函數實現的迭代行爲也能通過循環來實現(比如 for…of 和 Array#forEach),那麼 Observable 也能使用循環的方式使用嗎?

雖然大部分時候並不推薦,但是這也是完全可行的。比如我們這裏可以簡單地爲 Observable 實現 Iterable:

Observable[Symbol.iterator] = function () {
let current = null
let resolveCurrent = null
let rejectCurrent = null
let isDone = false
function update() {
current = new Promise((resolve, reject) => {
resolveCurrent = resolve rejectCurrent = reject
})
}
update()
this.subscribe(
item => { resolveCurrent(item) update() },
error => { rejectCurrent(error) update() },
() => { isDone = true })
const next = () => ({ done: isDone, value: isDone ? null : current })
return { next }
}

這樣(僅供表意的實現版本,並沒有經過嚴格驗證,請勿直接用於實際項目中),我們就能夠通過 for…of 循環的方式來使用:

for (let itemPromise of someObservable) {
let item = await itemPromise // do some thing with item
}

不過,這樣我們仍然需要在循環體中使用 await,並不美觀(其實已經比較美觀了,只是爲了引出下文這麼說),爲此我們可以更進一步,直接實現 Async Iterable(目前仍然是 Stage 3 的 提案 ,有望進入 ES2017 中):

Observable[Symbol.asyncIterator] = function () {
// 不詳細寫了,直接在 next 中返回 { done, value } 的 Promise 即可。
}

之後我們就能以更爲簡單的方式來循環了:

for await (let item of someObservable) {
// do some thing with item
}

運算符

對於 Promise 而言,由於有且只有一個數據,所以無需複雜的操作,僅需要一個簡單的變換(返回值)或者組合(返回另一個 Promise)功能即可,甚至還可以把組合變換與使用統一爲一個操作,也就是我們的 .then。

而對於 Observable,由於可以有任意多個數據,爲了使用上的方便,提供了很多運算符,用來簡化用戶代碼(可以參考 Array)。

同理,我們也無法簡單地使用一個方法來實現全部操作:

  • 對於變換,(最簡單的方式)需要使用 .map 方法,用來把 Observable 中的某個元素轉換成另一種形式;

  • 對於組合,(最簡單的方式)需要使用 .mergeMap 方法,用來把兩個 Observable 整合爲一個 Observable;

  • 對於使用,我們需要使用 .subscribe 方法,用來通知 Observer 我們需要它開始工作。

其它的運算符也不外乎是 變換 或者 組合 的某種特定操作,在理解了 Observable 的基本原理下,仔細閱讀文檔和對應的點線圖就能很快了解某個運算符了。

當然還可能有另一類運算符,比如 .toPromise 等,這些並不返回 Observable 的方法其實本身並不是一個運算符,僅僅是對 Observable 的原型擴展。

Angular 2 中的 Rx

在 Angular 2 中,其實必然會用到 Observable,因爲在 @Output 對應的 EventEmitter 實際上就是一個 Subject(同時爲 Observable 和 Observer),不過雖然 Angular 2 使用了,這並沒有和我們的代碼產生聯繫,所以如果不想使用 Rx,也可以完全無視它。

另一個使用了 Rx 的地方是 Http 模塊,其中 Observable 作爲大部分 API 的交互對象使用。當然,Http 本身就不是必須的,僅僅是一個官方的外部擴展,相比於其它第三方的庫而言也沒有任何實現上的特殊性(當然可能 API 設計上更爲統一)。所以如果仍然不想使用 Observable,可以使用 .toPromise 的方式轉換爲 Promise 來使用,或者使用第三方的 Http Client 比如 SuperAgent 等,還可以直接使用 Fetch API。由於 Zone.js 的存在,並不需要對 Angular 進行對應封裝。

其實在 Router 模塊中也使用了 Rx,比如 ActivatedRoute 中的一些 API 都是以 Observable 的方式交互,用來實現動態響應。當然,我們也可以只使用 ActivatedRouteSnapshot ,這樣就可以直接處理數據本身。同樣,Router 模塊也同樣沒有任何實現特殊性,如果我們願意,我們也可以使用 UI-Router 的 Angular 2 版本。

不過從上面也可以看出 Angular 官方在很大程度上是推薦使用 Rx 的,並且在 API 設計上大量應用了 Rx 來簡化複雜度,如果確實想要使用 Angular 2 進行團隊項目開發,瞭解一些 Rx 的知識還是很有意義的。相比之下,要完全不使用 Rx 可能反而需要更高的學習成本。

總結

Rx 的知識在 Angular 2 中並不必須,主要用在 API 交互上,之所以推薦學習並不只是爲了 Angular 2 的使用,函數響應式編程實踐本身也很有價值。

理解了 Iterable 和 AsyncIterable 之後,也可能把 Observable 看成一個 AsyncIterable 的特例,也就是一個異步的迭代容器。

Rx 提供了大量的運算符,用於對 Observable 的組合與變換。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章