最近一直忙于工作,也没倒开时间写博客,组里技术leader让小编去做一次RxJS的技术分享,说时迟那时快果断就打开博客准备先写一遍中文版,之后译成英文版发到公司confluence上,话不多少,开始吧~
一.基本概念
1.简介
- Rx全名:Reactive Extension
- 源自于微软,火于NetFlix
- 优势:在思考的维度上加入时间考量
2.Js Bin运行例子
fromEvent案例
- HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://unpkg.com/@reactivex/[email protected]/dist/global/Rx.js"></script>
</head>
<body>
<input id="height" type="number">
</body>
</html>
- ES6/Babel
const height = document.getElementById('height');
// 将Keyup事件变成Observable
// 约定俗成的写法会将流状的内容加一个$命名
const height$ = Rx.Observable.fromEvent(height, 'keyup');
height$.subscribe(val=> console.log(val.target.value + ' ' + new Date());
- Console
"1 Sun Jan 12 2020 14:24:06 GMT+0800 (中国标准时间)"
"12 Sun Jan 12 2020 14:24:07 GMT+0800 (中国标准时间)"
"123 Sun Jan 12 2020 14:24:07 GMT+0800 (中国标准时间)"
"1234 Sun Jan 12 2020 14:24:08 GMT+0800 (中国标准时间)"
combineLatest案例
- HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://unpkg.com/@reactivex/[email protected]/dist/global/Rx.js"></script>
</head>
<body>
<div>
<input id="length" type="number">
</div>
<div>
<input id="width" type="number"/>
</div>
<div id="area"></div>
</body>
</html>
- ES6/Babel
const length = document.getElementById('length');
const width = document.getElementById('width');
const area = document.getElementById('area');
// 将Keyup事件变成Observable
// 约定俗成的写法会将流状的内容加一个$命名
const length$ = Rx.Observable.fromEvent(length, 'keyup').pluck('target', 'value');
const width$ = Rx.Observable.fromEvent(width, 'keyup').pluck('target', 'value');
// 为了计算结果的话,需要进行合并
// combineLatest表示两个流中只要有一个有最新的值,就重新计算一遍
// zip 表示两个流都需要有新值出现,才会重新计算一次
const area$ = Rx.Observable.combineLatest(length$, width$, (l, w) => {
return l*w;
});
area$.subscribe(val => {
area.innerHTML = val
});
- 思想
length: ----- 1 ------------- 3
width: ----------- 2 -------
area: ----------- (2,1) -- (2,3)
\ \
--------------2--------6------
3.总结
- Rx和传统编程的区别在于要把任何变化想象成以时间为维度的和事件流
- 通过监听流的方法可以获得流中对应的数据,而不是自己去再次获取
二.常见操作符
1.常见创建类操作符
- from可以把数组、Promise、以及Iterator转换为Observable
- fromEvent可以把事件转化为Observable
- of接受一系列的数组,并把它们emit出去
- Interval
// interval每隔1s发射一个值,从0开始,发射的是位置数
// take只保留前三个数据,就触发complete方法
const interval$ = Rx.Observable.interval(1000).take(3);
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
});
- Timer
// timer的第一个参数是开始延迟多少毫秒发射,第二个参数是每次发射间隔是多少毫秒
// 如下事件会在开始后的2秒开始输出0,之后间隔1秒输出1 2 3 ...
// 如果只设置第一个参数则只会发射出一次
const timer$ = Rx.Observable.timer(2000, 1000)
timer$.subscribe(v=> console.log(v))
2.常见转换操作符
- map:对原始流中的元素进行处理,映射成另一个元素
xxx.map(event=> event.target.value)
- mapTo:使用场景,不关心流中的值,只关心事件是否触发(流是否发生改变)就行
const a$ = Rx.Observable.fromEvent(length, 'keyup').mapTo(1);
// 相当于.map(_=>1)
const b$ = Rx.Observable.fromEvent(width, 'keyup').mapTo(2);
Rx.Observable.combineLatest(a$, b$, (a, b) => a*b);
// 不管流内的lenth/width怎么变化,a$的值都是1,b$的值的都是2
- pluck:从对应的流对象的值中获取对应的属性值
xxx.pluck('target', 'value')
3.常见工具类操作符
- do:可以作为跟外部交互的桥梁,对相关内容进行处理
// interval每隔1s发射一个值,从0开始,发射的是位置数
// do作为中间桥梁,可以做subscribe做的内容,因为subscribe之后我们就无法使用操作符再进行处理了
// take只保留前三个数据,就触发complete方法
const interval$ = Rx.Observable
.interval(1000)
.do( v => console.log(`当前值是:${v}`))
.take(3);
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
输出内容
"当前值是:0"
0
"当前值是:1"
1
"当前值是:2"
2
"Complete"
4.常见变化类操作符scan
- scan:第一个入参是上一个流元素处理后的内容,第二个入参是自定义操作内容参数,return的内容将作为下一个scan的第一个入参
const interval$ = Rx.Observable
.interval(1000)
.filter( v => v%2===0) // 只保留是偶数的流数据
.scan((x,y) => { // x为上一个return的值
return x+y;
})
.map(x => x+1)
.take(4);
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
输出内容
1
3
7
13
"Complete"
5.常见数学类操作符reduce
- reduce:可以通过reduce返回方法计算每一个流发射的元素值得到最终的值,即每一个流发射的元素需要有限个,否则又是一个Never类型的Observable
const interval$ = Rx.Observable
.interval(1000)
.filter( v => v%2===0) // 只保留是偶数的流数据
.take(4)
.reduce((x,y) => { // x为上一个return的值
return x+y;
});
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
输出内容
12
"Complete"
6.过滤类操作符
- filter:设置根据某种条件过滤流的作用
// interval每隔1s发射一个值,从0开始,发射的是位置数
// filter过滤条件保留指定流内容
// do作为中间桥梁,可以做subscribe做的内容,因为subscribe之后我们就无法使用操作符再进行处理了
// take只保留前三个数据,就触发complete方法
const interval$ = Rx.Observable
.interval(1000)
.filter( v => v%2===0) // 只保留是偶数的流数据
.do( v => console.log(`当前值是:${v}`))
.take(3);
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
输出内容
"当前值是:0"
0
"当前值是:2"
2
"当前值是:4"
4
"Complete"
- take:参数设置保留前几个流的作用
- first/last:只保留流的第一个发射内容 / 最后一个发射内容
const interval$ = Rx.Observable
.interval(1000)
.filter( v => v%2===0) // 只保留是偶数的流数据
.do( v => console.log(`当前值是:${v}`))
.first();
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
输出内容
"当前值是:0"
0
"Complete"
- skip:过滤几个元素再发射
const interval$ = Rx.Observable
.interval(1000)
.filter( v => v%2===0) // 只保留是偶数的流数据
.do( v => console.log(`当前值是:${v}`))
.skip(2);
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
输出内容[因为有do所以会输出 当前值是x 的内容,但没发射对应元素]
"当前值是:0"
"当前值是:2"
"当前值是:4"
4
"当前值是:6"
6
"当前值是:8"
8
"当前值是:10"
10
- debounce:可以想象成一个滤波器(整流器),希望得到的流是符合时间间隔的,不符合的就抛弃掉,参数接的是Observable对象,比debounceTime更灵活,可参见debounceTime的样例代码
- debounceTime:可以想象成一个基于时间的滤波器,希望得到的流是符合时间间隔的,不符合的就抛弃掉[通常用于搜索的时候发送网络请求]
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://unpkg.com/@reactivex/[email protected]/dist/global/Rx.js"></script>
</head>
<body>
<div>
<input id="length" type="number">
</div>
</body>
</html>
const length = document.getElementById('length');
// 使用debounceTime将2秒内的流发射过滤掉,保留最后的发射内容
// 即只有停下来了2秒了,内容才会输出
const length$ = Rx.Observable
.fromEvent(length, 'keyup')
.pluck('target', 'value')
.debounceTime(2000);
// 相当于.debounce(()=> Rx.Observable.interval(300));
length$.subscribe(val => console.log(val));
输出内容
当输入停下2秒后,输出输入框中的内容
- distinct:只保留整个流中不一样的内容,重复的内容抛弃掉
const length = document.getElementById('length');
const length$ = Rx.Observable
.fromEvent(length, 'keyup')
.pluck('target', 'value')
.distinct();
length$.subscribe(val => console.log(val));
输出内容
在输入框中分别输入
1
12
123
12
1
1234
----------输出结果为----------
1
12
123
1234
-----即重复的内容不会输出
- distinctUntilChanged:只跟前一个元素比,如果前一个元素是黄色,如下一个也是黄色就抛弃掉,如是其他颜色则发射
const length = document.getElementById('length');
const length$ = Rx.Observable
.fromEvent(length, 'keyup')
.pluck('target', 'value')
.distinctUntilChanged();
length$.subscribe(val => console.log(val));
输出内容
在输入框中分别输入
1
12
123
123
12
123
----------输出结果为----------
1
12
123
12
123
-----即仅与流中上一个元素重复的内容不会输出
7. 合并类操作符
- merge:将两个流按各自时间顺序给叠加成一个流
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://unpkg.com/@reactivex/[email protected]/dist/global/Rx.js"></script>
</head>
<body>
<div>
<input id="length" type="number">
<input id="width" type="number">
</div>
</body>
</html>
const length = document.getElementById('length');
const width = document.getElementById('width');
const length$ = Rx.Observable
.fromEvent(length, 'keyup')
.pluck('target', 'value');
const width$ = Rx.Observable
.fromEvent(width, 'keyup')
.pluck('target', 'value');
const merged$ = Rx.Observable
.merge(length$, width$);
merged$.subscribe(val => console.log(val));
输出内容
在输入框中先后输入
1 空
1 2
13 2
13 24
134 24
1344 24
--------------------输出结果-----------------------
"1"
"2"
"13"
"24"
"134"
"1344"
- concat:先完成第一个流中所有发射内容,再进行第二个流的所有发射内容
const length = document.getElementById('length');
const width = document.getElementById('width');
const length$ = Rx.Observable
.fromEvent(length, 'keyup')
.pluck('target', 'value');
const width$ = Rx.Observable
.fromEvent(width, 'keyup')
.pluck('target', 'value');
const merged$ = Rx.Observable
.concat(length$, width$);
merged$.subscribe(val => console.log(val));
输出内容
在第一个框中输入的内容会一直输出到console中,第二个输入框中输入的内容会一直无法输出到console中
因为第一个框的事件是无穷的序列,所以第二个流永远无法执行到
- startWith:指定流发射的初始值,会先发射指定值。实际场合都是为了流填一个初始值,以免流没有初始值而报错。
const startWith$ = Rx.Observable
.from([1,2,3,4])
.startWith(0)
startWith$.subscribe(val => console.log(val));
输出内容
0
1
2
3
4
- combineLatest:简单的说两个流中有一个新元素生成的时候,就会在新流中生成一个新的元素
const length = document.getElementById('length');
const width = document.getElementById('width');
const length$ = Rx.Observable
.fromEvent(length, 'keyup')
.pluck('target', 'value');
const width$ = Rx.Observable
.fromEvent(width, 'keyup')
.pluck('target', 'value');
const combineLatest$ = Rx.Observable
.combineLatest(length$, width$, (l, w)=> {
return l*w;
})
combineLatest$.subscribe(val => console.log(val));
输出结果
会根据两个输入框输入的内容即是获得相乘的结果
- withLatestFrom:区别-withLatestFrom是以源事件流为基准,当基准流产生值的时候会去取另一个流的最新值放到新的流中【只有第一个流改变才会有输出,第二个流改变不会给新流增加内容】
- zip:区别-zip有对齐的特性,当两个流都有一个新的内容的时候,会组成新流中的一个元素【看弹珠图就可以理解,可以直接在jsbin中尝试】
const length = document.getElementById('length');
const width = document.getElementById('width');
const length$ = Rx.Observable
.fromEvent(length, 'keyup')
.pluck('target', 'value');
const width$ = Rx.Observable
.fromEvent(width, 'keyup')
.pluck('target', 'value');
const zip$ = Rx.Observable
.zip(length$, width$, (l, w)=> {
return l*w;
})
zip$.subscribe(val => console.log(val));
三.Observable的性质
- Observable有三种状态:next / error / complete 作为subscribe的三个方法参数,但这三个状态不一定都会达到,只是到对应状态的时候调用对应的方法
const interval$ = Rx.Observable.interval(1000).take(3);
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
- 特殊的状态: Never永不结束(如:计时器每隔几秒发送一些内容) / Empty(不发射内容但直接结束) / Throw(直接进入error状态)
// Never状态如下,会一直发射,因为不知道流什么时候结束
const interval$ = Rx.Observable
.interval(1000)
.filter( v => v%2===0) // 只保留是偶数的流数据
.do( v => console.log(`当前值是:${v}`))
.last();
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
// 手动抛出error,并在error状态函数中捕获并处理
const interval$ = Rx.Observable
.interval(1000)
.map(v => {
throw '抛出异常'; // 此时就会在error中输出
})
.take(4)
.reduce((x,y) => { // x为上一个return的值
return x+y;
});
interval$.subscribe(next => {
console.log(next);
}, error => {
console.log(error);
}, complete => {
console.log('Complete');
})
// 通常用于测试当中,使用官方的方式来创建never / error / empty 流
// 官方自带的never
const never$ = Rx.Observable.never();
// 官方自带的throw error
const throw$ = Rx.Observable.throw('出错了');
// 官方自带的empty()调用后悔直接调用complete方法中的内容
const empty$ = Rx.Observable.empty();