介绍
# 概念
RxJS
是 Reactive Extensions for JavaScript
的缩写,起源于 Reactive Extensions
,是一个基于可观测数据流 Stream
结合观察者模式和迭代器模式的一种异步编程的应用库。RxJS
是 Reactive
Extensions
在 JavaScript
上的实现。
注意!它跟
React
没啥关系,笔者最初眼花把它看成了React.js
的缩写(耻辱啊!!!)
对于陌生的技术而言,我们一般的思路莫过于,打开百度(google),搜索,然后查看官方文档,或者从零散的博客当中,去找寻能够理解这项技术的信息。但在很多时候,仅从一些只言片语中,的确也很难真正了解到一门技术的来龙去脉。
本文将从学习的角度来解析这项技术具备的价值以及能给我们现有项目中带来的好处。
# 背景
从开发者角度来看,对于任何一项技术而言,我们经常会去谈论的,莫过于以下几点:
- 应用场景?
- 如何落地?
- 上手难易程度如何?
- 为什么需要它?它解决了什么问题?
针对以上问题,我们可以由浅入深的来刨析一下RxJS
的相关理念。
# 应用场景?
假设我们有这样一个需求:
我们上传一个大文件之后,需要实时监听他的进度,并且待进度进行到100的时候停止监听。
对于一般的做法我们可以采用短轮询的方式来实现,在对于异步请求的封装的时候,如果我们采用Promise
的方式,那么我们一般的做法就可以采用编写一个用于轮询的方法,获取返回值进行处理,如果进度没有完成则延迟一定时间再次调用该方法,同时在出现错误的时候需要捕获错误并处理。
显然,这样的处理方式无疑在一定程度上给开发者带来了一定开发和维护成本,因为这个过程更像是我们在观察一个事件,这个事件会多次触发并让我感知到,不仅如此还要具备取消订阅的能力,Promise
在处理这种事情时的方式其实并不友好,而RxJS
对于异步数据流的管理就更加符合这种范式。
引用尤大的话:
我个人倾向于在适合
Rx
的地方用Rx
,但是不强求Rx for everything
。比较合适的例子就是比如多个服务端实时消息流,通过Rx
进行高阶处理,最后到view
层就是很清晰的一个Observable
,但是view
层本身处理用户事件依然可以沿用现有的范式。
# 如何落地?
针对现有项目来说,如何与实际结合并保证原有项目的稳定性也的确是我们应该优先考虑的问题,毕竟任何一项技术如果无法落地实践,那么必然给我们带来的收益是比较有限的。
这里如果你是一名使用
Angular
的开发者,或许你应该知道Angular
中深度集成了Rxjs
,只要你使用Angular
框架,你就不可避免的会接触到RxJs相关的知识。
在一些需要对事件进行更为精确控制的场景下,比如我们想要监听点击事件(click event),但点击三次之后不再监听。
那么这个时候引入RxJS
进行功能开发是十分便利而有效的,让我们能省去对事件的监听并且记录点击的状态,以及需要处理取消监听的一些逻辑上的心理负担。
你也可以选择为你的大型项目引入RxJS
进行数据流的统一管理规范,当然也不要给本不适合RxJS
理念的场景强加使用,这样实际带来的效果可能并不明显。
# 上手难易程度如何?
如果你是一名具备一定开发经验的JavaScript
开发者,那么几分钟或许你就能将RxJS
应用到一些简单的实践中了。
# 为什么需要它?它解决了什么问题?
如果你是一名使用JavaScript
的开发者,在面对众多的事件处理,以及复杂的数据解析转化时,是否常常容易写出十分低效的代码或者是臃肿的判断以及大量脏逻辑语句?
不仅如此,在JavaScript
的世界里,就众多处理异步事件的场景中来看,“麻烦”两个字似乎经常容易被提起,我们可以先从JS
的异步事件的处理方式发展史中来细细品味RxJS
带来的价值。
# 回调函数时代(callback)
使用场景:
- 事件回调
Ajax
请求Node API
setTimeout
、setInterval
等异步事件回调
在上述场景中,我们最开始的处理方式就是在函数调用时传入一个回调函数,在同步或者异步事件完成之后,执行该回调函数。可以说在大部分简单场景下,采用回调函数的写法无疑是很方便的,比如我们熟知的几个高阶函数:
forEach
map
filter
[1, 2, 3].forEach(function (item, index) {
console.log(item, index);
})
2
3
他们的使用方式只需要我们传入一个回调函数即可完成对一组数据的批量处理,很方便也很清晰明了。
但在一些复杂业务的处理中,我们如果仍然秉持不抛弃不放弃的想法顽强的使用回调函数的方式就可能会出现下面的情况:
fs.readFile('a.txt', 'utf-8', function(err, data) {
fs.readFile('b.txt', 'utf-8', function(err, data1) {
fs.readFile('c.txt', 'utf-8', function(err, data2) {
// ......
})
})
})
2
3
4
5
6
7
当然作为编写者来说,你可能觉得说这个很清晰啊,没啥不好的。但是如果再复杂点呢,如果调用的函数都不一样呢,如果每一个回调里面的内容都十分复杂呢。短期内自己可能清楚为什么这么写,目的是什么,但是过了一个月、三个月、一年后,你确定在众多业务代码中你还能找回当初的本心吗?
你会不会迫不及待的查找提交记录,这是哪个憨批写的,跟
shit
......,卧槽怎么是我写的。
这时候,面对众多开发者苦不堪言的回调地域
,终于还是有人出来造福人类了......
# Promise时代
Promise
最初是由社区提出(毕竟作为每天与奇奇怪怪的业务代码打交道的我们来说,一直用回调顶不住了啊),后来官方正式在ES6
中将其加入语言标准,并进行了统一规范,让我们能够原生就能new
一个Promise
。
就优势而言,Promise
带来了与回调函数不一样的编码方式,它采用链式调用,将数据一层一层往后抛,并且能够进行统一的异常捕获,不像使用回调函数就直接炸了,还得在众多的代码中一个个try catch
。
话不多说,看码!
function readData(filePath) {
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf-8', (err, data) => {
if (err) reject(err);
resolve(data);
})
});
}
readData('a.txt').then(res => {
return readData('b.txt');
}).then(res => {
return readData('c.txt');
}).then(res => {
return readData('d.txt');
}).catch(err => {
console.log(err);
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
对比一下,这种写法会不会就更加符合我们正常的思维逻辑了,这种顺序下,让人看上去十分舒畅,也更利于代码的维护。
优点:
- 状态改变就不会再变,任何时候都能得到相同的结果
- 将异步事件的处理流程化,写法更方便
缺点:
- 无法取消
- 错误无法被
try catch
(但是可以使用.catch
方式) - 当处于
pending
状态时无法得知现在处在什么阶段
虽然Promise
的出现在一定程度上提高了我们处理异步事件的效率,但是在需要与一些同步事件的进行混合处理时往往我们还需要面临一些并不太友好的代码迁移,我们需要把原本放置在外层的代码移到Promise
的内部才能保证某异步事件完成之后再进行继续执行。
# Generator函数
ES6
新引入了 Generator
函数,可以通过 yield
关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而为异步编程提供解决方案。形式上也是一个普通函数,但有几个显著的特征:
function
关键字与函数名之间有一个星号 "*" (推荐紧挨着function
关键字)- 函数体内使用
yield· 表达式,定义不同的内部状态 (可以有多个
yield`) - 直接调用
Generator
函数并不会执行,也不会返回运行结果,而是返回一个遍历器对象(Iterator Object
) - 依次调用遍历器对象的
next
方法,遍历Generator
函数内部的每一个状态
function* read(){
let a= yield '666';
console.log(a);
let b = yield 'ass';
console.log(b);
return 2
}
let it = read();
console.log(it.next()); // { value:'666',done:false }
console.log(it.next()); // { value:'ass',done:false }
console.log(it.next()); // { value:2,done:true }
console.log(it.next()); // { value: undefined, done: true }
2
3
4
5
6
7
8
9
10
11
12
这种模式的写法我们可以自由的控制函数的执行机制,在需要的时候再让函数执行,但是对于日常项目中来说,这种写法也是不够友好的,无法给与使用者最直观的感受。
# async / await
相信在经过许多面试题的洗礼后,大家或多或少应该也知道这玩意其实就是一个语法糖,内部就是把Generator
函数与自动执行器co
进行了结合,让我们能以同步的方式编写异步代码,十分畅快。
有一说一,这玩意着实好用,要不是要考虑兼容性,真就想大面积使用这种方式。
再来看看用它编写的代码有多快乐:
async readFileData() {
const data = await Promise.all([
'异步事件一',
'异步事件二',
'异步事件三'
]);
console.log(data);
}
2
3
4
5
6
7
8
9
10
直接把它当作同步方式来写,完全不要考虑把一堆代码复制粘贴的一个其他异步函数内部,属实简洁明了。
# RxJS
它在使用方式上,跟Promise
有点像,但在能力上比Promise
强大多了,不仅仅能够以流的形式对数据进行控制,还内置许许多多的内置工具方法让我们能十分方便的处理各种数据层面的操作,让我们的代码如丝一般顺滑。
优势:
- 代码量的大幅度减少
- 代码可读性的提高
- 很好的处理异步
- 事件管理、调度引擎
- 十分丰富的操作符
- 声明式的编程风格
function readData(filePath) {
return new Observable((observer) => {
fs.readFile(filePath, 'utf-8', (err, data) => {
if (err) observer.error(err);
observer.next(data);
})
});
}
Rx.Observable
.forkJoin(readData('a.txt'), readData('b.txt'), readData('c.txt'))
.subscribe(data => console.log(data));
2
3
4
5
6
7
8
9
10
11
12
13
这里展示的仅仅是RxJS
能表达能量的冰山一角,对于这种场景的处理办法还有多种方式。RxJS
擅长处理异步数据流,而且具有丰富的库函数。对于RxJS
而言,他能将任意的Dom
事件,或者是Promise
转换成observables
。