让我们一起来学习 RxJS

26 SEPTEMBER 2016 on  rxjs

这是一篇 RxJS 初学者教程。

What Is RxJS

通过阅读官方文档,不难得出:RxJS 可以很好解决异步和事件组合的问题。

这个时候我就有疑问了,异步问题不是用 Promise ( async/await ) 就好了吗?
至于事件,配合框架 ( React, Vue, Angular2 等 ) 的话不也很容易解决吗?

不管怎样, 让我们先看个 Hello World 吧。( 我要看 DEMO )

Rx's Hello World

// auto-complete
const Observable = Rx.Observable
const input = document.querySelector('input')const search$ = Observable.fromEvent(input, 'input')  .map(e => e.target.value).filter(value => value.length >= 1).throttleTime(100).distinctUntilChanged().switchMap(term => Observable.fromPromise(wikiIt(term))).subscribe(x => renderSearchResult(x),err => console.error(err))

上面的代码做了以下事情:

  • 监听 input 元素的 input 事件
  • 一旦发生,把事件对象 e 映射成 input 元素的值
  • 接着过滤掉值长度小于 1 的
  • 并且还设置了一个 throttle ( 节流器 ),两次输入间隔不超过 100 毫秒为有效输入
  • 如果该值和过去最新的值相等的话,忽略他
  • 最后,拿到值便调用 Wikipedia 的一个 API
  • 最后的最后,需要 subscribe 才能拿到 API 返回的数据

是不是看起来就觉得很 cool ,好想学!
短短几行代码就完成了一个 auto-complete 组件。

How It Works

那上面的代码是什么意思?
RxJS 到底是如何工作的?如何解决异步组合问题的?

Observable

Rx 提供了一种叫 Observable 的数据类型,兼容 ECMAScript 的 Observable Spec Proposal 草案标准。他是 Rx 最核心的数据类型,结合了 Observer Pattern,Iterator Pattern 。

那到底什么是 Observable ?

Observable 其实就是一个异步的数组。( ---> 2 minute introduction to rx )

不妨想像一下,数组 + 时间轴 = Observable 。

数组元素的值是未来某个时间点 emit ( 产生 ) 的,但是我们并不关心这个时间点,因为利用了「观察者模式」subscribe ( 订阅 ) 了这个数组,只要他 emit 了值,就会自动 push给我们。

我们再用图来表示一下的话:

--a---b-c--d-----e--|-->

这种图叫做 marble diagram 。
我们可以把 ASCII 的 marble 图转成 SVG 的:ASCII -> SVG 。

- 表示时间轴,a ~ e 表示 emit 的值,| 则表示这个 stream 已经结束了。
比方说,click 事件用上图来表示:a 表示第 1 次点击,b 表示第 2 次点击,如此类推。

如果你觉得 Observable 这个名字不够形象不够 cool 的话,你可把他叫做 stream ,因为他的 marble 图就像 steam 一样。所以啊,下面我都会把 Observable 称作 stream 。

Operators

那么,我们怎么对 stream 进行操作呢?怎么把多个 stream 组合在一起呢?

我们前面不是说了「 Observable 其实就是异步数组」吗?在 JavaScript 里的数组不是有很多内置的方法吗?比如 mapfilterreduce 等等。类似地,Observable 也有自己的方法,也就是所谓的 operator 。比如上面 Rx's Hello World 例子中的 mapfilterthrottleTimedistinctUntilChanged 等等很多很有用的 operator 。

面对 RxJS 那么多 operator ,我们要怎么学习呢?很简单:

分类别 + 画 marble 图 + 看例子 + 选

现在,就让我们画出上面 Hello World 例子的 marble 图。

const search$ = Observable.fromEvent(input, 'input')  .map(e => e.target.value).filter(value => value.length >= 1).throttleTime(100).distinctUntilChanged().switchMap(term => Observable.fromPromise(wikiIt(term))).subscribe(x => renderSearchResult(x),err => console.error(err))

假设输入了 5 次,每次输入的值一次为:aabcdc ,并且第 3 次输入的 c 和第 4 次的 d 的时间间隔少于 100ms :

---i--i---i-i-----i---|--> (input)map---a--a---c-d-----c---|-->bfilter---a--a---c-d-----c---|-->bthrottleTime---a--a---c-------c---|-->bdistinctUntilChanged---a--a---c----------|-->bswitchMap---x--y---z----------|-->

如果我告诉你学习 RxJS 的捷径是「学会看和画 marble 图」,你信还是不信?

Learn By Doing

现在,就让我们结合上面的知识,来实现一个简单的 canvas 画板。

根据 canvas 的 API ,我们需要知道两个点的坐标,这样才能画出一条线。

Step 1

( 我要看 DEMO )

那么,现在我们需要做的是创建一个关于鼠标移动的 stream 。于是,我们去文档找对应的 operator 类别,也就是 Creation Operators ,然后得到 fromEvent 。

const canvas = document.querySelector('canvas')const move$ = Rx.Observable.fromEvent(canvas, 'mousemove')

对应的 marble 图:

--m1---m1-m2--m3----m4---|-->  (mousemove)

接着,我们需要拿到每次鼠标移动时的坐标。也就是说:需要变换 stream 。
对应类别的 operator 文档:Transformation Operators ---> map 。

const move$ = Rx.Observable.fromEvent(canvas, 'mousemove')  .map(e => ({ x: e.offsetX, y: e.offsetX }))

此时的 marble 图:

--m1---m2-m3--m4----m5---|-->  (mousemove)map
--x1---x2-x3--x4----x5---|-->  (点坐标)

然后,怎么拿到两个点的坐标呢?我们需要再变换一下 stream 。
对应类别的 operator 文档:Transformation Operators ---> bufferCount 。

const move$ = Rx.Observable.fromEvent(canvas, 'mousemove')  .map(e => ({ x: e.offsetX, y: e.offsetY })).bufferCount(2)

marble 图:

--m1---m2-m3--m4----m5---|-->  (mousemove)map--x1---x2-x3--x4----x5---|-->  (点坐标)bufferCount(2)-------x1-----x3----x5---|---> (两点坐标)x2     x4

然而你会发现,此时画出来的线段是不连续的。为什么?我也不知道!!
那就让我们看看别人是怎么写的吧:canvas paint 。

Step 2

( 先让我要看看 DEMO )

换了一种思路,并没有变换 stream ,而是把两个 stream 组合在一起。
查看文档 Combination Operators ---> zip 以及 Filtering Operators ---> skip

const move$ = Rx.Observable.fromEvent(canvas, 'mousemove')  .map(e => ({ x: e.offsetX, y: e.offsetY }))const diff$ = move$  .zip(move$.skip(1), (first, sec) => ([ first, sec ]))

此时的 marble 图:

--x1---x2-x3--x4----x5---|-->  (move$)skip(1)
-------x2-x3--x4----x5---|-->  --x1---x2-x3--x4----x5---|-->  (move$)
-------x2-x3--x4----x5---|-->  zip-------x1-x2--x3----x4---|-->  (diff$)x2 x3  x4    x5

这样一来,diff$ emit 的值就依次为 (x1, x2)(x2, x3)(x3, x4) …… 现在,鼠标移动的时候,就可以画出美丽的线条。

Step 3

( 我想看 DEMO )

就在此时我恍然大悟,终于知道前面用 bufferCount 为什么不行了。我们不妨来比较一下:

-------x1-----x3----x5---|---> (bufferCount)x2     x4-------x1-x2--x3----x4---|-->  (diff$)x2 x3  x4    x5

bufferCount emit 的值依次为:(x1, x2), (x3, x4) …… x2 和 x3 之间是有间隔的。这就是为什么线段会不连续的原因。

然后看 bufferCount 文档的话,你会发现可以使用 bufferCount(2, 1) 实现同样的效果。这样的话,我们就不需要使用 zip 来组合两个 stream 了。Cool ~

const move$ = Rx.Observable.fromEvent(canvas, 'mousemove')  .map(e => ({ x: e.offsetX, y: e.offsetX })).bufferCount(2, 1)

此时的 marble 图:

--m1---m2-m3--m4----m5---|-->  (mousemove)map--x1---x2-x3--x4----x5---|-->  (点坐标)bufferCount(2, 1)-------x1-x2--x3----x4---|---> (两点坐标)x2 x3  x4    x5

Step 4

( 我就要看 DEMO )

接下来,我们想实现「只有鼠标按下时,才能画画,否则不能」。
首先我们需要创建两个关于鼠标动作的 stream 。

const down$ = Rx.Observable.fromEvent(canvas, 'mousedown')
const up$ = Rx.Observable.fromEvent(canvas, 'mouseup')

当鼠标按下的时候,我们需要把他变换成鼠标移动的 stream ,直到鼠标放开。
查看文档 Transformation Operators ---> switchMapTo 。

down$.switchMapTo(move$)

此时的 marble 图:

--d---d-d-----d---d--|-->  (mousedown)switchMapTo--m---m-m-----m---m--|-->

此时,鼠标放开了我们还能继续画画,这显然不是我们想要的。这个时候我们很容易会使用 takeUntil 这个 operator ,但是这是不对的,因为他会把 stream complete 掉。

还是让我们看看别人是怎么写的吧:canvas paint 。

Step 5

( 我只想看 DEMO )

思路是这个样子的:

把 up$ 和 down$ 组合成一个新的 stream ,但为了分辨他们,我们需要先把他们变换成新的 stream 。
查看文档 Combination Operators ---> merge 。
Transformation Operators ---> map 。

const down$ = Rx.Observable.fromEvent(canvas, 'mousedown')  .map(() => 'down')
const up$ = Rx.Observable.fromEvent(canvas, 'mouseup')  .map(() => 'up')const upAndDown$ = up$.merge(down$)

再来看看他们的 marble 图:

--d--d-d----d--d---|-->  (down$)
----u---u-u------u-|-->  (up$)merge--d-ud-du-u-d--d-u-|-->  (upAndDown$)

此时,我们再变换 upAndDown$ 。如果是 down 的话,则变换成 move$ ,否则变换成一个空的 stream 。
查看文档 Creation Operators ---> empty 。
Transformation Operators ---> switchMap 。

upAndDown$  .switchMap(action =>action === 'down' ? move$ : Rx.Observable.empty())

你要的 marble 图:

--d-ud-du-u-d--d-u-|-->  (upAndDown$)switchMap--m-em-me-e-m--m-e-|-->

其实这个 canvas 画板不用 RxJS 实现也不会很难。但是当我们把他扩展成一个「你画我猜」之后,用 RxJS 处理异步就会变得简单起来。比如,添加新的工具栏 ( 调色板,撤销…… ) ,即时通信 ( 同步画板,聊天 ) ……

另外,如果你想边学习 RxJS 边实现一些小东西的话:

  • staltz - rxjs training
  • GitHub - Who to Follow
  • RxJS 4.x Example
  • RxJs Playground
  • Yet Another RSS Reader
  • rx-ifying a chat room built with reactjs and socket io
  • angular2-hacknews

Production

怎么把 RxJS 应用到实际生产的 web 应用当中呢?
怎么结合到当前流行的框架当中呢?

Vue

你可以直接在各种 Lifecycle Hooks 中使用 RxJS 。

比如 created 的时候初始化一个 Observable ,beforeDestroy 时就取消订阅 Observable 。( 查看 DEMO )

new Vue({  el: '#app',data: {time: ''},created () {this.timer$ = Rx.Observable.interval(1000).map(() => new Date()).map(d => moment(d).format('hh:mm:ss')).subscribe(t => {this.time = t})},beforeDestroy () {this.timer$.unsubscribe()}
})

其实已经有对应的插件 vue-rx 帮我们干了上面的 dirty work 。他会分别在 init 和 beforeDestroy 的时候自动地订阅和取消订阅 Observable :Vue.js + RxJS binding mixin in 20 lines 。

因此,我们可以直接把一个 Observable 写到 data 中:vue-rx/example.html 。

React

类似地,React 也可以在他组件的 lifecycle hooks 里调用 RxJS:fully-reactive-react 。 也可以使用 rxjs-react-component 把 Observable 绑定到 state 。 如果你结合 Redux 的话,可以使用这个 redux-oservable 。

Angular2

RxJS 已经是 Angular2 的标配,不多说。
更多可查看对应的文档 Angular2 - Server Communication 。

更多关于 RxJS 的集成:RxJS community 。

You Might Not Need RxJS

根据 When to Use RxJS ,我们可以知道 RxJS 的适用场景是:

  • 多个复杂的异步或者事件组合在一起
  • 处理多个数据序列(有一定顺序)

我觉得,如果你没被异步问题困扰的话,那就不要使用 RxJS 吧,因为 Promise 已经能够解决简单的异步问题了。至于 Promise 和 Observable 的区别是什么呢?可以看Promise VS Observable 。

讲真,RxJS 在实际生产中适用的业务场景有哪些?哪些场景是需要多个异步组合在一起的?游戏吗?即时通信?还有一些特殊的业务。是我的写的业务太少了吗?还是我平时写业务的时候,为写而写,没有把他们抽象起来。

另外,我倒是对 Teambition 关于 RxJS 的思路有点感兴趣:xufei - 数据的关联计算 -> Brooooooklyn 评论 & xufei - 对当前单页应用的技术栈思考。

Summary

  • RxJS 是用来解决异步和事件组合问题
  • Observable = 异步数组 = 数组 + 时间轴 = stream
  • Operators = 分类别 + 画 marble 图 + 看例子 + 选
  • 更多更详细的更准确的请看文档!

让我们一起来学习 RxJS 吧!

推荐一篇学习Rx非常棒的文章!!!!!!相关推荐

  1. 【每周CV论文推荐】 深度学习人脸检测入门必读文章

    欢迎来到<每周CV论文推荐>.在这个专栏里,还是本着有三AI一贯的原则,专注于让大家能够系统性完成学习,所以我们推荐的文章也必定是同一主题的. 人脸图像是整个图像处理领域里面研究时间最长, ...

  2. fasterrcnn论文_【每周CV论文推荐】 深度学习人脸检测入门必读文章

    我的新书<深度学习之人脸图像算法>市了,欢迎大家关注! 言有三新书来袭,业界首次深入全面讲解深度学习人脸图像算法​mp.weixin.qq.com 欢迎来到<每周CV论文推荐> ...

  3. 老猿学5G扫盲贴:推荐三篇介绍HTTP2协议相关的文章

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 5G中的服务化接口调用都是基于HTTP2协议的,老 ...

  4. 王喆<深度学习推荐系统实战>之推荐模型篇学习笔记

    一.协同过滤 1.在 MovieLens 数据集中,不同用户对物品打分的标准不尽相同.比如说,有的人可能爱打高分,评价的影片得分都在 4 分以上,有的人爱打低分,大部分影片都在 3 分以下.你觉得这样 ...

  5. 推荐几篇图片隐写技术的文章

    这几天了解与学习图片隐写相关的技术点,在查找与阅读相关的文章后,选了三篇不错的文章推荐给大家. 说明:三篇文章是一个系列,专门讲图片隐写.内容全面,有原理介绍,还有实践操作指引,可所谓用心之作,很棒! ...

  6. 推荐一篇讲解各种debug技术的文章,相当不错!

    讲述debug的各种工具和技巧的文章,相当不错 ! http://blog.csdn.net/agan4014/archive/2008/03/20/2199790.aspx 转载于:https:// ...

  7. 【每周CV论文推荐】初学模型量化值得阅读的文章

    欢迎来到<每周CV论文推荐>.在这个专栏里,还是本着有三AI一贯的原则,专注于让大家能够系统性完成学习,所以我们推荐的文章也必定是同一主题的. 模型量化是非常重要的模型压缩方法,在工业界应 ...

  8. 推荐|深度学习领域引用量最多的前20篇论文简介

    来源:全球人工智能 作者:Pedro Lopez,数据科学家,从事金融与商业智能. 译者:海棠,审阅:袁虎. 深度学习是机器学习和统计学交叉领域的一个子集,在过去的几年里得到快速的发展.强大的开源工具 ...

  9. 送给前端的你,推荐几篇前端汇总文章。(来自知乎专栏)

    送给前端的你,推荐几篇前端汇总文章.(来自知乎专栏) 来源:https://zhuanlan.zhihu.com/p/22229868 作者:路人甲 链接:https://zhuanlan.zhihu ...

最新文章

  1. Vue静态资源的获取
  2. 【新星计划】MATLAB绘制图形
  3. docker 三种挂载文件路径方式
  4. 2020双11,Dubbo3.0 在考拉的超大规模实践
  5. 【Python2】Keras_ResNet 在Cifar10数据集上分类,Flask框架部署目标检测模型
  6. Kibana部署及配置(四)
  7. spring3.2 aop 搭建 (1)
  8. 计算机无法使用光驱启动,电脑BIOS怎么设置光盘启动 三种类型BIOS设置光驱启动的图文详解教程...
  9. 牛客题霸 [拼接所有的字符串产生字典序最小的字符串] C++题解/答案
  10. css实现鼠标覆盖显示大图
  11. 兼容IE8遇到的问题
  12. 数字图像处理实验六 图像复原
  13. vscode 代码片段如何输出$符号
  14. OpenCV开发笔记(五十一):红胖子8分钟带你深入了解透视变换(图文并茂+浅显易懂+程序源码)
  15. Coding and Paper Letter(七十五)
  16. 岁月温柔-21 妈妈转到省医院第2天
  17. local class incompatible: stream classdesc serialVersionUID = 4696092613551164015, local
  18. Java代码加密,Java加密方式,Java加密使用说明
  19. VSCode 使用ssh远程链接服务器时报错 Downloading VS Code Server failed
  20. PCA主成分分析(PCA降维)

热门文章

  1. 青岛大学计算机科学技术学院夏辉,张云红-青岛大学计算机科学技术学院
  2. android文档导航条跳来跳去,莫名其妙的Android导航栏
  3. 软件测试技术概念辨析
  4. python h_pythonh控制
  5. image-conversion库压缩png图片背景黑色问题解决
  6. ecology8流程表单常用js
  7. Python爬取暴走漫画动态图
  8. 后端服务器的压力测试
  9. DevOps的这些经典著作,你都看过了吗?
  10. Wordpress自媒体主题:JustNews 5.7.2(113p.cn)