讓我們一起來學習 RxJS

aezi3943 8年前發布 | 63K 次閱讀 RxJS JavaScript開發

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 PatternIterator 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 邊實現一些小東西的話:

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

 

來自:https://fe.ele.me/let-us-learn-rxjs/

 

 本文由用戶 aezi3943 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!