RxJS 之於異步,就像 JQuery 之於 do

記得當年我剛學 JavaScript 的時候,是從原生的 dom api 學起的,用原生的 dom api 完成一些增刪改的功能,之後就會學習 JQuery。

剛接觸 JQuery 的時候,感覺這也太爽了吧。比如這樣一段邏輯:

創建一個 p 標籤包含一個文本節點,然後插入到 container 中。

用原生 dom api 寫是這樣的:

const containerDom = document.getElementById('container');

const textDom = document.createTextNode("Hello World.");
const paraDom = document.createElement("p");
paraDom.appendChild(textDom);

containerDom.appendChild(paraDom);

而用 JQuery 寫是這樣的:

const $container = $('#container');

$container.append('<p>Hello World.</p>');

比起用原生 dom api 來寫簡化太多了!這也是爲什麼 JQuery 當年那麼火的原因。

雖然現在都用 Vue、React 這種數據驅動的前端框架來寫頁面,基本不直接操作 dom 了。但涉及到一些活動頁等需要直接操作 dom 的場景,用 JQuery 依然很方便。

那 JQuery 做了什麼呢?

JQuery 把 dom 封裝了一層,提供了很多操作 dom 的 api,並且支持鏈式調用,可以方便的組織 dom 操作邏輯,而且還支持插件來自定義一些方法在鏈式調用中使用。

可能你會說,JQuery 不是基本用不到了麼,提它幹什麼?

因爲我覺得 JQuery 對 dom 操作的這層封裝很好,把操作 dom 的複雜度降低了很多。前端除了經常操作 dom 外,還會經常處理異步,比如 XHR 和 Fetch、Event Listener 等,雖然可以用 Promise 封裝,還可以進一步簡化成 async/await 的寫法,但是 Promise 和 async/await  只是改變了異步邏輯的書寫形式,並沒有降低異步邏輯編寫的複雜度。 能不能就像 JQuery 對 dom 操作的封裝那樣,把異步邏輯也給封裝一層,簡化下異步邏輯的編寫呢?

確實有這樣的一個庫,就是 Rx.js。

寫 JQuery 的時候我們是把 dom 封裝了一層,比如 const $container = $(dom),這樣就能用 JQuery 內置的工具函數或者通過插件擴展的一些函數,通過鏈式調用把邏輯串起來。

爲了和 dom 對象容易區分,我們會把 JQuery 對象命名成 $、$yy 等。

那麼 Rx.js 第一步要做的也是把異步邏輯包一層:

也就是把 Event Listener、Promise、回調函數這種異步代碼包一層:

// 包一層 Event Listener
const observable$ = Rx.Observable.fromEvent(document.querySelector('button'), 'click'); 

// 包一層 Promise
const observable2$ = Rx.Observable.fromPromise(fetch('/users'));

// 包一層 callback
const observeable3$ = Rx.Observable.bindCallback(fs.exists);

包裝以後的對象不叫 RxJS 對象,叫做 Observable 對象,而且爲了便於區分,我們會把它命名爲 xxx$、yyy$,就像 JQuery 對象我們會命名成 $xxx、$yyy 一樣。

然後就可以用內置的一系列工具函數了,這些叫做操作符 operator:

observable$.pipe(
    throttleTime(300),
    take(3),
    map(event => event.target.value)
);

比如異步邏輯我們經常做節流處理,那就不用自己寫了,直接用內置的操作符 throttleTime 就行。

還有忽略前三次事件 take(3),對數據做一次映射 map(() => xxx) 等等這些常見異步邏輯用操作符來寫就很簡單。

把異步邏輯組織成鏈條(或者叫管道 pipe),用操作符來寫每步的處理邏輯,然後串聯起來,這樣就把異步邏輯的書寫變爲了 pipe 的組織。而且就像 JQuery 可以寫插件來擴展一樣,Rxjs 也支持自定義操作符。

經過這個管道之後,數據經過了每一步異步邏輯的處理,我們可以通過 subcribe 監聽,拿到最終的數據。

observerable$.subscribe((value) => {
    // xxx
})

當然,也可能在處理的過程中出錯了,那也要把 error 傳下去,並且最終處理完以後也會有個通知,所以可以寫這樣三種情況的處理:

observerable$.subscribe({
    next: (v) => {},
    error: (err) =>{},
    complete: () => {}
});

這些處理邏輯叫做 Observer。

這就是 RxJs 做的事情了。因爲異步邏輯是對某個事件的響應,這也叫做響應式編程

剛纔我們創建 Observable 是包了一層 Event Listener、callback、Promise,當然也可以直接創建這樣一個 Observable 對象:

比如我們把一系列數封裝成 Observable:

// 多個數據
const observable$ = Rx.Observable.of(1, 2, 3); 

// 數組中的多個數據
const observable2$ = Rx.Observable.from([1,2,3]);

或者經過一些邏輯邏輯產生一系列數據:

var observable$ = new Rx.Observable(function (observer) {
    observer.next(1);
    observer.next(2);
    observer.next(3);
    setTimeout(() => {
        observer.next(4);
        observer.complete();
    }, 1000);
});

或者這樣:

const observable$ = new Rx.Subject();
 
observable$.next(1);
observable$.next(2);

這裏的區別是 Subject 是可以在外部調用 next 來產生數據的,而 new Observable 是在回調函數內調用 next 產生數據。

我們小結一下:

就像 JQuery 對 dom 包了一層,然後提供了簡化 dom 操作的 api 一樣,RxJS 也對異步邏輯包裝了一層,然後提供了一系列 operator。我們可以把 EventListenr、Promise、callback 等包裝成 Observable(或者自己用 of、from、Subject 等創建 Observable),然後用內置的或者自己擴展的 oprator 組織處理管道,在管道的末尾用 Observer 接受數據、處理錯誤。這樣就把異步邏輯的編寫,轉變爲了操作符管道的組織。當對內置的 operator 足夠熟練或者自己沉澱了一些 operator 之後,寫異步的邏輯速度會變得很快。

因爲 RxJS 只是對異步邏輯的封裝,和 Vue、React 等前端框架並不衝突,所以可以很好的結合在一起。(Angular 甚至默認就集成了 RxJS)

比如在 Vue 裏面,我們可以把事件用 Subject 封裝成一個 Observable,然後就可以用 RxJS 的操作符來組織異步邏輯了:

<div @click="clickHandler">點我</div>
import { Subject } from 'rxjs'
import { debounceTime } from 'rxjs/operators'

export default {
   data() {
     return {
       observable$: new Subject()
     }
   },
   created() {
      this.observable$.pipe(debounceTime(500)).subscribe((event) => {
        // xxx
      })
   },
   methods: {
      clickHandler(event) {
         this.observable$.next(event)
      }
   }
}

在 React 裏面也一樣,用 Subject 自己創建個 Observale,就可以把異步邏輯的編寫轉變爲 operator 的組裝了:

class MyButton extends React.Component {
    constructor(props) {
       super(props);
       this.state = { count: 0 };

       this.observable$ = new Rx.Subject();
        
       this.observable$.pipe(
           debounceTime(500),
           map(() => 1),
           scan((total, num) => total + num, 0)
       );

       this.observable$.subscribe(x => {
          this.setState({ count: x })
       })
    }
    render() {
        return <button onClick={event => this.observable$.next(event)}>{
            this.state.count
        }</button>
    }
}

我們用 Subject 創建了個 Observable 對象,每次點擊都調用 next 產生一個數據,傳入處理管道。

管道我們是用 operator 組織的,先做了 500ms 的截流,然後把值變爲 1,之後計數。

處理完之後傳遞給 Observer 的就是累加後的數值,設置到 state 即可。

這樣一段節流 + 計數的異步邏輯就寫完了,其實就是組裝了下 operator,這就是 RxJS 的意義。

總結

用原生的 dom api 進行 dom 操作比較繁瑣,所以我們會使用 JQuery,它把 dom 包了一層,提供了很多方便的內置 api,而且還支持通過插件擴展,這樣極大的簡化了 dom 操作。

除了操作 dom,前端開發還經常要寫異步邏輯,同樣也需要這樣一個包一層的庫來簡化,它就是 Rx.js。

Rx.js 把 Event Listener、Promise、callback 等封裝成了 Observable(也可以自己創建),提供了很多操作符 operator(還可以自定義),用它們來組裝成處理管道(pipe)來處理異步邏輯,最後傳入 Observer 來接收數據和處理錯誤。這樣把異步邏輯的編寫轉變爲了 operator 的組裝,把填空題變爲了選擇題,異步邏輯的編寫速度和體驗自然會提升很多。

而且,RxJS 是專門處理異步邏輯的,可以和前端框架很好的結合在一起使用。

就像用 JQuery 操作 dom 很爽一樣,熟悉了 RxJS 的 operator,用 RxJS 編寫(組裝)異步邏輯的體驗也非常棒。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/S7qVcxBUE2PXB43Hl6ZFLg