RxJS简易入门

什么是RxJS?RxJS解决什么样的问题?通过怎么样的手段?带着这些问题,我们便来学习一下RxJS,本文旨在帮助大家进行RxJS的入门。

官网上对以上问题进行了解释

RxJS is a library for composing asynchronous and event-based programs by using observable sequences. It provides one core type, the Observable, satellite types (Observer, Schedulers, Subjects) and operators inspired by Array#extras (map, filter, reduce, every, etc) to allow handling asynchronous events as collections

Think of RxJS as Lodash for events

ReactiveX combines the Observer pattern with the Iterator pattern and functional programming with collections to fill the need for an ideal way of managing sequences of events.

这里面可以把握到的几个关键词
asynchronous 异步
observable sequences 可观测的序列
observer 观察者
operators 操作符
array
lodash
observer pattern 观察者模式
iterator pattern 迭代器模式

因此我们至少可以在字面上得出下面这样一句话,并且解答以上的一个问题

RxJS是什么

RxJS是一个JS库,通过观察者模式以及迭代器模式,实现了一个可观测的对象序列并且提供了丰富的运算符,来帮助我们来处理异步问题,它可以看成用来处理异步(事件)的lodash

虽然原文用的是events,不过事件本身就是一种异步现象,或者我们可以认为网络请求,定时器这些就是在数据返回的时候给出了一个事件

如果你对异步的概念不是很理解得话,可以自行查找资料,笔者也在写一些关于异步的文章,可以作为参考
lodash是一个很有名的函数库,里面提供了非常多的函数,用来帮助我们处理数组,字符串,对象等。
这句话也稍稍说明了RxJS是怎么来解决异步问题的,不过暂时还让人比较难以理解,暂时把这些概念稍微记一下,通过对比的例子,我们再来认识这些概念,同时我们将会去探索另外一个问题的答案

RxJS解决了什么问题

我们先从一个很简单的需求入手

  1. 在一个输入框里面的内容改变后打印出来改变的内容
  <input type="text" id="input-test">

原生JS代码实现

    document.getElementById('input-test')
    .addEventListener('input', (e) => {
      console.log(e.target.value)
    })

RxJS代码实现

    rxjs.fromEvent(
      document.getElementById('input-test'), 'input'
    )
      .subscribe(e => console.log(e.target.value))

看起来好像没有什么很大的区别,也没有看到RxJS有什么作用。
不过通过这个例子我们可以解释一下上面的一些名词
rxjs.fromEvent 产生了一个 observable 对象
subscribe 里面的回调函数就是我们常说的 observer 观察者
而subscribe相当于观察者模式里面的订阅,用来连接观察者与被观察的对象

这里我们也就可以知道为什么说RxJS实现了观察者模式。他通过subscribe函数将一个观察者observer注册到了observable里面,当observable中有事件发生(数据产生)的时候,便会调用observer函数,执行其中的代码。

如果RxJS只能做到上面的这些事情,那么其实也没有什么大不了的,毕竟原生的DOM事件本身就是一种观察者模式的实现,我们为什么要画蛇添足再引入一个RxJS呢?接下来我们一步步的去复杂化这个需求,看看我们会遇到什么问题,对于这些问题原生的处理与RxJS的处理有什么不同

  1. 限制300ms防抖
    节流跟防抖我们都不会很陌生,下面我们分别使用原生代码以及RxJS来实现输入框的防抖功能

原生JS

    let time = null
    const interval = 300
    document.getElementById('input-test')
      .addEventListener('input', (e) => {
        time = Date.now()
        setTimeout(() => {
          if (Date.now() - time > interval) {
            console.log(e.target.value)
          }
        }, interval);
      })

RxJS代码的实现

    const interval = 300
    rxjs.fromEvent(
      document.getElementById('input-test'), 'input'
    ).pipe(
      rxjs.operators.map(e => e.target.value),
      rxjs.operators.debounceTime(interval),
    )
      .subscribe(e => console.log(e))

这里我们就可以很明显的感觉到,使用RxJS实现的代码要明显的清爽很多。
观测原生的代码,我们可以发现以下问题

  1. 具有全局变量
  2. 不可复用

当然以上的两个问题,我们可以通过封装写一个函数来进行解决,或者使用lodash等相关的库,例如

    document.getElementById('input-test')
      .addEventListener(
        'input',
        _.debounce(
          (e) => console.log('e', e.target.value),
          2000
        )
      )

如果自己去写函数,那么肯定会花费时间,单纯的需要使用节流防抖,lodash也能应付,不过依旧是那句话,RxJS能处理更多。
观察RxJS的代码,其重点在于operators 操作符。而在这里我们接触了两个操作符

  • map
  • debounceTime

在前面我们提到的关键词里面有一个array数组,而数组里面也有一个很常见的方法map,在这里操作符map跟数组的map的使用方式是一致的只不过一个的目标是数组,而另一个是observable对象,其都是对内容进行转化
而另外一个就是debounceTime,它跟我们的在lodash中使用的debounce函数的作用类似,区别只有名字不一样,而在RxJS中也有debounce这个操作符,debounceTime是因为我们这种需求太常见了,所以单独抽了出来,通过使用dobounce可以实现debounceTime,当然RxJS中的debounce远比lodash中的dobounce以及自身的debounceTime要强大得多,比如在这个例子中,可以根据输入值的长度来设置不同的dobounce,在我们输入第一个字符的时候,假如1s内我们不进行输入那么就会进行输出,但是假如我们在1s内输入了第二个字符,那么就必须等到2s后才会进行输出了,不过在实际的使用过程中,最常见的肯定还是debounceTime。

通过这两个操作符,我们可以知道为什么RxJS被称为异步的lodash,他跟lodash一样,提供了非常多的封装好的操作符(方法),通过这些操作符可以方便我们对异步的处理,提高我们的开发效率,提升我们代码的可维护性

上面还有一个陌生的方法,pipe 管道,这个并不是操作符而是observable对象的一个方法,他跟我们后面要提到的一个概念 数据流 息息相关,在这里我们先暂时略过,因为在早期的版本中,要使用操作符是可以直接进行链式调用的

observable.map().dobounceTime.xxx.subscribe

在RxJS的v6版本进行全面的优化,必须通过pipe来使用操作符,虽然看起来麻烦了很多,但是很明显,使用pipe进行操作符的调用,假如我们想对一堆操作符进行组合,我们只需要实现一个数组就好了,而链式调用我们则可能需要去改动原型之类的,并不是很方便,通过pipe可以更加方便的对多个操作符进行封装

RxJS可以提供的异步操作符远远不止一个dobounceTime,一个map这么的简单,即使像lodash这么有名的库,也只是封装了throttle,dobounce两个跟时间跟异步相关的函数,而RxJS能做出这么多的操作符是因为他站在了一个与lodash完全不同的角度来看待问题的。

接下来我们再去复杂化我们的例子,看看我们还会遇到什么问题,以及RxJS提供了哪些操作符来解决这些问题。

  1. 增加两个输入框,必须在三个输入框内都输入内容以后才会进行打印

html代码

<input type="text" id="input-test">
<input type="text" id="input-test2">

原生JS代码

    let time = [0, 0]
    const interval = 300
    const flagArr = [false, false]
    let valueArr = ['', '']
    const print = (index, e) => {
      time[index] = Date.now()
      setTimeout(() => {
        if (Date.now() - time[index] > interval) {
          flagArr[index] = true
          valueArr[index] = e.target.value
          if (flagArr.every(v => v)) {
            console.log(valueArr.join('/'));
          }
        }
      }, interval);
    }
    document.getElementById('input-test')
      .addEventListener('input', (e) => {
        print(0, e)
      })
    document.getElementById('input-test2')
      .addEventListener('input', (e) => {
        print(1, e)
      })

如果说前面的代码还让你觉得这都不是事得话,我觉得你看到这部分代码可能多多少少会有点头疼了,这里还是已经进行过一部分抽象的结果了,实际上我相信很多人会直接复制粘贴一遍相关的内容~~

  • 四个全局变量
  • 一个函数
  • 相互交错的代码

这样的代码假如出bug了是比较难定位bug的。
我们再来看看RxJS的代码实现

    const interval = 300
    const operatorList = [
      rxjs.operators.map(e => e.target.value),
      rxjs.operators.debounceTime(interval),
    ]
    rxjs.combineLatest(
      rxjs.fromEvent(
        document.getElementById('input-test'), 'input'
      ).pipe(...operatorList),
      rxjs.fromEvent(
        document.getElementById('input-test2'), 'input'
      ).pipe(...operatorList),
    ).pipe(
      rxjs.operators.map(arr => arr.join('/')),
    )
      .subscribe(e => console.log(e))

至少从效果上来看,我们省去了两个全局被交叉引用的变量。
并且更加重要的是,我们将两个observable对象或者说两条数据流进行了合并,产生了一条新的数据流
在这里我们可以看到,使用RxJS处理有多个数据源,或者说事件的产生对象的情况,会变得比较的方便。

  1. 调用接口,接口返回内容,最终将接口中返回的内容打印到div上,注意,此时的网络不稳定

这个看起来很简单,但是实际上,因为网络是异步的,那么完全存在,先发出去的请求后返回的可能,最终导致在页面上的显示不正确,在tab页上的处理上,就很有可能会遇到这种情况。
例如简书的关注界面,在这个界面我们很快速的点击左边两个人的头像,那么很有可能出现虽然左边显示的A,但是右边显示B的文章的情况

错误的显示

下面我们分别使用原生JS以及RxJS来处理这种情况

  <input type="text" id="input-test">
  <input type="text" id="input-test2">
  <div id="text"></div>

给出一个函数用来模拟接口

const getData = (v) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(v + 'test')
    }, Math.random() * 3000);
  });
}

通过随机函数我们模拟接口的不稳定返回
假如我们是这样编写处理接口返回的代码

    const print = async (index, e) => {
      time[index] = Date.now()
      setTimeout(async () => {
        if (Date.now() - time[index] > interval) {
          flagArr[index] = true
          valueArr[index] = e.target.value
          if (flagArr.every(v => v)) {
            const v = await getData(valueArr.join('/'))
         document.getElementById('text').innerHTML = v
          }
        }
      }, interval);
    }

那么很明显,假如某个接口比它后发生的接口慢了,最终将会造成显示错误
所以我们需要添加标记物来对每个接口都进行记录

    let time = [0, 0]
    const interval = 50
    const flagArr = [false, false]
    let valueArr = ['', '']
    let promiseFlag = 0

    const print = async (index, e) => {
      time[index] = Date.now()
      ++promiseFlag
      let currentPromise = promiseFlag
      setTimeout(async () => {
        if (Date.now() - time[index] > interval) {
          flagArr[index] = true
          valueArr[index] = e.target.value
          if (flagArr.every(v => v)) {
            const v = await getData(valueArr.join('/'))
            if (promiseFlag === currentPromise) {
              document.getElementById('text').innerHTML = v
            }
          }
        }
      }, interval);
    }

而RxJS的代码如何实现的呢

    rxjs.combineLatest(
      rxjs.fromEvent(
        document.getElementById('input-test'), 'input'
      ).pipe(...operatorList),
      rxjs.fromEvent(
        document.getElementById('input-test2'), 'input'
      ).pipe(...operatorList),
    ).pipe(
      rxjs.operators.map(arr => arr.join('/')),
      rxjs.operators.switchMap((v) => getData(v)),
    )
      .subscribe(v => document.getElementById('text').innerHTML = v)

依旧是一个操作符搞定的事情,switchMap可以始终获取最新的数据流而不需要我们进行自我的标记,而switchMap其实跟我们上面使用的combineLatest同属于合并类的操作符,在我们又接触到一个点,Promise对象是可以转换成为一个observable对象的

现在再回过头来看这些代码,我们可以发现当我们遇到以下几种情况的时候是可以考虑一下RxJS的

  1. 当数据的来源有多个的时候
  2. 当数据处理与事件与时间有关的时候
  3. 当我们觉得自己的异步代码不够优雅的时候
  4. 当我们想对某些异步操作进行封装的时候

对于前两点我们可以很明显的看到,使用RxJS在处理这些复杂的情况可以很方便的进行处理,对异步的支持远远不止提供了节流防抖,对于第三点,我们可以很明显的看到,使用了RxJS,我们的代码的中间变量减少了很多。而第四点,大家可以思考一下怎么对上面的代码进行封装了~看看是原生的代码好封装,还是使用了RxJS的代码好封装一点,上面的功能去掉两个输入框(其实加上也没有什么关系)很容易就变成了一个搜索框的需求了。

通过以上的例子,我们可以得出RxJS到底能帮助我们做什么,它确确实实是能够帮助我们解决问题的,那么我们的最后一个问题就来了。

RxJS是如何帮助我们解决问题的

其实通过上面的例子的学习,相信大家对操作符有了一个大概的概念,但是在上面我们还留下了一个问题,什么是数据流

一个observable对象就是一个数据流

以上这句话其实挺废话的,我个人更加倾向于,数据流是加上了时间的数组,而通过操作符,我们可以得到另外的数据流

不过这样还是有点抽象,为什么一个observable对象就是一个数据流?什么叫做给数组加上了时间?
我们以一个很常见的创建类的操作符interval作为例子

import { interval } from "rxjs";

interval(1000).subscribe(x => console.log(x))

这句代码会在每1000ms进行一次打印

  1. 1000ms 打印 0
  2. 2000ms 打印 1
  3. 3000ms 打印 2

...

以此类推,对这样的数据我们可以画一张图来进行表示

image

这就是为什么我们称为数据流的原因
相比较于数组

  1. 数组是有长度的,而数据流可以没有,也就是说数据流可以永不完结
  2. 对数组进行的操作同步的,而数据流可以是异步的,其实通过一些操作符可以产生同步的数据流

当数据产生的数据,就会传递给subscribe中的observer函数,从而执行对应的操作,而不同的observable对象控制数据的产生频率也是不一样的
上面这种图被称为弹珠图,也有人称为宝珠图,通过这样的图片,我们可以很清晰的看到每个数据是在什么时候产生的,而弹珠图也远远不止如此,我们回答下面这个问题

operator跟pipe是什么?

我们可以这样理解,数据流中的对象传入管道pipe,经过operator操作符的作用,最终产生了新的数据流

import { interval } from "rxjs";
import { map } from "rxjs/operators";

interval(1000).pipe(
  map(x => x * 10)
).subscribe(x => console.log(x))

其对应的弹珠图如下


image

(这个其实我拿的官网上的图,对应上面的代码得话第一个应该是0,第二个是1以此类推,并且没有完结标记)
我们可以看到弹珠图其实可以是二维的,经过管道以及操作符,最终产生了新的数据流,而observer中接受到的数据依旧是新的数据流的数据了

  1. 1000ms 0(10)
  2. 2000ms 10(20)
  3. 3000ms 30(30)

当然也可以通过更多的操作对齐进行对应的操作


image

并且,并不是所有的数据最后都会通过管道,他们有可能被进行了过滤,比如我们的节流以及防抖就过滤掉了很多我们不需要的数据,而这些数据最终出来以后,也不见得会是原来的样子了,他们可能会被进行转化,甚至一个管道可以跟另外一个数据流进行合并,最终产生来自两个来源的数据,但是对于我们的观察者来说,他们之关心当数据产生的时候,要做什么,这样就实现了解耦合
此外,将复杂的工作分发给了不同的操作符,也使得我们的代码更加的清晰已读。

其实加入你实现过一次观察者模式,比如如下的代码


class Store {
  constructor(state) {
    this.state = state
    this.observers = []
  }
  getState() {
    return this.state
  }
  setState(state) {
    this.state = Object.assign({}, this.state, state)
    // 通知观察者
    this._notifyAllObservers()
  }
  // 应该是一个内部方法
  _notifyAllObservers() {
    this.observers.forEach(observer => {
      observer.update(this.state)
    })
  }
  registere(name, update) {
    this.observers.push({
      name,
      update
    })
  }
  unRegistered(name) {
    this.observers = this.observers.filter(v => v.name !== name)
  }
  clear() {
    this.observers = []
  }
}

就很容易发现一个事情,虽然我们对观察者模式的描述经常是
当目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新
很像我们平时的时候接到电话然后再去决定自己要去做什么。但是其实看实现我们更加应该这样说
告诉一仆人当这件事情发生的时候你应该做什么,然后让这个仆人去被观察的对象哪里待着,等到这个事情发生的时候就去做
这里就可以说明,为什么在上文中我们会写出一个这么复杂的回调函数出来,因为这个仆人他既需要记录时间,有需要调用接口查看标记物,还需要查看其它的东西状态(仆人:我太难了),并且因为每次事件产生的时候你都需要创建一个仆人(observer),所以全局变量也就变成必须的东西

而使用RxJS加上操作符,其实本质上还是观察者模式,但是我个人觉得是对观察者模式的一种封装,其实我们自己如果去对上面的内容进行封装得话,一些比较挫的办法就是一个大的闭包,然后if else,observable内部肯定也是一个大的闭包。但是RxJS与普通的观察者模式不同的是,它内置了一个观察者,我们可以认为这个观察者就做了一个事情,用一个手机外面发短息
也就是说我们原先只能用一个仆人去做的事情现在可以分摊给多个了,一个仆人用来判断时间是否正确,一个用来判断接口返回是不是最新的,这样我们的业务逻辑就进行了解耦合,并且这些仆人还可以多次利用,使得我们的排列组合更加的自由,而前面我们提到的全局变量这些,其实RxJS就以及给我们处理好了,使得我们可以更加去关注业务逻辑,而不是具体的实现。

那么到这里我们终于知道了前面说的

实现了一个可观测的对象序列并且提供了丰富的运算符
到底是什么意思,他们为什么能够帮助我们解决问题。

尾声

对于RxJS的学习,这篇文章就暂时讲到这里,这篇文章我弱化了RxJS的很多概念,因为RxJS的概念说多也不多说少也不少,而系统的学习得话肯定会把操作符都过一遍,那么就很容易让人陷入,这个操作符在实际使用中到底有什么作用的困惑之中,个人觉得学习一门新的技术,一开始还是应该去了解它到底解决了什么样子的问题,再去一步步深入技术细节会比较得好
所以最后,只需要你能看到我们现在遇到的一些问题,并且能够知道RxJS使用了一个数据流的概念加上很多的操作符来解决这些问题,我感觉这篇文章就差不多了。
不过在实际的使用之中,我并不觉得,学习了RxJS就一定要去使用它,比如lodash中的节流防抖,在vue中其实并没有比RxJS难用,甚至简单的场景下完全更加的好用,主要还是看你需不需要它。

学习资料以及参考

有兴趣可以自行学习了

RxJS官网
不管看再多的官网外的知识,官网的内容一般是最准确的,特别是对一些操作符的描述

一个介绍各自操作符的弹珠图的网站
与官网不同的是,这个网站的弹珠图是动态可以拖动的

深入浅出RxJS
虽然是v5的版本,但是也是唯一一本中文的关于RxJS的出版书籍,

30天精通RxJS
一个台湾人写得RxJS教程,都是繁体字,可以自行搜索一下简书掘金上的简体字文章

RxJS官方的资源库
其实是官网的一部分,不过很多人可能都没注意到,实在是有点可惜

本文参考代码
此外,在RxJS的源代码中也存在很多的exmple实例
可以进行参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容