本文是一篇 RxJS 實戰教程,利用 RxJS 和 github API 來一步步作一個 github 小應用。所以,文章的重點是解釋 RxJS 的使用,而涉及的 ES6語法、webpack 等知識點不予講解。javascript
本例的全部代碼在 github 倉庫:rxjs-examplecss
首先要注意的是,目前在 github 上有兩個主流 RxJS,它們表明不一樣的版本:html
ReactiveX - rxjs RxJS 5 beta 版java
Reactive-Extensions - RxJS RxJS 4.x 穩定版react
這兩個版本的安裝和引用稍有不一樣:jquery
# 安裝 4.x 穩定版 $ npm install rx --save # 安裝 5 beta 版 $ npm install rxjs --save
// 4.x 穩定版 import Rx from 'rx'; // 5 beta 版 import Rx from 'rxjs/Rx';
除此之外,它們的語法也稍有不一樣,好比在 5 beta 版裏,subscribe
時能夠代入一個對象做爲參數,也能夠代入回調函數做爲參數,而 4.x 版則只支持以回調函數爲參數的狀況:webpack
// 5 beta var observer = { next: x => console.log('Observer got a next value: ' + x), error: err => console.error('Observer got an error: ' + err), complete: () => console.log('Observer got a complete notification'), }; Observable.subscribe(observer); // 5 和 4.x 都支持: Observable.subscribe(x => console.log(x), (err) => console.log(err), () => console.log('completed'));
其餘更多語法不一樣能夠參考:git
4.x 穩定版 Documentgithub
如上所說,咱們要利用 RxJS 和 github API 來一步步作一個 github 小應用。首先完成其基本功能,即經過一個 input 輸入文字,並實時根據 input 內值的變化去發送異步請求,調用 github API 進行搜索。如圖所示(線上 Demo):
經過
RxJS
,在輸入過程當中實時進行異步搜索:
hover
到 avator 上以後異步獲取用戶信息
安裝 webpack 配置編譯環境,並使用 ES6 語法。安裝以下依賴,並配置好 webpack:
webpack
webpack-dev-server
babel-loader
babel-preset-es2015
html-webpack-plugin
css-loader / postcss 及其餘
jquery
rx(4.x 版本)
經過webpack-dev-server
,咱們將會啓動一個 8080 端口的服務器,使得咱們編譯好的資源能夠在localhost:8080/webpack-dev-server
訪問到。
在index.html
中編寫一個input
,咱們將在index.js
中,經過 RxJS 的 Observable 監聽input
的keyup
事件。可使用fromEvent
來建立一個基於 DOM 事件的流,並經過map
和filter
進一步處理。
<!-- index.html --> <input class="search" type="text" maxlength="1000" required placeholder="search in github"/>
// src/js/index.js import Rx from 'rx'; $(() => { const $input = $('.search'); // 經過 input 的 keyup 事件來建立流 const observable = Rx.Observable.fromEvent($input, 'keyup') // 並獲取每次 keyup 時搜索框的值,篩選出合法值 .map(() => $input.val().trim()) .filter((text) => !!text) // 利用 do 能夠作一些不影響流的事件,好比這裏打印出 input 的值 .do((value) => console.log(value)); // 開啓監聽 observable.subscribe(); });
去 input 裏隨便打打字,能夠看到咱們已經成功監聽了keyup
事件,並在每次keyup
時在 console 裏輸出 input 當前的值。
監聽了 input 事件,咱們就可以在每次keyup
時拿到 value,那麼就能夠經過它來異步獲取數據。將整個過程拆分一下:
用戶在 input 裏輸入任意內容
觸發keyup
事件,獲取到當前 value
將 value 代入到一個異步方法裏,經過接口獲取數據
利用返回數據渲染 DOM
也就是說,咱們要把原有的 Observable 中每一個事件返回的 value 進行異步處理,並使其返回一個新的 Observable。能夠這麼處理:
讓每一個 value 返回一個 Observable
經過flatMap
將全部的 Observable 扁平化,成爲一個新的 Observable
圖解flatMap
:
而既然須要異步獲取數據,那麼在上面的第一步時,能夠經過fromPromise
來建立一個 Observable:
// src/js/helper.js const SEARCH_REPOS = 'https://api.github.com/search/repositories?sort=stars&order=desc&q='; // 建立一個 ajax 的 promise const getReposPromise = (query) => { return $.ajax({ type: "GET", url: `${SEARCH_REPOS}${query}`, }).promise(); }; // 經過 fromPromise 建立一個 Observable export const getRepos = (query) => { const promise = getReposPromise(query); return Rx.Observable.fromPromise(promise); };
// src/js/index.js import {getRepos} from './helper'; // ... const observable = Rx.Observable.fromEvent($input, 'keyup') .map(() => $input.val()) .filter((text) => !!text) .do((value) => console.log(value)) // 調用 getRepos 方法將返回一個 Observable // flatMap 則將全部 Observable 合併,轉爲一個 Observable .flatMap(getRepos); // ...
這樣,每一次keyup
的時候,都會根據此時 input 的 value 去異步獲取數據。但這樣作有幾個問題:
不斷打字時會接二連三觸發異步請求,佔用資源影響體驗
若是相鄰的keyup
事件觸發時 input 的值同樣,也就是說按下了不改變 value 的按鍵(好比方向鍵),會重複觸發同樣的異步事件
發出多個異步事件以後,每一個事件所耗費的時間不必定相同。若是前一個異步所用時間較後一個長,那麼當它最終返回結果時,有可能把後面的異步率先返回的結果覆蓋
因此接下來咱們就處理這幾個問題。
針對上面的問題,一步一步進行優化。
不斷打字時會接二連三觸發異步請求,佔用資源影響體驗
也就是說,當用戶在連續打字時,咱們不該該繼續進行以後的事件處理,而若是打字中斷,或者說兩次keyup
事件的時間間隔足夠長時,才應該發送異步請求。針對這點,可使用 RxJS 的debounce
方法:
如圖所示,在一段時間內事件被不斷觸發時,不會被以後的操做所處理;只有超過指定時間間隔的事件纔會留下來:
// src/js/index.js // ... const observable = Rx.Observable.fromEvent($input, 'keyup') // 若 400ms 內連續觸發 keyup 事件,則不會繼續往下處理 .debounce(400) .map(() => $input.val()) .filter((text) => !!text) .do((value) => console.log(value)) .flatMap(getRepos); // ...
若是相鄰的
keyup
事件觸發時 input 的值同樣,也就是說按下了不改變 value 的按鍵(好比方向鍵),會重複觸發同樣的異步事件
也就是說,對於任意相鄰的事件,若是它們的返回值同樣,則只要取一個(重複事件中的第一個)就行了。能夠利用distinctUntilChanged
方法:
// src/js/index.js // ... const observable = Rx.Observable.fromEvent($input, 'keyup') .debounce(400) .map(() => $input.val()) .filter((text) => !!text) // 只取不同的值進行異步 .distinctUntilChanged() .do((value) => console.log(value)) .flatMap(getRepos); // ...
發出多個異步事件以後,每一個事件所耗費的時間不必定相同。若是前一個異步所用時間較後一個長,那麼當它最終返回結果時,有可能把後面的異步率先返回的結果覆蓋
這個蛋疼的問題我相信你們極可能碰見過。在發送多個異步請求時,由於所用時長不必定,沒法保障異步返回的前後順序,因此,有時候可能早請求的異步的結果會覆蓋後來請求的異步結果。
而這種狀況的處理方式就是,在連續發出多個異步的時候,既然咱們期待的是最後一個異步返回的結果,那麼就能夠把以前的異步取消掉,不 care 其返回了什麼。所以,咱們可使用flatMapLatest
API(相似於 RxJava 中的switchMap
API,同時在 RxJS 5.0 中也已經更名爲switchMap
)
經過flatMapLatest
,當 Observable 觸發某個事件,返回新的 Observable 時,將取消以前觸發的事件,而且再也不關心返回結果的處理,只監視當前這一個。也就是說,發送多個請求時,不關心以前請求的處理,只處理最後一次的請求:
// src/js/index.js // ... const observable = Rx.Observable.fromEvent($input, 'keyup') .debounce(400) .map(() => $input.val()) .filter((text) => !!text) .distinctUntilChanged() .do((value) => console.log(value)) // 僅處理最後一次的異步 .flatMapLatest(getRepos); // ...
至此,咱們對 input keyup
以及異步獲取數據的整個事件流處理完畢,並進行了必定的優化,避免了過多的請求、異步返回結果錯亂等問題。但建立了一個流以後也有對其進行監聽:
// src/js/index.js // ... const observable = Rx.Observable.fromEvent($input, 'keyup') .debounce(400) .map(() => $input.val()) .filter((text) => !!text) .distinctUntilChanged() .do((value) => console.log(value)) .flatMapLatest(getRepos); // 第一個回調中的 data 表明異步的返回值 observable.subscribe((data) => { // 在 showNewResults 方法中使用返回值渲染 DOM showNewResults(data); }, (err) => { console.log(err); }, () => { console.log('completed'); }); // 異步返回的結果是個 Array,表明搜索到的各個倉庫 item // 遍歷全部 item,轉化爲 jQuery 對象,最後插入到 content_container 中 const showNewResults = (items) => { const repos = items.map((item, i) => { return reposTemplate(item); }).join(''); $('.content_container').html(repos); };
這樣,一個經過 RxJS 監聽事件的流已經徹底創建完畢了。整個過程使用圖像來表示則以下:
而若是咱們不使用 RxJS,用傳統方式監聽 input 的話:
// src/js/index.js import {getRepos} from './helper'; $(() => { const $input = $('.search'); const interval = 400; var previousValue = null; var fetching = false; var lastKeyUp = Date.now() - interval; $input.on('keyup', (e) => { const nextValue = $input.val(); if (!nextValue) { return; } if (Date.now() - lastKeyUp <= interval) { return; } lastKeyUp = Date.now(); if (nextValue === previousValue) { return; } previousValue = nextValue; if (!fetching) { fetching = true; getRepos(nextValue).then((data) => { fetching = false; showNewResults(data); }); } }); });
挺複雜了吧?並且即使如此,這樣的處理仍是不夠到位。上面僅僅是經過fetching
變量來判斷是否正在異步,若是正在異步,則不進行新的異步;而咱們更但願的是可以取消舊的異步,只處理新的異步請求。
按照上面的教程,咱們在 Observable 中獲取到了數據、發送異步請求並拿到了最新一次的返回值。以後,再經過subscribe
,在監聽的回調中將返回值拼接成 HTML 並插入 DOM。
可是有一個問題:小應用的另外一個功能是,當鼠標hover
到頭像上時,異步獲取並展示用戶的信息。但是用戶頭像是在subscribe
回調中動態插入的,又該如何建立事件流呢?固然了,能夠在每次插入 DOM 以後在利用fromEvent
建立一個基於hover
的事件流,但那樣老是不太好的,寫出來的代碼也不夠 Rx。或許咱們就不該該在.flatMapLatest(getRepos)
以後中斷流的傳遞?但那樣的話,又該如何把異步的返回值插入 DOM 呢?
針對這種狀況,咱們可使用 RxJS 的do
方法:
你想在do
的回調內作什麼均可以,它不會影響到流內的事件;除此之外,還能夠拿到流中各個事件的返回值:
var observable = Rx.Observable.from([0, 1, 2]) .do((x) => console.log(x)) .map((x) => x + 1); observable.subscribe((x) => { console.log(x); });
因此,咱們能夠利用do
來完成 DOM 的渲染:
// src/js/index.js // ... // $conatiner 是裝載搜索結果的容器 div const $conatiner = $('.content_container'); const observable = Rx.Observable.fromEvent($input, 'keyup') .debounce(400) .map(() => $input.val()) .filter((text) => !!text) .distinctUntilChanged() .do((value) => console.log(value)) .flatMapLatest(getRepos) // 首先把以前的搜索結果清空 .do((results) => $conatiner.html('')) // 利用 Rx.Observable.from 將異步的結果轉化爲 Observable,並經過 flatMap 合併到原有的流中。此時流中的每一個元素是 results 中的每一個 item .flatMap((results) => Rx.Observable.from(results)) // 將各 item 轉化爲 jQuery 對象 .map((repos) => $(reposTemplate(repos))) // 最後把每一個 jQuery 對象依次加到容器裏 .do(($repos) => { $conatiner.append($repos); }); // 在 subscribe 中實際上什麼都不用作,就能達到以前的效果 observable.subscribe(() => { console.log('success'); }, (err) => { console.log(err); }, () => { console.log('completed'); });
簡直完美!如今咱們這個observable
在最後經過map
,依次返回了一個 jQuery 對象。那麼以後若是要對頭像添加hover
的監聽,則能夠在這個流的基礎上繼續進行。
hover
的事件流咱們接下來針對用戶頭像的hover
事件建立一個流。用戶的詳細資料是異步加載的,而hover
到頭像上時彈出 modal。若是是第一個hover
,則 modal 裏只有一個 loading 的圖標,而且異步獲取數據,以後將返回的數據插入到 modal 裏;而若是已經拿到並插入好了數據,則再也不有異步請求,直接展現:
沒有數據時展現 loading,同時異步獲取數據
異步返回後插入數據。且若是已經有了數據則直接展現
先無論上一個流,咱們先建立一個新的事件流:
// src/js/index.js // ... const initialUserInfoSteam = () => { const $avator = $('.user_header'); // 經過頭像 $avator 的 hover 事件來建立流 const avatorMouseover = Rx.Observable.fromEvent($avator, 'mouseover') // 500ms 內重複觸發事件則會被忽略 .debounce(500) // 只有當知足了下列條件的流纔會繼續執行,不然將中斷 .takeWhile((e) => { // 異步獲取的用戶信息被新建到 DOM 裏,該 DOM 最外層是 infos_container // 所以,若是已經有了 infos_container,則能夠認爲咱們已經異步獲取過數據了,此時 takeWhile 將返回 false,流將會中斷 const $infosWrapper = $(e.target).parent().find('.user_infos_wrapper'); return $infosWrapper.find('.infos_container').length === 0; }) .map((e) => { const $infosWrapper = $(e.target).parent().find('.user_infos_wrapper'); return { conatiner: $infosWrapper, url: $(e.target).attr('data-api') } }) .filter((data) => !!data.url) // getUser 來異步獲取用戶信息 .flatMapLatest(getUser) .do((result) => { // 將用戶信息組建成爲 DOM 元素,並插入到頁面中。在這以後,該用戶對應的 DOM 裏就會擁有 infos_container 這個 div,因此 takeWhile 會返回 false。也就是說,以後再 hover 上去,流也不會被觸發了 const {data, conatiner} = result; showUserInfo(conatiner, data); }); avatorMouseover.subscribe((result) => { console.log('fetch user info succeed'); }, (err) => { console.log(err); }, () => { console.log('completed'); }); };
上面的代碼中有一個 API 須要講解:takeWhile
由圖可知,當takeWhile
中的回調返回true
時,流能夠正常進行;而一旦返回false
,則以後的事件不會再發生,流將直接終止:
var source = Rx.Observable.range(1, 5) .takeWhile(function (x) { return x < 3; }); var subscription = source.subscribe( function (x) { console.log('Next: ' + x); }, function (err) { console.log('Error: ' + err); }, function () { console.log('Completed'); }); // Next: 0 // Next: 1 // Next: 2 // Completed
建立好針對hover
的事件流,咱們能夠把它和上一個事件流結合起來:
// src/js/index.js // ... const initialUserInfoSteam = ($repos) => { const $avator = $repos.find('.user_header'); // ... } const observable = Rx.Observable.fromEvent($input, 'keyup') // ... .do(($repos) => { $conatiner.append($repos); initialUserInfoSteam($repos); }); // ...
如今這樣就已經可使用了,但依舊不夠好。目前總共有兩個流:監聽 input keyup
的流和監聽mouseover
的流。可是,由於用戶頭像是動態插入的 ,因此咱們必須在$conatiner.append($repos);
以後才能建立並監聽mouseover
。不過鑑於咱們已經在最後的do
方法裏插入了獲取的數據,因此能夠試着把兩個流合併到一塊兒:
// src/js/index.js // ... const initialUserInfoSteam = ($repos) => { const $avator = $repos.find('.user_header'); const avatorMouseover = Rx.Observable.fromEvent($avator, 'mouseover') // ... 流的處理跟以前的同樣 // 但咱們再也不須要 subscribe 它,而是返回這個 Observable return avatorMouseover; }; const observable = Rx.Observable.fromEvent($input, 'keyup') // ... .do(($repos) => { $conatiner.append($repos); // 再也不在 do 裏面建立新的流並監聽 // initialUserInfoSteam($repos); }) // 相反,咱們繼續這個流的傳遞,只是經過 flatMap 將原來的流變成了監聽 mouseover 的流 .flatMap(($repos) => { return initialUserInfoSteam($repos); }); // ...
DONE !
栗子中使用到的 RxJS API:
from
經過一個可迭代對象來建立流
fromEvent
經過 DOM 事件來建立流
debounce
若是在必定時間內流中的某個事件不斷被觸發,則不會進行以後的事件操做
map
遍歷流中全部事件,返回新的流
filter
篩選流中全部事件,返回新的流
flatMap
對各個事件返回的值進行處理並返回 Observable,而後將全部的 Observable 扁平化,成爲一個新的 Observable
flatMapLatest
對各個事件返回的值進行處理並返回 Observable,而後將全部的 Observable 扁平化,成爲一個新的 Observable。但只會獲取最後一次返回的 Observable,其餘的返回結果不予處理
distinctUntilChanged
流中若是相鄰事件的結果同樣,則僅篩選出一個(剔除重複值)
do
能夠依次拿到流上每一個事件的返回值,利用其作一些無關流傳遞的事情
takeWhile
給予流一個判斷,只有當takeWhile
中的回調返回true
時,流纔會繼續執行;不然將中斷以後的事件