構建流式應用—RxJS詳解[轉]

目錄

  • 常規方式實現搜索功能
  • RxJS · 流 Stream
  • RxJS 實現原理簡析
    • 觀察者模式
    • 迭代器模式
    • RxJS 的觀察者 + 迭代器模式
  • RxJS 基礎實現
    • Observable
    • Observer
  • RxJS · Operators
    • Operators ·入門
    • 一系列的 Operators 操作
  • 使用 RxJS 一步步實現搜索功能
  • 總結

常規方式實現搜索

做一個搜索功能在前端開發中其實並不陌生,一般的實現方式是:監聽文本框的輸入事件,將輸入內容發送到後臺,最終將後臺返回的數據進行處理並展示成搜索結果。

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

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

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

  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>

已無用的請求仍然執行的解決方式,可以在發起請求前聲明一個當前搜索的狀態變量,後臺將搜索的內容及結果一起返回,前端判斷返回數據與當前搜索是否一致,一致才走到渲染邏輯。最終代碼爲

<input id="text"></input>
<script>
    var text = document.querySelector('#text'),
        timer = null,
        currentSearch = '';

    text.addEventListener('keyup', (e) =>{
        clearTimeout(timer)
        timer = setTimeout(() => {
            // 聲明一個當前所搜的狀態變量
            currentSearch = '書'; 

            var searchText = e.target.value;
            $.ajax({
                url: `search.qq.com/${searchText}`,
                success: data => {
                    // 判斷後臺返回的標誌與我們存的當前搜索變量是否一致
                    if (data.search === currentSearch) {
                        // 渲染展示
                        render(data);
                    } else {
                        // ..
                    }
                }           
            });
        },250)
    })
</script>

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

var text = document.querySelector('#text');
var inputStream = Rx.Observable.fromEvent(text, 'keyup')
                    .debounce(250)
                    .pluck('target', 'value')
                    .flatMapLatest(url => Http.get(url))
                    .subscribe(data => render(data));

可以明顯看出,基於 RxJS 的實現,代碼十分簡潔!

RxJS · 流 Stream

RxJS 是 Reactive Extensions for JavaScript 的縮寫,起源於 Reactive Extensions,是一個基於可觀測數據流在異步編程應用中的庫。RxJS 是 Reactive Extensions 在 JavaScript 上的實現,而其他語言也有相應的實現,如 RxJava、RxAndroid、RxSwift 等。學習 RxJS,我們需要從可觀測數據流(Streams)說起,它是 Rx 中一個重要的數據類型。

流是在時間流逝的過程中產生的一系列事件。它具有時間與事件響應的概念。

rxjs_stream

下雨天時,雨滴隨時間推移逐漸產生,下落時對水面產生了水波紋的影響,這跟 Rx 中的流是很類似的。而在 Web 中,雨滴可能就是一系列的鼠標點擊、鍵盤點擊產生的事件或數據集合等等。

RxJS 基礎實現原理簡析

對流的概念有一定理解後,我們來講講 RxJS 是怎麼圍繞着流的概念來實現的,講講 RxJS 的基礎實現原理。RxJS 是基於觀察者模式和迭代器模式以函數式編程思維來實現的。

觀察者模式

觀察者模式在 Web 中最常見的應該是 DOM 事件的監聽和觸發。

  • 訂閱:通過 addEventListener 訂閱 document.body 的 click 事件。
  • 發佈:當 body 節點被點擊時,body 節點便會向訂閱者發佈這個消息。
document.body.addEventListener('click', function listener(e) {
    console.log(e);
},false);

document.body.click(); // 模擬用戶點擊

將上述例子抽象模型,並對應通用的觀察者模型

2016-11-01 9 53 52

迭代器模式

迭代器模式可以用 JavaScript 提供了 Iterable Protocol 可迭代協議來表示。Iterable Protocol 不是具體的變量類型,而是一種可實現協議。JavaScript 中像 Array、Set 等都屬於內置的可迭代類型,可以通過 iterator 方法來獲取一個迭代對象,調用迭代對象的 next 方法將獲取一個元素對象,如下示例。

var iterable = [1, 2];

var iterator = iterable[Symbol.iterator]();

iterator.next(); // => { value: "1", done: false}
iterator.next(); // => { value: "2", done: false}

iterator.next(); // => { value: undefined, done: true}

元素對象中:value 表示返回值,done 表示是否已經到達最後。

遍歷迭代器可以使用下面做法。

var iterable = [1, 2];
var iterator = iterable[Symbol.iterator]();

var iterator = iterable();

while(true) {
    try {
        let result = iterator.next();  // <= 獲取下一個值
    } catch (err) {
        handleError(err);  // <= 錯誤處理
    }
    if (result.done) {
        handleCompleted();  // <= 無更多值(已完成)
        break;
    }
    doSomething(result.value);
}

主要對應三種情況:

  • 獲取下一個值
    調用 next 可以將元素一個個地返回,這樣就支持了返回多次值。

  • 無更多值(已完成)
    當無更多值時,next 返回元素中 done 爲 true。

  • 錯誤處理
    當 next 方法執行時報錯,則會拋出 error 事件,所以可以用 try catch 包裹 next 方法處理可能出現的錯誤。

RxJS 的觀察者 + 迭代器模式

RxJS 中含有兩個基本概念:Observables 與 Observer。Observables 作爲被觀察者,是一個值或事件的流集合;而 Observer 則作爲觀察者,根據 Observables 進行處理。
Observables 與 Observer 之間的訂閱發佈關係(觀察者模式) 如下:

  • 訂閱:Observer 通過 Observable 提供的 subscribe() 方法訂閱 Observable。
  • 發佈:Observable 通過回調 onNext 方法向 Observer 發佈事件。

下面爲 Observable 與 Observer 的僞代碼

// Observer
var Observer = {
    next(value) {
        alert(`收到${value}`);
    }
};

// Observable
function Observable (Observer) {
    setTimeout(()=>{
        Observer.next('A');
    },1000)
}

// subscribe
Observable(Observer);

上面實際也是觀察者模式的表現,那麼迭代器模式在 RxJS 中如何體現呢?

在 RxJS 中,Observer 除了有 next 方法來接收 Observable 的事件外,還可以提供了另外的兩個方法:error() 和 complete(),與迭代器模式一一對應。

var Observer = {
    next(value) { /* 處理值*/ },
    error(error) { /* 處理異常 */ },
    complete() { /* 處理已完成態 */ }
};

結合迭代器 Iterator 進行理解:

  • next()
    Observer 提供一個 next 方法來接收 Observable 流,是一種 push 形式;而 Iterator 是通過調用 iterator.next() 來拿到值,是一種 pull 的形式。

  • complete()
    當不再有新的值發出時,將觸發 Observer 的 complete 方法;而在 Iterator 中,則需要在 next 的返回結果中,當返回元素 done 爲 true 時,則表示 complete。

  • error()
    當在處理事件中出現異常報錯時,Observer 提供 error 方法來接收錯誤進行統一處理;Iterator 則需要進行 try catch 包裹來處理可能出現的錯誤。

下面是 Observable 與 Observer 實現觀察者 + 迭代器模式的僞代碼,數據的逐漸傳遞傳遞與影響其實就是流的表現。

// Observer
var Observer = {
    next(value) {
        alert(`收到${value}`);
    },
    error(error) {
        alert(`收到${value}`);
    },
    complete() {
        alert("complete");
    },
};

// Observable
function Observable (Observer) {
    [1,2,3].map(item=>{
        Observer.next(item);
    });

    Observer.complete();
    // Observer.error("error message");
}

// subscribe
Observable(Observer);

RxJS 基礎實現

有了上面的概念及僞代碼,那麼在 RxJS 中是怎麼創建 Observable 與 Observer 的呢?

創建 Observable

RxJS 提供 create 的方法來自定義創建一個 Observable,可以使用 onNext 來發出流。

var Observable = Rx.Observable.create(observer => {
    observer.onNext(2);
    observer.onCompleted();
    return  () => console.log('disposed');
});

創建 Observer

Observer 可以聲明 next、err、complete 方法來處理流的不同狀態。

var Observer = Rx.Observer.create(
    x => console.log('Next:', x),
    err => console.log('Error:', err),
    () => console.log('Completed')
);

最後將 Observable 與 Observer 通過 subscribe 訂閱結合起來。

var subscription = Observable.subscribe(Observer);

RxJS 中流是可以被取消的,調用 subscribe 將返回一個 subscription,可以通過調用 subscription.unsubscribe() 將流進行取消,讓流不再產生。

看了起來挺複雜的?換一個實現形式:

// @Observables 創建一個 Observables
var streamA = Rx.Observable.just(2);

// @Observer streamA$.subscribe(Observer)
streamA.subscribe(v => console.log(v));

將上面代碼改用鏈式寫法,代碼變得十分簡潔:

Rx.Observable.just(2).subscribe(v => console.log(v));

RxJS · Operators 操作

Operators 操作·入門

Rx.Observable.just(2).subscribe(v => console.log(v));

上面代碼相當於創建了一個流(2),最終打印出2。那麼如果想將打印結果翻倍,變成4,應該怎麼處理呢?

方案一?: 改變事件源,讓 Observable 值 X 2

Rx.Observable.just(2 * 2 /* <= */).subscribe(v => console.log(v));

方案二?: 改變響應方式,讓 Observer 處理 X 2

Rx.Observable.just(2).subscribe(v => console.log(v * 2 /* <= */));

優雅方案: RxJS 提供了優雅的處理方式,可以在事件源(Observable)與響應者(Observer)之間增加操作流的方法。

Rx.Observable.just(2)
             .map(v => v * 2) /* <= */
             .subscribe(v => console.log(v));

map 操作跟數組操作的作用是一致的,不同的這裏是將流進行改變,然後將新的流傳出去。在 RxJS 中,把這類操作流的方式稱之爲 Operators(操作)。RxJS提供了一系列 Operators,像map、reduce、filter 等等。操作流將產生新流,從而保持流的不可變性,這也是 RxJS 中函數式編程的一點體現。關於函數式編程,這裏暫不多講,可以看看另外一篇文章 《談談函數式編程》

到這裏,我們知道了,流從產生到最終處理,可能經過的一些操作。即 RxJS 中 Observable 將經過一系列 Operators 操作後,到達 Observer。

          Operator1   Operator2
Observable ----|-----------|-------> Observer

一系列的 Operators 操作

RxJS 提供了非常多的操作,像下面這些。

Aggregate,All,Amb,ambArray,ambWith,AssertEqual,averageFloat,averageInteger,averageLong,blocking,blockingFirst,blockingForEach,blockingSubscribe,Buffer,bufferWithCount,bufferWithTime,bufferWithTimeOrCount,byLine,cache,cacheWithInitialCapacity,case,Cast,Catch,catchError,catchException,collect,concatWith,Connect,connect_forever,cons,Contains,doAction,doAfterTerminate,doOnComplete,doOnCompleted,doOnDispose,doOnEach,doOnError,doOnLifecycle,doOnNext,doOnRequest,dropUntil,dropWhile,ElementAt,ElementAtOrDefault,emptyObservable,fromNodeCallback,fromPromise,fromPublisher,fromRunnable,Generate,generateWithAbsoluteTime,generateWithRelativeTime,Interval,intervalRange,into,latest (Rx.rb version of Switch),length,mapTo,mapWithIndex,Materialize,Max,MaxBy,mergeArray,mergeArrayDelayError,mergeWith,Min,MinBy,multicastWithSelector,nest,Never,Next,Next (BlockingObservable version),partition,product,retryWhen,Return,returnElement,returnValue,runAsync,safeSubscribe,take_with_time,takeFirst,TakeLast,takeLastBuffer,takeLastBufferWithTime,windowed,withFilter,withLatestFrom,zipIterable,zipWith,zipWithIndex

關於每一個操作的含義,可以查看官網進行了解。下面將舉幾個例子。

just
just 可以將普通數據轉換成流式數據 Observable。如上面的 Rx.Observable.just(2)。

fromEvent
除了數值外,RxJS 還提供了關於事件的操作,fromEvent 可以用來監聽事件。當事件觸發時,將事件 event 轉成可流動的 Observable 進行傳輸。下面示例表示:監聽文本框的 keyup 事件,觸發 keyup 可以產生一系列的 event Observable。

var text = document.querySelector('#text');
Rx.Observable.fromEvent(text, 'keyup')
             .subscribe(e => console.log(e));

map
map 方法跟我們平常使用的方式是一樣的,不同的只是這裏是將流進行改變,然後將新的流傳出去。上面示例已有涉及,這裏不再多講。

Rx.Observable.just(2)
             .map(v => 10 * v)
             .subscribe(v => console.log(v));

Rx 提供了許多的操作,爲了更好的理解各個操作的作用,我們可以通過一個可視化的工具 marbles 圖 來輔助理解。如 map 方法對應的 marbles 圖如下

map

箭頭可以理解爲時間軸,上面的數據經過中間的操作,轉變成下面的模樣。

FlatMap
FlatMap 也是 RxJS 中常用的接口,我們來結合 marbles 圖來理解它

rxjs_flatmap

上面的數據流中,產生了新的分支流(流中流),FlatMap 的作用則是將分支流調整回主幹上,最終分支上的數據流都經過主幹的其他操作,其實也是將流中流進行扁平化。

FlatMapLatest
FlatMapLatest 與 FlatMap 都是將分支流疏通到主幹上,而不同的地方在於 FlatMapLatest 只會保留最後的流,而取消拋棄之前的流。

除了上面提到的 marbles,也可以 ASCII 字符的方式來繪製可視化圖表,下面將結合 Map、FlatMap 和 FlatMapLatest 進行對比來理解。

@Map             @FlatMap            @FlatMapLatest
                         ↗  ↗                 ↗  ↗
-A------B-->           a2 b2                a2 b2  
-2A-----2B->          /  /                 /  /  
                    /  /                 /  /
                  a1 b1                a1 b1
                 /  /                 /  /
                -A-B----------->     -A-B---------->
                --a1-b1-a2-b2-->     --a1-b1---b2-->

FlatMap 和 FlatMapLatest 中,A 和 B 是主幹上產生的流,a1、a2 爲 A 在分支上產生,b1、b2 爲 B 在分支上產生,可看到,最終將歸併到主幹上。FlatMapLatest 只保留最後的流,所以將 A 的 a2 拋棄掉。

Debounce
Debounce 操作可以操作一個時間戳 TIMES,表示經過 TIMES 毫秒後,沒有流入新值,那麼纔將值轉入下一個操作。

rxjs_debounce

RxJS 中的操作符是滿足我們以前的開發思維的,像 map、reduce 這些。另外,無論是 marbles 圖還是用 ASCII 字符圖這些可視化的方式,都對 RxJS 的學習和理解有非常大的幫助。

使用 RxJS 一步步實現搜索示例

RxJS 提供許多創建流或操作流的接口,應用這些接口,我們來一步步將搜索的示例進行 Rx 化。

使用 RxJS 提供的 fromEvent 接口來監聽我們輸入框的 keyup 事件,觸發 keyup 將產生 Observable。

var text = document.querySelector('#text');
Rx.Observable.fromEvent(text, 'keyup')
             .subscribe(e => console.log(e));

這裏我們並不想輸出事件,而想拿到文本輸入值,請求搜索,最終渲染出結果。涉及到兩個新的 Operators 操作,簡單理解一下:

  • pluck('target', 'value')
    將輸入的 event,輸出成 event.target.value。

  • flatMap()
    將請求搜索結果輸出回給 Observer 上進行渲染。

var text = document.querySelector('#text');
Rx.Observable.fromEvent(text, 'keyup')
             .pluck('target', 'value') // <--
             .flatMap(url => Http.get(url)) // <--
             .subscribe(data => render(data))

上面代碼實現了簡單搜索呈現,但同樣存在一開始提及的兩個問題。那麼如何減少請求數,以及取消已無用的請求呢?我們來了解 RxJS 提供的其他 Operators 操作,來解決上述問題。

  • debounce(TIMES)
    表示經過 TIMES 毫秒後,沒有流入新值,那麼纔將值轉入下一個環節。這個與前面使用 setTimeout 來實現函數節流的方式有一致效果。

  • flatMapLatest()
    使用 flatMapLatest 替換 flatMap,將能取消上一個已無用的請求,只保留最後的請求結果流,這樣就確保處理展示的是最後的搜索的結果。

最終實現如下,與一開始的實現進行對比,可以明顯看出 RxJS 讓代碼變得十分簡潔。

var text = document.querySelector('#text');
Rx.Observable.fromEvent(text, 'keyup')
             .debounce(250) // <- throttling behaviour
             .pluck('target', 'value')
             .flatMapLatest(url => Http.get(url)) // <- Kill the previous requests
             .subscribe(data => render(data))

總結

本篇作爲 RxJS 入門篇到這裏就結束,關於 RxJS 中的其他方面內容,後續再拎出來進一步分析學習。
RxJS 作爲一個庫,可以與衆多框架結合使用,但並不是每一種場合都需要使用到 RxJS。複雜的數據來源,異步多的情況下才能更好凸顯 RxJS 作用,這一塊可以看看民工叔寫的《流動的數據——使用 RxJS 構造複雜單頁應用的數據邏輯》 相信會有更好的理解。點擊查看更多文章>>

附:
RxJS(JavaScript) https://github.com/Reactive-Extensions/RxJS
RxJS(TypeScript ) https://github.com/ReactiveX/rxjs

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章