讓我們一起來學習 RxJS
What Is RxJS
通過 閱讀 官方文檔 ,不難得出:RxJS 可以很好 解決異步和事件組合的問題 。
這個時候我就有疑問了,異步問題不是用 Promise ( async/await ) 就好了嗎?
至于事件,配合框架 ( React, Vue, Angular2 等 ) 的話不也很容易解決嗎?
不管怎樣, 讓我們先看個 Hello World 吧。( 我要看 DEMO )
Rx's Hello World
// auto-complete
const Observable = Rx.Observable
const input = document.querySelector('input')
const search$ = Observable.fromEvent(input, 'input')
.map(e => e.target.value)
.filter(value => value.length >= 1)
.throttleTime(100)
.distinctUntilChanged()
.switchMap(term => Observable.fromPromise(wikiIt(term)))
.subscribe(
x => renderSearchResult(x),
err => console.error(err)
)</code></pre>
上面的代碼做了以下事情:
-
監聽input元素的input事件
-
一旦發生,把事件對象e映射成input元素的值
-
接著過濾掉值長度小于1的
-
并且還設置了一個throttle( 節流器 ),兩次輸入間隔不超過 100 毫秒為有效輸入
-
如果該值和過去最新的值相等的話,忽略他
-
最后,拿到值便調用Wikipedia的一個API
-
最后的最后,需要subscribe才能拿到API返回的數據
是不是看起來就覺得很cool,好想學!
短短幾行代碼就完成了一個 auto-complete 組件。
How It Works
那上面的代碼是什么意思?
RxJS 到底是如何工作的?如何解決異步組合問題的?
Observable
Rx 提供了一種叫 Observable 的數據類型,兼容 ECMAScript 的 Observable Spec Proposal 草案標準。他是 Rx 最核心的數據類型,結合了 Observer Pattern , Iterator Pattern 。
那到底什么是 Observable ?
Observable 其實就是一個 異步的數組 。 ( ---> 2 minute introduction to rx )
不妨想像一下, 數組 + 時間軸 = Observable 。
數組元素的值是未來某個時間點 emit ( 產生 ) 的,但是我們并不關心這個時間點,因為利用了「觀察者模式」 subscribe ( 訂閱 ) 了這個數組,只要他 emit 了值,就會自動 push 給我們。
我們再用圖來表示一下的話:
--a---b-c--d-----e--|-->
這種圖叫做 marble diagram 。
我們可以把 ASCII 的 marble 圖轉成 SVG 的: ASCII -> SVG 。
- 表示時間軸, a ~ e 表示 emit 的值, | 則表示這個 stream 已經結束了。
比方說, click 事件用上圖來表示: a 表示第 1 次點擊, b 表示第 2 次點擊,如此類推。
如果你覺得 Observable 這個名字不夠形象不夠 cool 的話,你可把他叫做 stream ,因為他的 marble 圖就像 steam 一樣。所以啊,下面我都會把 Observable 稱作 stream 。
Operators
那么,我們怎么對 stream 進行操作呢?怎么把多個 stream 組合在一起呢?
我們前面不是說了「 Observable 其實就是 異步數組 」嗎?在 JavaScript 里的數組不是有很多內置的方法嗎?比如 map , filter , reduce 等等。類似地,Observable 也有自己的方法,也就是所謂的 operator 。比如上面例子中的 map , filter , throttleTime , distinctUntilChanged 等等很多很有用的 operator 。
面對 RxJS 那么多 operator ,我們要怎么學習呢?很簡單:
分類別 + 畫 marble 圖 + 看例子 + 選
現在,就讓我們畫出上面 Hello World 例子的 marble 圖。
const search$ = Observable.fromEvent(input, 'input')
.map(e => e.target.value)
.filter(value => value.length >= 1)
.throttleTime(100)
.distinctUntilChanged()
.switchMap(term => Observable.fromPromise(wikiIt(term)))
.subscribe(
x => renderSearchResult(x),
err => console.error(err)
)
假設輸入了 5 次,每次輸入的值一次為: a , ab , c , d , c ,并且第 3 次輸入的 c 和第 4 次的 d 的時間間隔少于 100ms :
---i--i---i-i-----i---|--> (input)
map
---a--a---c-d-----c---|-->
b
filter
---a--a---c-d-----c---|-->
b
throttleTime
---a--a---c-------c---|-->
b
distinctUntilChanged
---a--a---c----------|-->
b
switchMap
---x--y---z----------|--></code></pre>
如果我告訴你學習 RxJS 的捷徑是「學會看和畫 marble 圖」 ,你信還是不信?
Learn By Doing
現在,就讓我們結合上面的知識,來實現一個簡單的 canvas 畫板。
根據 canvas 的 API ,我們需要知道兩個點的坐標,這樣才能畫出一條線。
Step 1
( 我要看 DEMO )
那么,現在我們需要做的是 創建 一個關于鼠標移動的 stream 。于是,我們 去文檔找對應的 operator 類別 ,也就是 Creation Operators ,然后得到 fromEvent 。
const canvas = document.querySelector('canvas')
const move$ = Rx.Observable.fromEvent(canvas, 'mousemove')</code></pre>
對應的 marble 圖:
--m1---m1-m2--m3----m4---|--> (mousemove)
接著,我們需要拿到每次鼠標移動時的坐標。也就是說:需要 變換 stream 。
對應類別的 operator 文檔: Transformation Operators ---> map 。
const move$ = Rx.Observable.fromEvent(canvas, 'mousemove')
.map(e => ({ x: e.offsetX, y: e.offsetX }))
此時的 marble 圖:
--m1---m2-m3--m4----m5---|--> (mousemove)
map
--x1---x2-x3--x4----x5---|--> (點坐標)
然后,怎么拿到兩個點的坐標呢?我們需要再 變換 一下 stream 。
對應類別的 operator 文檔: Transformation Operators ---> bufferCount 。
const move$ = Rx.Observable.fromEvent(canvas, 'mousemove')
.map(e => ({ x: e.offsetX, y: e.offsetY }))
.bufferCount(2)
marble 圖:
--m1---m2-m3--m4----m5---|--> (mousemove)
map
--x1---x2-x3--x4----x5---|--> (點坐標)
bufferCount(2)
-------x1-----x3----x5---|---> (兩點坐標)
x2 x4</code></pre>
然而你會發現,此時畫出來的 線段是不連續的 。為什么?我也不知道!!
那就讓我們看看別人是怎么寫的吧: canvas paint 。
Step 2
( 先讓我要看看 DEMO )
換了一種思路,并沒有 變換 stream ,而是把兩個 stream 組合 在一起。
查看文檔 Combination Operators ---> zip 以及 Filtering Operators ---> skip
const move$ = Rx.Observable.fromEvent(canvas, 'mousemove')
.map(e => ({ x: e.offsetX, y: e.offsetY }))
const diff$ = move$
.zip(move$.skip(1), (first, sec) => ([ first, sec ]))</code></pre>
此時的 marble 圖:
--x1---x2-x3--x4----x5---|--> (move$)
skip(1)
-------x2-x3--x4----x5---|-->
--x1---x2-x3--x4----x5---|--> (move$)
-------x2-x3--x4----x5---|-->
zip
-------x1-x2--x3----x4---|--> (diff$)
x2 x3 x4 x5</code></pre>
這樣一來,diff$ emit 的值就依次為(x1, x2),(x2,x3),(x3,x4) …… 現在,鼠標移動的時候,就可以 畫出美麗的線條 。
Step 3
( 我想看 DEMO )
就在此時我恍然大悟,終于知道前面用 bufferCount 為什么不行了。我們不妨來比較一下:
-------x1-----x3----x5---|---> (bufferCount)
x2 x4
-------x1-x2--x3----x4---|--> (diff$)
x2 x3 x4 x5</code></pre>
bufferCount emit 的值依次為:(x1,x2),(x3,x4)…… x2和x3之間是有間隔的。這就是為什么線段會不連續的原因。
然后看 bufferCount 文檔的話,你會發現 可以使用 bufferCount(2,1) 實現同樣的效果。這樣的話,我們就不需要使用zip來組合兩個stream。Cool
const move$ = Rx.Observable.fromEvent(canvas, 'mousemove')
.map(e => ({ x: e.offsetX, y: e.offsetX }))
.bufferCount(2, 1)
此時的marble圖:
--m1---m2-m3--m4----m5---|--> (mousemove)
map
--x1---x2-x3--x4----x5---|--> (點坐標)
bufferCount(2, 1)
-------x1-x2--x3----x4---|---> (兩點坐標)
x2 x3 x4 x5</code></pre>
Step 4
( 我就要看 DEMO )
接下來,我們想實現「只有鼠標按下時,才能畫畫,否則不能」。
首先我們需要 創建 兩個關于鼠標動作的 stream 。
const down$ = Rx.Observable.fromEvent(canvas, 'mousedown')
const up$ = Rx.Observable.fromEvent(canvas, 'mouseup')
當鼠標按下的時候,我們需要把他 變換 成鼠標移動的 stream ,直到鼠標放開。
查看文檔 Transformation Operators ---> switchMapTo 。
down$.switchMapTo(move$)
此時的 marble 圖:
--d---d-d-----d---d--|--> (mousedown)
switchMapTo
--m---m-m-----m---m--|--></code></pre>
此時,鼠標放開了我們還能 繼續畫畫 ,這顯然不是我們想要的。這個時候我們很容易會使用 takeUntil 這個 operator ,但是這是不對的,因為他會把 stream complete 掉。
還是讓我們看看別人是怎么寫的吧: canvas paint 。
Step 5
( 我只想看 DEMO )
思路是這個樣子的:
把up$和down$組合成一個新的stream,但為了分辨他們,我們需要先把他們變換成新的stream 。
查看文檔 Combination Operators ---> merge 。
Transformation Operators ---> map 。
const down$ = Rx.Observable.fromEvent(canvas, 'mousedown')
.map(() => 'down')
const up$ = Rx.Observable.fromEvent(canvas, 'mouseup')
.map(() => 'up')
const upAndDown$ = up$.merge(down$)</code></pre>
再來看看他們的 marble 圖:
--d--d-d----d--d---|--> (down$)
----u---u-u------u-|--> (up$)
merge
--d-ud-du-u-d--d-u-|--> (upAndDown$)</code></pre>
此時,我們再 變換upAndDown$ 。如果是down的話,則變換成move$,否則變換成一個空的stream 。
查看文檔 Creation Operators ---> empty 。
Transformation Operators ---> switchMap 。
upAndDown$
.switchMap(action =>
action === 'down' ? move$ : Rx.Observable.empty()
)
你要的 marble 圖:
--d-ud-du-u-d--d-u-|--> (upAndDown$)
switchMap
--m-em-me-e-m--m-e-|--></code></pre>
其實這個canvas畫板不用RxJS實現也不會很難。但是當我們把他擴展成一個「你畫我猜」之后,用RxJS處理異步就會變得簡單起來。比如,添加新的工具欄(調色板,撤銷…… ) ,即時通信(同步畫板,聊天) ……
另外,如果你想邊學習 RxJS 邊實現一些小東西的話:
- staltz - rxjs training
- GitHub - Who to Follow
- RxJS 4.x Example
- RxJs Playground
- Yet Another RSS Reader
- rx-ifying a chat room built with reactjs and socket io
- angular2-hacknews
Production
怎么把RxJS應用到實際生產的web應用當中呢?
怎么結合到當前流行的框架當中呢?
Vue
你可以直接在各種 Lifecycle Hooks 中使用 RxJS 。
比如created的時候初始化一個Observable,beforeDestroy時就取消訂閱Observable。( 查看 DEMO )
new Vue({
el: '#app',
data: {
time: ''
},
created () {
this.timer$ = Rx.Observable.interval(1000)
.map(() => new Date())
.map(d => moment(d).format('hh:mm:ss'))
.subscribe(t => {
this.time = t
})
},
beforeDestroy () {
this.timer$.unsubscribe()
}
})</code></pre>
其實已經有對應的插件vue-rx幫我們干了上面的dirty work。他會分別在init和beforeDestroy的時候自動地訂閱和取消訂閱 Observable :Vue.js + RxJS binding mixin in 20 lines 。
因此,我們可以直接把一個Observable寫到data中:vue-rx/example.html 。
React
類似地,React也可以在他組件的lifecycle hooks里調用RxJS:fully-reactive-react。也可以使用rxjs-react-component把 Observable 綁定到 state 。 如果你結合 Redux 的話,可以使用這個 redux-oservable 。
Angular2
RxJS已經是Angular2的標配,不多說。
更多可查看對應的文檔Angular2 - Server Communication 。
更多關于RxJS的集成:RxJS community 。
You Might Not Need RxJS
根據 When to Use RxJS ,我們可以知道RxJS的適用場景是:
-
多個復雜的異步或者事件組合在一起
-
處理多個數據序列(有一定順序)
我覺得,如果你沒被異步問題困擾的話,那就不要使用RxJS吧,因為Promise已經能夠解決簡單的異步問題了。至于Promise和 Observable的區別是什么呢?可以看 Promise VS Observable 。
講真,RxJS 在實際生產中適用的業務場景有哪些?哪些場景是需要多個異步組合在一起的?游戲嗎?即時通信?還有一些特殊的業務。是我的寫的業務太少了嗎?還是我平時寫業務的時候,為寫而寫,沒有把他們抽象起來。
另外,我倒是對 Teambition 關于 RxJS 的思路有點感興趣: xufei - 數據的關聯計算 -> Brooooooklyn 評論 & xufei - 對當前單頁應用的技術棧思考 。
Summary
-
RxJS 是用來解決異步和事件組合問題
-
Observable = 異步數組 = 數組 + 時間軸 = stream
-
Operators = 分類別 + 畫 marble 圖 + 看例子 + 選
-
更多更詳細的更準確的請看 文檔 !
來自:https://fe.ele.me/let-us-learn-rxjs/