1. 程式人生 > >RXjs簡介(來自思否:segmentfault.com)

RXjs簡介(來自思否:segmentfault.com)

Introduction to RxJS

1. 前言

1.1 什麼是RxJS

RxJSReactiveX程式設計理念的JavaScript版本。ReactiveX來自微軟,它是一種針對非同步資料流的程式設計。簡單來說,它將一切資料,包括HTTP請求,DOM事件或者普通資料等包裝成流的形式,然後用強大豐富的操作符對流進行處理,使你能以同步程式設計的方式處理非同步資料,並組合不同的操作符來輕鬆優雅的實現你所需要的功能。

1.2 RxJS可用於生產嗎?

ReactiveX由微軟於2012年開源,目前各語言庫由ReactiveX組織維護。RxJSGitHub上已有8782個star,目前最新版本為5.5.2

,並持續開發維護中,其中官方測試用例共計2699個。

誰在使用Rx

1.3 RxJS對專案程式碼的影響?

RxJS中的流以Observable物件呈現,獲取資料需要訂閱Observable,形式如下:

const ob = http$.getSomeList(); //getSomeList()返回某個由`Observable`包裝後的http請求
ob.subscribe((data) => console.log(data));
//在變數末尾加$表示Observable型別的物件。

以上與Promise類似:

const promise = http.getSomeList(); // 返回由`Promise
`包裝的http請求 promise.then((data) => console.log(data));

實際上Observable可以認為是加強版的Promise,它們之間是可以通過RxJSAPI互相轉換的:

const ob = Observable.fromPromise(somePromise); // Promise轉為Observable
const promise = someObservable.toPromise(); // Observable轉為Promise

因此可以在Promise方案的專案中安全使用RxJS,並能夠隨時升級到完整的RxJS方案。

1.4 RxJS會增加多少體積?

RxJS(v5)整個庫壓縮後約為140KB,由於其模組化可擴充套件的設計,因此僅需匯入所用到的類與操作符即可。匯入RxJS常用類與操作符後,打包後的體積約增加30-60KB,具體取決於匯入的數量。

不要用 import { Observable } from 'rxjs'這種方式匯入,這會匯入整個rxjs庫,按需匯入的方式如下:import { Observable } from 'rxjs/Observable' //匯入類import 'rxjs/add/operator/map' // 匯入例項操作符 import 'rxjs/add/observable/forkJoin' // 匯入類操作符

2. RxJS快速入門

2.1 初級核心概念

  • Observable
  • Observer
  • Operator

Observable被稱為可觀察序列,簡單來說資料就在Observable中流動,你可以使用各種operator對流進行處理,例如:

const ob = Observable.interval(1000);
ob.take(3).map(n => n * 2).filter(n => n > 2);

第一步程式碼我們通過類方法interval建立了一個Observable序列,ob作為源會每隔1000ms發射一個遞增的資料,即0 -> 1 -> 2。第二步我們使用操作符對流進行處理,take(3)表示只取源發射的前3個數據,取完第三個後關閉源的發射;map表示將流中的資料進行對映處理,這裡我們將資料翻倍;filter表示過濾掉出符合條件的資料,根據上一步map的結果,只有第二和第三個資料會留下來。

上面我們已經使用同步程式設計建立好了一個流的處理過程,但此時ob作為源並不會立刻發射資料,如果我們在map中列印n是不會得到任何輸出的,因為ob作為Observable序列必須被“訂閱”才能夠觸發上述過程,也就是subscribe(釋出/訂閱模式)。

const ob = Observable.interval(1000);
ob.take(3).map(n => n * 2).filter(n => n > 0).subscribe(n => console.log(n));

結果:

2 //第24 //第3

上面程式碼中我們給subscribe傳入了一個函式,這其實是一種簡寫,subscribe完整的函式簽名如下:

ob.subscribe({
    next: d => console.log(d),
    error: err => console.error(err),
    complete: () => console.log('end of the stream')
})

直接給subscribe傳入一個函式會被當做是next函式。這個完整的包含3個函式的物件被稱為observer(觀察者),表示的是對序列結果的處理方式。next表示資料正常流動,沒有出現異常;error表示流中出錯,可能是執行出錯,http報錯等等;complete表示流結束,不再發射新的資料。在一個流的生命週期中,errorcomplete只會觸發其中一個,可以有多個next(表示多次發射資料),直到complete或者error

observer.next可以認為是Promisethen的第一個引數,observer.error對應第二個引數或者Promisecatch

RxJS同樣提供了catch操作符,err流入catch後,catch必須返回一個新的Observable。被catch後的錯誤流將不會進入observererror函式,除非其返回的新observable出錯。

Observable.of(1).map(n => n.undefinedMethod()).catch(err => {
    // 此處處理catch之前發生的錯誤
    return Observable.of(0); // 返回一個新的序列,該序列成為新的流。
});

2.2 建立可觀察序列

建立一個序列有很多種方式,我們僅列舉常用的幾種:

Observable.of(...args)

Observable.of()可以將普通JavaScript資料轉為可觀察序列,點我測試

Observable.fromPromise(promise)

Promise轉化為Observable點我測試

Observable.fromEvent(elment, eventName)

DOM事件建立序列,例如Observable.fromEvent($input, 'click')點我測試

Observable.ajax(url | AjaxRequest)

傳送http請求,AjaxRequest參考這裡

Observable.create(subscribe)

這個屬於萬能的建立方法,一般用於只提供了回撥函式的某些功能或者庫,在你用這個方法之前先想想能不能用RxJS上的類方法來建立你所需要的序列,點我測試

2.3 合併序列

合併序列也屬於建立序列的一種,例如有這樣的需求:進入某個頁面後拿到了一個列表,然後需要對列表每一項發出一個http請求來獲取對應的詳細資訊,這裡我們把每個http請求作為一個序列,然後我們希望合併它們。合併有很多種方式,例如N個請求按順序序列發出(前一個結束再發下一個);N個請求同時發出並且要求全部到達後合併為陣列,觸發一次回撥;N個請求同時發出,對於每一個到達就觸發一次回撥。如果不用RxJS,我們會比較難處理這麼多情形,不僅實現麻煩,維護更麻煩,下面是使用RxJS對上述需求的解決方案:

const ob1 = Observable.ajax('api/detail/1');
const ob2 = Observable.ajax('api/detail/2');
...
const obs = [ob1, ob2...];
// 分別建立對應的HTTP請求。
  1. N個請求按順序序列發出(前一個結束再發下一個)
Observable.concat(...obs).subscribe(detail => console.log('每個請求都觸發回撥'));
  1. N個請求同時並行發出,對於每一個到達就觸發一次回撥
Observable.merge(...obs).subscribe(detail => console.log('每個請求都觸發回撥'));
  1. N個請求同時發出並且要求全部到達後合併為陣列,觸發一次回撥
Observable.forkJoin(...obs).subscribe(detailArray => console.log('觸發一次回撥'));

3. 使用RxJS實現搜尋功能

搜尋是前端開發中很常見的功能,一般是監聽<input />keyup事件,然後將內容傳送到後臺,並展示後臺返回的資料。

<input id="text"></input>
<script>
    var text = document.querySelector('#text');
    text.addEventListener('keyup', (e) =>{
        var searchText = e.target.value;
        // 傳送輸入內容到後臺
        $.ajax({
            url: `/search/${searchText}`,
            success: data => {
              // 拿到後臺返回資料,並展示搜尋結果
              render(data);
            }
        });
    });
</script>

上面程式碼實現我們要的功能,但存在兩個較大的問題:

  • 多餘的請求

當想搜尋“愛迪生”時,輸入框可能會存在三種情況,“愛”、“愛迪”、“愛迪生”。而這三種情況將會發起 3 次請求,存在 2 次多餘的請求。

  • 已無用的請求仍然執行

一開始搜了“愛迪生”,然後馬上改搜尋“達爾文”。結果後臺返回了“愛迪生”的搜尋結果,執行渲染邏輯後結果框展示了“愛迪生”的結果,而不是當前正在搜尋的“達爾文”,這是不正確的。

減少多餘請求數,可以用 setTimeout 函式節流的方式來處理,核心程式碼如下:

<input id="text"></input>
<script>
    var text = document.querySelector('#text'),
        timer = null;
    text.addEventListener('keyup', (e) =>{
        // 在 250 毫秒內進行其他輸入,則清除上一個定時器
        clearTimeout(timer);
        // 定時器,在 250 毫秒後觸發
        timer = setTimeout(() => {
            console.log('發起請求..');
        },250)
    })
</script>

已無用的請求仍然執行 的解決方式,可以在發起請求前宣告一個當前搜尋的狀態變數,後臺將搜尋的內容及結果一起返回,前端判斷返回資料與當前搜尋是否一致,一致才走到渲染邏輯。最終程式碼為:

" title="" data-original-title=“複製”>

<input id=“text”></input>
<script>
var text = document.querySelector(’#text’),
timer = null,
currentSearch = ‘’;
text.addEventListener(<span class="hljs-string">'keyup'</span>, (e) =&gt;{
    clearTimeout(timer)
    timer = setTimeout(<span class="hljs-function"><span class="hljs-params">()</span> =&gt;</span> {
        <span class="hljs-comment">// 宣告一個當前所搜的狀態變數</span>
        currentSearch = <span class="hljs-string">'書'</span>; 

        <span class="hljs-keyword">var</span> searchText = e.target.value;
        $.ajax({
            <span class="hljs-attr">url</span>: <span class="hljs-string">`/search/<span class="hljs-subst">${searchText}</span>`</span>,
            <span class="hljs-attr">success</span>: <span class="hljs-function"><span class="hljs-params">data</span> =&gt;</span> {
                <span class="hljs-comment">// 判斷後臺返回的標誌與我們存的當前搜尋變數是否一致</span>
                <span class="hljs-keyword">if</span> (data.search === currentSearch) {
                    <span class="hljs-comment">// 渲染展示</span>
                    render(data);
                } <span class="hljs-keyword">else</span> {
                    <span class="hljs-comment">// ..</span>
                }
            }           
        });
    },<span class="hljs-number">250</span>)
})

</script>

上面程式碼基本滿足需求,但程式碼開始顯得亂糟糟。我們來使用RxJS實現上面程式碼功能,如下:

var text = document.querySelector('#text');
var inputStream = Rx.Observable.fromEvent(text, 'keyup') //為dom元素繫結'keyup'事件
                    .debounceTime(250) // 防抖動
                    .pluck('target', 'value') // 取值
                    .switchMap(url => Http.get(url)) // 將當前輸入流替換為http請求
                    .subscribe(data => render(data)); // 接收資料

RxJS能簡化你的程式碼,它將與流有關的內部狀態封裝在流中,而不需要在流外定義各種變數來以一種上帝視角控制流程。Rx的程式設計方式使你的業務邏輯流程清晰,易維護,並顯著減少出bug的概率。

個人總結的常用操作符:

類操作符(通常為合併序列或從已有資料建立序列)合併 forkJoin, merge, concat建立 of, from, fromPromise, fromEvent, ajax, throw例項操作符(對流中的資料進行處理或者控制流程)map, filter,switchMap, toPromise, catch, take, takeUntil, timeout, debounceTime, distinctUntilChanged, pluck。對於這些操作符的使用不再詳細描述,請參閱網上資料。

            </div>