RxJS進階——關於流的理解和應用

genetalks_大資料發表於2019-04-01

RxJS是微軟公司推出的響應式程式設計的JavaScript庫。 對於它的學習,最開始我的理解是把它當成是 能優雅地解決非同步問題的lodash。 隨著學習的深入,發現它採用了訂閱者模式,其中也帶有純函式的思想。 直到在使用了RxJS 6之後才瞭解其少有人意識到的另一面——流。

什麼是流?這裡我們不用專業術語來解釋,用生活中大家熟悉的的例子來類比,比如“河流”。

河流有什麼特點? 至少有兩個特點:

有朝向。 水往低處流,河流雖然可能會蜿蜒盤旋,但是朝向是固定的,比如我國的長江和黃河就都是由西往東流。 在RxJS中資料的流向也是固定的,就是從傳送者到訂閱者。基本都如下面這種形式:

from(Promise.resolve(1)) // 流的源頭
......
.subscribe(x => console.log(x)); // 流的終點
複製程式碼

有分支。

大的河流一般有幹流和支流,大大小小的支流匯入幹流。

RxJS中的資料則可以通過操作符將資料流進行聚合或拆分。

// 流的聚合
mergeMap(from(Promise.resolve(1)), from(Promise.resolve(2)))
......
.subscribe(x => console.log(x))

// 流的拆分
const obs$ = from(Promise.resolve(1)
obs$.subscribe(x => console.log(x))
obs$.subscribe(x => {
  // do sth
})/
複製程式碼

RxJS 6 相對於 RxJS 5(這裡指5.5以下的版本,因為pipe函式在RxJS 5.5中作為新特性已被引入。) 來說不僅修改了一部分操作符的名稱,同時做了一個較大的改動,引入了管道(pipe)。這個改動到底有多大?

首先是寫法上的變化。 RxJS 5的這種操作符的呼叫方式有沒有一種似曾相識的感覺? 是的,它看上去很像JQuery那種義大利麵條式的鏈式呼叫 而RxJS 6和Gulp的寫法有些像,想想Gulp是什麼?基於流的構建工具!

// RxJS 5 虛擬碼
myObservable
  .map(data => data * 2)
  .switchMap(...)
  .throttle(...))
  .subscribe(...);

// RxJS 6 虛擬碼
myObservable
  .pipe(map(data => data * 2), switchMap(...), throttle(...))
  .subscribe(...);
複製程式碼

這種寫法上的變化就帶來了用法上的變化,以前的固定“河流”可以通過“管道”(pipe)來控制形成靈活的“水流”。

下面舉個例子來更加形象地闡述加入管道之後流的靈活性。

現在有一個這樣的業務場景: 點選按鈕之後傳送一個請求,讓服務端開始執行任務,然後輪詢傳送請求查詢任務執行狀態,根據不同狀態進行不同操作。有3種狀態"controlling"——繼續輪詢, "stop"——停止輪詢,"finish"——停止輪詢,並進行後續操作。

不考慮判斷條件,虛擬碼是下面這樣子:

// 開始任務
start$().pipe(
  switchMap(() => interval(1000)), // 開始輪詢
  switchMap(() => getStatus$()), // 查詢狀態
  )
.subscribe(x => {
  // 後續操作
})
複製程式碼

這段程式碼有一個問題沒有解決,根據狀態進行相應操作。 先來看看這3種狀態對應的操作。

  • controlling。繼續輪詢很好處理,不進行任何操作即可。
  • stop。停止輪詢的操作符有3個:take,需要固定次數,這個次數沒法預先確定。takeUntil,需要建立一個額外的subject來進行停止,應該可以實現,不過程式碼量比較大。takeWhile,只需簡單的邏輯判斷即可,比較合適。
  • finish。問題來了,如過我們在管道操作符中判斷狀態的並停止流的話,那麼訂閱者將無法收到訊息,意味著後續操作無法執行。

解決方法就是把後續操作放到管道中。程式碼如下:

// 開始任務
start$().pipe(
  switchMap(() => interval(1000)), // 開始輪詢
  switchMap(() => getStatus$()), // 查詢狀態
  filter(x => x==='stop' || x==='finish') // 'controlling'狀態下繼續輪詢,其它狀態進行對應操作
  takeWhile(x => x!=='stop') // 當為'stop'時結束輪詢
  tap(() => {
    // 後續操作
  })
  takeWhile(() => false) // 操作完成結束輪詢
  )
.subscribe();
複製程式碼

現在需求變化了,在另一段程式碼中,我們也要通過查詢狀態並根據狀態進行,但是不再需要開始任務和輪詢了。 那麼上面的程式碼查詢和操作部分可以利用pipe方法抽取出來。

const handle = pipe(
  switchMap(() => getStatus$()), // 查詢狀態
  filter(x => x==='stop' || x==='finish') // 'controlling'狀態下繼續輪詢,其它狀態進行對應操作
  takeWhile(x => x!=='stop') // 當為'stop'時結束輪詢
  tap(() => {
    // 後續操作
  })
  takeWhile(() => false) // 操作完成結束輪詢
)
start$().pipe(
  switchMap(() => interval(1000)), // 開始輪詢
  handle
).subscribe();
// 直接複用查詢狀態程式碼和後續操作部分程式碼
other.pipe(
  handle
).subscribe()
複製程式碼

總結一下。RxJS比較完整的理解應該是基於流的訂閱者模式,而流的靈活性體現在可拆分和聚合,有了pipe管道的加入,流的可複用性增強,因此更容易對程式碼邏輯進行抽象。

原文連結:tech.gtxlab.com/rxjs-stream…


作者資訊:朱德龍,人和未來高階前端工程師。

相關文章