RXjs簡介(來自思否:segmentfault.com)
Introduction to RxJS
1. 前言
1.1 什麼是RxJS
?
RxJS
是ReactiveX
程式設計理念的JavaScript
版本。ReactiveX
來自微軟,它是一種針對非同步資料流的程式設計。簡單來說,它將一切資料,包括HTTP請求,DOM事件或者普通資料等包裝成流的形式,然後用強大豐富的操作符對流進行處理,使你能以同步程式設計的方式處理非同步資料,並組合不同的操作符來輕鬆優雅的實現你所需要的功能。
1.2 RxJS
可用於生產嗎?
ReactiveX
由微軟於2012年開源,目前各語言庫由ReactiveX
組織維護。RxJS
在GitHub
上已有8782
個star,目前最新版本為5.5.2
2699
個。
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
,它們之間是可以通過RxJS
的API
互相轉換的:
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 //第2秒
4 //第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
表示流結束,不再發射新的資料。在一個流的生命週期中,error
和complete
只會觸發其中一個,可以有多個next
(表示多次發射資料),直到complete
或者error
。
observer.next
可以認為是Promise
中then
的第一個引數,observer.error
對應第二個引數或者Promise
的catch
。
RxJS
同樣提供了catch
操作符,err
流入catch
後,catch
必須返回一個新的Observable
。被catch
後的錯誤流將不會進入observer
的error
函式,除非其返回的新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請求。
- N個請求按順序序列發出(前一個結束再發下一個)
Observable.concat(...obs).subscribe(detail => console.log('每個請求都觸發回撥'));
- N個請求同時並行發出,對於每一個到達就觸發一次回撥
Observable.merge(...obs).subscribe(detail => console.log('每個請求都觸發回撥'));
- 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) =>{
clearTimeout(timer)
timer = setTimeout(<span class="hljs-function"><span class="hljs-params">()</span> =></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> =></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>