RxJS不完全指北(入门篇)

dynsxyc 2019-07-01

什么是RxJS?

RxJS是一个JavaScript库,用来编写异步基于事件的程序。RxJS结合了观察者模式迭代器模式使用集合的函数式编程,以满足以一种理想方式来管理事件序列所需要的一切。

可以把RxJS当作用来处理事件的Lodash

为什么要学Rxjs?

在现在的Web开发中,异步(Async)操作随处可见,比如使用ajax提交一个表单数据,我们需要等待服务端返回提交结果后执行后续操作,这就是一个典型的异步操作。虽然JavaScript为了方便开发者进行异步操作,提出了很多解决方案(callback,Promise,Async/await等等),但是随着需求愈加复杂,如何优雅的管理异步操作仍然是个难题。

此外,异步操作API千奇百怪,五花八门:

  1. DOM Events
  2. XMLHttpRequest
  3. fetch
  4. WebSockets
  5. Service Worker
  6. Timer
  7. ......

以上这些常用的API全部都是异步的,但是每个使用起来却完全不同,无形中给开发者增加了很大的学习和记忆成本。

使用RxJS可以很好的帮助我们解决上面两个问题,控制大量异步代码的复杂度,保持代码可读性,并统一API。

举个栗子:页面上有一个搜索框,用户可以输入文本进行搜索,搜索时要向服务端发送异步请求,为了减小服务端压力,前端需要控制请求频率,1秒最多发送5次请求,并且输入为空时不发送请求,最后将搜索的结果显示在页面上。

通常我们的做法是这样的,先判断输入是否为空,如果不为空,则构造一个截流函数来控制请求频率,这其中涉及到创建和销毁定时器,此外,由于每个请求返回时间不确定,如何获取最后一次搜索结果,需要构造一个栈来保存请求顺序, 想完美实现需求并不简单。

RxJS是如何解决这个问题的呢?请看下面的代码:

// 1.获取dom元素
const typingInput = document.querySelector("#typing-input"); // 输入
const typingBack = document.querySelector("#typing-back"); // 输出

// 2.模拟异步请求
const getData = value =>
  new Promise(resolve =>
    setTimeout(() => resolve(`async data ${value}`), 1000)
  );

// 3.RxJS操作
const source$ = fromEvent(typingInput, "input") // 创建事件数据流
.pipe( // 管道式操作
  map(e => e.target.value), // 获取输入的数据
  filter(i => i), // 过滤空数据
  debounceTime(200), // 控制频率
  switchMap(getData) // 转化数据为请求
);
// 4.输入结果
source$.subscribe(asyncData => (typingBack.innerHTML = asyncData));

这就是全部代码,也许有些地方看不太懂 ,没关系,先不要着急,我们分步解读一下。

  1. 使用选择器获取了两个dom元素,第一个是输入框,第二个是搜索结果的容器;
  2. 使用Promise来模拟一个异步请求的函数,1秒后返回请求结果;
  3. 这部分是RxJS操作,这里我们要先介绍一个概念,“数据流”(stream,简称“流”),“流”是RxJS中一种特殊的对象,我们可以想象数据流就像一条河流,而数据就是河里的水,顺流而下。代表“流”的变量一般用“$”结尾,这是RxJS编程的一种约定,被成为“芬兰式命名法”。
    代码中的source$就是输入框的输入事件产生的数据流,我们可以使用pipe方法,像搭建“管道”一样对流中的数据进行加工,先使用map函数将事件对象转化成输入值,然后使用fllter方法过滤掉无效的输入,接着使用debounceTime控制数据向下流转的频率,最后使用switchMap把输入值转化成异步请求,整个数据流就构建完成了。
  4. 最后我们使用数据流的subscribe方法添加对数据的操作,也就是将请求的结果输出到页面上。

    注意,这段代码我们使用的全部变量都是用const声明的,全部是不可变的,也即是变量声明时是什么值,就永远是什么值,就像定义函数一样。相对于传统的指令式编程,RxJS的代码就是由一个一个不可变的函数组成,每个函数只是对输入参数作出相应,然后返回结果,这样的代码写起来更加清爽,也更好维护

RxJS结合了函数式响应式这两种编程思想,为了更深入的了解RxJS,先来介绍一下什么是函数式编程和响应式编程。

函数式编程

函数式编程(Functional Porgramming)是一种编程范式,就像“面向对象编程”一样,是一种编写代码的“方法论”,告诉我们应该如何思考和解决问题。不同于面向对象编程,函数式编程强调使用函数来解决问题。

这里有两个问题:

  1. 任何语言都支持函数式编程么?并不是,能够支持函数式编程的语言至少要满足“函数是一等公民(First Class)”这个要求,意思是函数可以被赋值给一个变量,并且可以作为参数传递给另一个函数,也可以作为另一个函数的返回值。显然JavaScript满足这个条件。
  2. 函数式编程里的函数有什么特别之处?函数式编程里要求函数满足以下几个要求:声明式、纯函数、数据不可变

声明式(Declarative)

与之对应的是命令式编程,也是最常见的编程模式。

举个例子,我们希望写个函数,把数组中的每个元素乘以2,使用命令式编程,大概是这个样子的:

function double(arr) {
    const result = []
    for(let i=0,l=arr.length;i<l;i++) {
        result.push(arr[i] * 2)
    }
    return result
}

我们将整个逻辑过程完整描述了一遍,完美。

但如果又来了一个新需求,实现一个新函数,把数组中每个元素加1,简单,再来一遍:

function addOne(arr) {
    const result = []
    for(let i=0,l=arr.length;i<l;i++) {
        result.push(arr[i] + 1)
    }
    return result
}

是不是感觉哪里不对?double和addOne百分之九十的代码完全一样,“重复的代码是万恶之源。”我们应该想办法改进一下。

这里就体现了命令式编程的一个问题,程序按照逻辑过程来执行,但是很多问题都有相似的模式,比如上面的double和addOne。很自然我们想把这个模式抽象一下,减少重复代码。

接下来我们使用JavaScript的map函数来重写double和addOne:

function double(arr) {
    return arr.map(function(item) { return item * 2 })
}

function addOne(arr) {
    return arr.map(function(item) { return item + 1 })
}

重复代码全部被封装到map函数中。而我们需要做的只是告诉map函数应该如何映射数据,这就是声明式编程。相比较之前的代码,这样的代码更容易维护。

如果使用箭头函数,代码还可以进一步简化:

const double = arr => arr.map(item => item * 2)

const addOne = arr => arr.map(item => item + 1)

注意以上两个函数的返回结果都是一个新的数组,而并没有对原数组进行修改,这符合函数式编程的另外一个要求:纯函数

纯函数(Pure Function)

纯函数是指满足以下两个条件的函数:

  1. 相同的参数输入,返回相同的输出结果;
  2. 函数内不会修改任何外部状态,比如全局变量或者传入的参数对象;

举个栗子:

const arr = [1, 2, 3, 4, 5]

arr.slice(0, 3) // [1, 2, 3]

arr.slice(0, 3) // [1, 2, 3]

arr.slice(0, 3) // [1, 2, 3]

JavaScript中数组的slice方法不管执行几次,返回值都相同,并且没有改变任何外部状态,所以slice就是一个纯函数。

const arr = [1, 2, 3, 4, 5]

arr.splice(0, 3) // [1, 2, 3]

arr.splice(0, 3) // [4, 5]

arr.slice(0, 3) // []

相反,splice方法每次调用的结果就不同,因为splice方法改变了全局变量arr的值,所以splice就不是纯函数。

不纯的函数往往会产生一些副作用(Side Effect),比如以下这些:

  1. 改变全局变量;
  2. 改变输入参数引用对象;
  3. 读取用户输入,比如调用了alert或者confirm函数;
  4. 抛出一个异常;
  5. 网络I/O,比如发送了一个AJAX请求;
  6. 操作DOM;

使用纯函数可以大大增强代码的可维护性,因为固定输入总是返回固定输出,所以更容易写单元测试,也就更不容易产生bug。

数据不可变(Immutability)

数据不可变是函数式编程中十分重要的一个概念,意思是如果我们想改变一个变量的值,不是直接对这个变量进行修改,而是通过调用函数,产生一个新的变量。

如果你是一个前端工程师,肯定已经对数据不可变的好处深有体会。在JavaScript中,字符串(String),数字(Number)这两种类型就是不可变的,使用他们的时候往往不容易出错,而数组(Array)类型就是可变的, 使用数组的pop、push等方法都会改变原数组对象,从而引发各种bug。

注意,虽然ES6已经提出了使用const声明一个常量(不可变数据),但是这只能保证声明的对象的引用不可改变,而这个对象自身仍然可以变化。比如用const声明一个数组,使用push方法仍然可以像数组中添加元素。

和面向对象编程相比,面向对象编程更倾向把状态的改变封装到对象内部,以此让代码更清晰。而函数式编程倾向数据和函数分离,函数可以处理数据,但不改变原数据,而是通过产生新数据的方式作为运算结果,以此来尽量减少变化的部分,让我们的代码更清晰。

响应式编程

和函数式编程类似,响应式编程(Reactive Programming)也是一种编程的范式。从设计模式的角度来说,响应式编程就是“观察者模式”的一种有效实践。简单来说,响应式编程指当数据发生变化时,主动通知数据的使用者这个变化

很多同学都使用过vue框架开发,vue中很出名的数据双向绑定就是基于响应式编程的设计思想实现的。当我们在通过v-bind绑定一个数据到组件上以后,不管这个数据何时发生变化,都会主动通知绑定过的组件,使我们开发时可以专注处理数据本身,而不用关心如何同步数据。

而在相应时编程里最出名的框架就是微软开发的Reactive Extension。这套框架旨在帮助开发者解决复杂的异步处理问题。我们的主角RxJS就是这个框架的JS版本。

怎么使用RxJS

安装

npm install rxjs

导入

import Rx from "rxjs";

请注意,这样导入会将整个RxJS库全部导入进来,而实际项目未必会用上Rxjs的全部功能,全部导入会让项目打包后变得非常大,我们推荐使用深链(deep link)的方式导入Rxjs,只导入用的上的功能,比如我们要使用Observable类,就只导入它:

import { Observable } from "rxjs/Observable";

实际项目中,按需导入是一个好办法,但是如果每个文件都写一堆import语句,那就太麻烦了。所以,更好的实践是用一个文件专门导入RxJS相关功能,其他文件再导入这个文件,把RxJS导入工作集中管理

篇幅有限,下一讲将会讲解RxJS中几个核心概念,欢迎各位留言拍砖~

RxJS不完全指北(入门篇)

相关推荐