dynsxyc 2019-07-01
RxJS是一个JavaScript库,用来编写异步和基于事件的程序。RxJS结合了观察者模式、迭代器模式和使用集合的函数式编程,以满足以一种理想方式来管理事件序列所需要的一切。
可以把RxJS当作用来处理事件的Lodash。
在现在的Web开发中,异步(Async)操作随处可见,比如使用ajax提交一个表单数据,我们需要等待服务端返回提交结果后执行后续操作,这就是一个典型的异步操作。虽然JavaScript为了方便开发者进行异步操作,提出了很多解决方案(callback,Promise,Async/await等等),但是随着需求愈加复杂,如何优雅的管理异步操作仍然是个难题。
此外,异步操作API千奇百怪,五花八门:
以上这些常用的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));
这就是全部代码,也许有些地方看不太懂 ,没关系,先不要着急,我们分步解读一下。
注意,这段代码我们使用的全部变量都是用const声明的,全部是不可变的,也即是变量声明时是什么值,就永远是什么值,就像定义函数一样。相对于传统的指令式编程,RxJS的代码就是由一个一个不可变的函数组成,每个函数只是对输入参数作出相应,然后返回结果,这样的代码写起来更加清爽,也更好维护。
RxJS结合了函数式和响应式这两种编程思想,为了更深入的了解RxJS,先来介绍一下什么是函数式编程和响应式编程。
函数式编程(Functional Porgramming)是一种编程范式,就像“面向对象编程”一样,是一种编写代码的“方法论”,告诉我们应该如何思考和解决问题。不同于面向对象编程,函数式编程强调使用函数来解决问题。
这里有两个问题:
与之对应的是命令式编程,也是最常见的编程模式。
举个例子,我们希望写个函数,把数组中的每个元素乘以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)
注意以上两个函数的返回结果都是一个新的数组,而并没有对原数组进行修改,这符合函数式编程的另外一个要求:纯函数。
纯函数是指满足以下两个条件的函数:
举个栗子:
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),比如以下这些:
使用纯函数可以大大增强代码的可维护性,因为固定输入总是返回固定输出,所以更容易写单元测试,也就更不容易产生bug。
数据不可变是函数式编程中十分重要的一个概念,意思是如果我们想改变一个变量的值,不是直接对这个变量进行修改,而是通过调用函数,产生一个新的变量。
如果你是一个前端工程师,肯定已经对数据不可变的好处深有体会。在JavaScript中,字符串(String),数字(Number)这两种类型就是不可变的,使用他们的时候往往不容易出错,而数组(Array)类型就是可变的, 使用数组的pop、push等方法都会改变原数组对象,从而引发各种bug。
注意,虽然ES6已经提出了使用const声明一个常量(不可变数据),但是这只能保证声明的对象的引用不可改变,而这个对象自身仍然可以变化。比如用const声明一个数组,使用push方法仍然可以像数组中添加元素。
和面向对象编程相比,面向对象编程更倾向把状态的改变封装到对象内部,以此让代码更清晰。而函数式编程倾向数据和函数分离,函数可以处理数据,但不改变原数据,而是通过产生新数据的方式作为运算结果,以此来尽量减少变化的部分,让我们的代码更清晰。
和函数式编程类似,响应式编程(Reactive Programming)也是一种编程的范式。从设计模式的角度来说,响应式编程就是“观察者模式”的一种有效实践。简单来说,响应式编程指当数据发生变化时,主动通知数据的使用者这个变化。
很多同学都使用过vue框架开发,vue中很出名的数据双向绑定就是基于响应式编程的设计思想实现的。当我们在通过v-bind绑定一个数据到组件上以后,不管这个数据何时发生变化,都会主动通知绑定过的组件,使我们开发时可以专注处理数据本身,而不用关心如何同步数据。
而在相应时编程里最出名的框架就是微软开发的Reactive Extension。这套框架旨在帮助开发者解决复杂的异步处理问题。我们的主角RxJS就是这个框架的JS版本。
安装
npm install rxjs
导入
import Rx from "rxjs";
请注意,这样导入会将整个RxJS库全部导入进来,而实际项目未必会用上Rxjs的全部功能,全部导入会让项目打包后变得非常大,我们推荐使用深链(deep link)的方式导入Rxjs,只导入用的上的功能,比如我们要使用Observable类,就只导入它:
import { Observable } from "rxjs/Observable";
实际项目中,按需导入是一个好办法,但是如果每个文件都写一堆import语句,那就太麻烦了。所以,更好的实践是用一个文件专门导入RxJS相关功能,其他文件再导入这个文件,把RxJS导入工作集中管理。
篇幅有限,下一讲将会讲解RxJS中几个核心概念,欢迎各位留言拍砖~