Rxjs光速入門

lhyt發表於2018-09-17

0. 前言

本文基於5.5.11版本!

本文基於5.5.11版本!

本文基於5.5.11版本!

Rx指的是響應式程式設計的實踐工具擴充套件——reactive extension,程式設計風格是響應式程式設計+函數語言程式設計。Rxjs則是這種模式的js的實現,處理非同步能力優秀,將非同步操作抽象為時間軸上的點。既可以當作像lodash那樣的工具庫來用,也可以用來統一管理資料流,他的出現解決了一些問題:

  • 簡化了程式碼
  • 簡短且具有良好的可讀性
  • 很好的處理非同步

文件看這裡

1. Observable

Rxjs核心概念就是Observable,一個可觀察物件,代表著接下來將要發生的一系列事件

Rx.Observable.create(observer => {
        observer.next(1);
        observer.next(2);
        observer.next(3);
    })  // 資料來源
    .map(x => x * 2)  // 操作符
    .subscribe(x => {console.log(x)})  // 訂閱資料
複製程式碼

Observable作為資料來源產生資料,通過內部迭代器next一個個地產生資料,observer被動接受資料,經過一系列操作符處理,在下游用subscribe訂閱資料來源最終結果進行操作。每次subscribe,create裡面的observer就會呼叫一次

2. 產生資料來源

Observable.create:最原始的建立資料流的方法,其他方法其實是基於此方法的封裝,一般用其他的都可以滿足各種場景。每次最後subscribe都會執行一次create傳入的函式

Rx.Observable.create(observer => {
    observer.next(1);
    observer.next(2);
    observer.next(3);
    observer.error('err');
    observer.complete();
    observer.next(4); // 完成後不再監聽error或者next
}).subscribe(console.log,
 err => {console.log('err', err)},
  () => {console.log('complete')})
複製程式碼

建立同步資料流的基礎方法of比較常用,還有其他的各種功能的產生資料來源的方法如:repeat、generate、range、never、throw等(cold observable)

非同步資料流常用方法:interval、timer、fromPromise、fromEvent、ajax等 (後面三者是hot observable)

3. Hot & Cold Observable

  • cold:subscribe後接受的是Observable產生過的所有的資料
  • hot:subscribe後接受的是Observable被subscribe後產生的資料,之前的不算
// cold
Rx.Observable.create(observer => {
    const producer = new Producer()
    // observer與producer關聯起來
})


// hot
const producer = new Producer()
Rx.Observable.create(observer => {
    // observer與producer關聯起來
})
複製程式碼

每一次被subscribe,會觸發Rx.Observable.create(observer)裡面的observer函式。cold型別的是每一次都是一個新的生產者,所以它會把所有的資料都訂閱。而hot型別是共享同一個生產者,所以只是訂閱以後的資料

來個例子:

先來一個生產者類:

class Producer {
    constructor(init) {
        this.num = init
    }
   connect(observer) {
        this.observer = observer
        return this
    }
    add() {
        setInterval(() => {
            this.num += 1
            this.observer && this.observer.next(this.num)
        }, 1000)
        return this
    }
}
複製程式碼

hot:

const p = new Producer(10)
const producing = p.add()
const ob = Rx.Observable.create(observer => {
    producing.connect(observer)
})

setTimeout(() => {
    ob.subscribe(x => console.log('p1',x))  
}, 1000);
setTimeout(() => {
    ob.subscribe(x => console.log('p2',x))  
}, 3000);
setTimeout(() => {
    ob.subscribe(x => console.log('p3',x))  
}, 5000);
複製程式碼

cold:

const ob = Rx.Observable.create(observer => {
    const p = new Producer(10)
    const producing = p.add()
    producing.connect(observer)
})

setTimeout(() => {
    ob.subscribe(x => console.log('p1',x))  
}, 1000);
setTimeout(() => {
    ob.subscribe(x => console.log('p2',x))  
}, 3000);
setTimeout(() => {
    ob.subscribe(x => console.log('p3',x))  
}, 5000);
複製程式碼

cold型別的,所有的訂閱者都會從頭到尾接收到所有的資料(每一次訂閱都new一個生產者);而hot型別只接受訂閱後的產生的資料(所有的訂閱共享生產者)

5. 操作符

一個Observable物件代表一個資料流,對於實際應用上的一些複雜的問題,我們當然不直接subscribe資料流,而是先讓它經過一系列處理再subscribe。這個一系列的處理就是通過操作符來處理

image

  • 接受上游的資料,經過處理流到下游
  • 來自上游可能是源頭、可能是其他操作符甚至其他流
  • 返回的是新的Observable,整個過程鏈式呼叫

操作符的實現

  • 鏈式呼叫:返回this、返回同類例項
  • 函數語言程式設計:純函式、無副作用 那麼很容易推理出來,底層實現是返回新的Observable物件,而rx世界中一切產生資料來源的方法都是基於create封裝,操作符返回的物件還具有subscribe方法。
Rx.Observable.myof = function(...args) {
  return new Rx.Observable.create(observer => {
    args.forEach(arg => {
      observer.next(arg)
    })
  })
}

Rx.Observable.prototype.mymap = function(fn) {
  return new Rx.Observable(observer => {
    this.subscribe({
      next: x => observer.next(fn(x))
    })
  })
}

Rx.Observable.myof(1,2,3).mymap(x => x*2).subscribe(console.log)

複製程式碼

6. 彈珠圖

用彈珠圖看rx的資料流,特別形象而且容易理解,下面看一下例子:

const source1$ = Rx.Observable.interval(500).map(x => 'source1: ' + x).take(5)
const source2$ = Rx.Observable.interval(1000).map(x => 'source2: ' + x).take(5)
const source3$ = Rx.Observable.of(1, 2, 3)
source1$.merge(source2$).concat(source3$).subscribe(console.log)
複製程式碼

merge是將兩個資料流按時間軸順序合併起來,concat是把資料流連線到前面一個資料流後面(不管時間軸順序)

image

很顯而易見,輸出結果是0012314234, 123

7. Subject

在Rxjs中,有一個Subject型別,它具有Observer和Observable的功能,不僅可以使用操作符,還可以使用next、error、complete,但是本身不是操作符

// 看了前面的描述,那麼我們用的時候想產生資料來源,很容易就會想到這樣的方法:
let obs;
Rx.Observable.create(observer => {
    obs = observer
}).subscribe(console.log)
obs.next(123)
複製程式碼

但是,說好的函數語言程式設計,不能有副作用,是純函式,因此需要subject了

const subject = new Rx.Subject()
subject.map(x => x * 2).subscribe(console.log)
subject.next(1)
subject.next(2)
subject.complete()
複製程式碼

但是subject擅長於連線的特性,更重要的是用來做多播(一個物件被多個物件訂閱):

const source$ = Rx.Observable.interval(1000).take(3);// 從0開始每秒輸出一個數,輸出三個
source$.subscribe(x => {console.log('source1', x)})
setTimeout(() => {
    source$.subscribe(x => {console.log('source2', x)})
}, 1100);
複製程式碼

那麼,問題來了,下面的輸出結果是:

const source$ = Rx.Observable.interval(1000).take(3);// 從0開始每秒輸出一個數,輸出三個
source$.subscribe(x => {console.log('source1', x)})
setTimeout(() => {
    source$.subscribe(x => {console.log('source2', x)})
}, 1100);
複製程式碼

"source1先列印0,一秒後source1和2都列印1,再一秒後都列印3"

"恭喜答錯了。interval產生cold observable,資料來源來自外部的才是hot(幾個Fromxx的都是hot型別的),一對多的多播當然是要hot observable的,cold的訂閱一次就從新的Observable開始了。"

實際上答案應該是source1先列印0,後面兩秒source1和2分別列印10、21,最後source2列印2。那麼要實現上面那個理想的答案,應該用上subject。因為有一個關鍵點,subject狀態唯一而統一,被自身例項subject.complete過後,再次subject.next也是無法被subscribe了。

我們利用一下subject就可以優雅而且不違反函數語言程式設計規則來實現這個功能:

const source$ = new Rx.Subject();
let i = 0;
const time = setInterval(() => {
    if (i === 2) {
        clearInterval(time)
    }
    source$.next(i ++)
}, 1000)
source$.subscribe(x => {console.log('s1', x)})
setTimeout(() => {
    source$.subscribe(x => {console.log('s2', x)})
}, 1100);

複製程式碼

當然,我們還沒發揮Rxjs的api作用,我們還可以用multicast來連線subject例項

const source$ = Rx.Observable.interval(1000).take(3).multicast(new Rx.Subject());
source$.subscribe(x => {console.log('source1', x)})
setTimeout(() => {
    source$.subscribe(x => {console.log('source2', x)})
}, 1100);
source$.connect() //需要手動呼叫, 不然前面程式碼不會有結果
// 這才是source1先列印0,一秒後source1和2都列印1,再一秒後都列印3的情況
複製程式碼

總結

知識點:
  1. Observable.create(observer => {})是建立資料流基礎方法,裡面的observer有next、error方法吐出資料,complete方法表示完成整個過程(相當於empty操作符),當complete後,這個observer吐出的資料再也不能被下游subscribe到。每一次被subscribecreate裡面的函式都會呼叫一次
  2. hot Observable是隻訂閱subscribe後的資料,cold Observable訂閱這個Observable從頭到尾產生過的資料。這是因為hot共享生產者,cold的是每一次subscribe都是一個新的生產者
  3. Subject具有Observable和observer的功能,所以我們就不用違反函數語言程式設計的規則從外面拿到observer物件操作next了,可以直接用Subject的例項
  4. 看文件,看各種操作符,如何鏈式呼叫,畫彈珠圖理解,你懂的
優點和特點
  • Rxjs以Observable為核心,全程通過釋出訂閱模式實現訂閱Observable的變化進行一系列操作
  • 函式式+響應式程式設計,中間的操作符鏈式操作由next迭代器模式實現,並且由於是純函式所以每一次返回一 個新的Observable例項
  • 在某些程度,可以單純拿出Observable一套當作像lodash、underscore這種工具庫使用
  • Rxjs將所有的非同步和同步資料流抽象成放在時間軸上處理的資料點,可以通過彈珠圖清晰理解整個資料流過程,處理非同步的能力優秀
  • 每一個資料流經過各種操作符操作,多個資料流協同、合併、連線,使得整個Rxjs應用在顯得流程清晰
缺點:
  1. api較多,學習成本高,比較抽象
  2. 程式碼簡潔以鏈式操作為主,維護性不如傳統的物件導向+模組化
  3. 庫比較龐大,簡單問題需要引入一系列api使得專案檔案體積變大,就算按需引入也比其他庫大

相關文章