本文是一篇 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
时,流才会继续执行;不然将中断以后的事件