前言
很多时候,上游Observable对象吐出的数据,并不都是下游关⼼的, 这时我们需要过滤掉下游不关⼼的数据,只保留下游感兴趣的数据,实现 这类功能的⼯具就是过滤类操作符。
过滤类操作符最基本的功能就是对⼀个给定的数据流中每个数据判断 是否满⾜某个条件,如果满⾜条件就可以传递给下游,否则就抛弃掉。
- 过滤掉不满足判定条件的数据 —— filter
- 获得满足判定条件的第一个数据 —— first
- 获得满足判定条件的最后一个数据 —— last
- 从数据流中选取最先出现的若干数据 —— take
- 从数据流中选取最后出现的若干数据 —— takeLast
- 从数据流中选取数据直到某种情况发生 —— takeWhile和takeUntil
- 从数据流中忽略最先出现的若干数据 —— skip
- 从数据流中忽略数据直到某种情况发生 —— skipwhile和skipUntil
- 基于时间的数据流量筛选 —— throttleTime、debounceTime和auditTime
- 基于数据内容的数据流量筛选 —— throttle debounce和audit
- 基于采样方式的数据流量筛选 —— sample和sampleTime
- 删除重复的数据 —— distnct
- 删除重复的连续数据 —— distnctUntilChanged和distinctUntilKeyChanged
- 忽略数据流中的所有数据 —— ignoreElements
- 只选取指定出现位置的数据 —— elementAt
- 判断是否只有一个数据满足判定条件 —— single
一、操作符介绍
1.filter 过滤掉不满足判定条件的数据
const { range } = rxjs;
const { filter } = rxjs.operators;
const source$ = range(1, 5);
const even$ = source$.pipe(
filter(x => x % 2 === 0)
)
even$.subscribe(
console.log,
);
// 2 4
在上⾯的代码中,⾸先利⽤range产⽣1到5之间所有的正整数,然后通 过filter来过滤掉不满⾜偶数判定条件的数字。
使⽤filter产⽣的Observable对象,产⽣数据的时机和上游是⼀致的,当 上游产⽣数据的时候,只要这个数据满⾜判定条件,就会⽴刻被同步传给 下游。
filter 还是很好理解的,和 js 中的filter除了操作的源不同都是一样的。
2.first 获得满足判定条件的第一个数据
const { of } = rxjs;
const { first } = rxjs.operators;
const source$ = of(3, 1, 4, 1, 5, 9);
const first$ = source$.pipe(
first(
x => x % 2 === 0,
)
)
first$.subscribe(
res => console.log(res),
);
// 4
与之相似的还有一个 last 顾名思义就是找到最后一个满足判定数据的条件。比较容易理解,不过多阐述了。
注意: 和first不同的是,last⽆论如何都要等到上游Observable完结的时候才 吐出数据,因为上游Observable完结之前,last也⽆从知道是不是拿到了“最 后⼀个”数据。
3.take 选取最先出现的若干数据
take就是“拿”,从上游Observable拿数据,拿够了就完结,⾄于怎么 算“拿够”,由take的参数来决定,take只⽀持⼀个参数count,也就是限定拿 上游Observable的数据数量。
const { interval } = rxjs;
const { take } = rxjs.operators;
const source$ = interval(1000);
const last$ = source$.take(3);
last$.subscribe(
res => console.log(res),
);
// 0 1 2
虽然source$是⼀个永不完结的数据流,但是take的参数3限定了它只拿 3个,三秒之后,take产⽣的last$就完成任务,⽴刻完结。
4.takeLast 选取最后出现的若干数据
take相当于⼀个可以获取多个数据的first,那么takeLast相当于⼀个可 以获取多个数据的last。和last⼀样,takeLast只有在上游数据完结的时候才 能决定“最后”的数据是哪些,在吐出这些数据之后⽴刻完结。
如果上游在⼀段时间范围内产⽣的数据,那么就必须要等到上游完结 takeLast产⽣的Observable对象才产⽣数据。
const { interval } = rxjs;
const { take, takeLast } = rxjs.operators;
const source$ = interval(1000);
const take$ = source$.pipe(
take(5)
)
const last3$ = take$.pipe(
takeLast(3)
)
last3$.subscribe(
res => console.log(res),
);
// 2 3 4
take的作⽤是获取上游的数据,只要没有超过给定的数量限制,上游 产⽣⼀个数据,take都会⽴刻转⼿给下游。所以,弹珠图上take产⽣的 Observable对象数据产⽣时刻和source$是⼀致的;takeLast只有确定上游数 据完结的时候才能产⽣数据,⽽且是⼀次性产⽣所有数据,即takeLast在 take产⽣的Observable对象完结时把2、3、4数据⼀次性传给下游。
在上⾯的代码中,如果不使⽤take,直接让source$成为takeLast的上 游,那么takeLast产⽣的Observable对象永远不会产⽣数据,也永远不会完 结,因为它等不到上游的“最后数据”。
5.takeWhile 选取数据直到某种情况发生
takeWhile接受⼀个判定函数作为参数,这个判定函数有两个 参数,分别代表上游的数据和对应的序号,takeWhile会吐出上游数据,直 到判定函数返回false,只要遇到第⼀个判定函数返回false的情况, takeWhile产⽣的Observable就完结。
const { range } = rxjs;
const { take, takeWhile } = rxjs.operators;
const source$ = range(1, 100);
const takeWhile$ = source$.pipe(
takeWhile(
value => value % 2 === 0
)
)
takeWhile$.subscribe(
res => console.log(res),
);
// 什么都没有
在上⾯的例⼦中,takeWhile$⼀个数据都不吐出就完结,因为上游 source$吐出的第⼀个数据是1,不满⾜判定条件。
6.takeUntil 选取数据直到某种情况发生
takeUntil 让我们可以⽤Observable对象来控制另⼀个Observable对象的数据产⽣。
takeUntil的神奇特点就是其参数是另⼀个Observable对象notifier,由这 个notifier来控制什么时候结束从上游Observable拿数据,因为notifier本⾝又 是⼀个Observable,吐出数据可以⾮常灵活,这就意味着可以利⽤⾮常灵 活的规则⽤takeUntil产⽣下游Observable。
使⽤takeUntil,上游的数据直接转⼿给下游,直到(Until)参数 notifier吐出⼀个数据或者完结,这个notifier就像⼀个⽔龙头开关,控制着 takeUntil产⽣的Observable对象,⼀开始这个⽔龙头开关是打开状态,上游 的数据像⽔⼀样直接流到下游,但是notifier只要⼀有动静,⽔龙头开关⽴ 刻关闭,上游通往下游的通道也就关闭了。
const { timer, interval } = rxjs;
const { take, takeUntil } = rxjs.operators;
const source$ = interval(1000);
const notifier$ = timer(2500);
const takeUntil$ = source$.pipe(
takeUntil(notifier$)
)
takeUntil$.subscribe(
res => console.log(res),
);
// 0 1
在上⾯的代码中,notifier$的作⽤就是在未来给source$⼀个“通知”,切 断⽔龙头的开关,所以使⽤timer这个操作符,在2.5秒之后动⼿。
作为takeUntil的notifier参数如果在吐出数据或者完结之前抛出 了错误,那takeUntil也会把这个错误抛给下游,从⽽关闭了上下游之间的 通道。
7.skip 忽略最先出现的若干数据
从上游Observable获取多个数据,除了“只拿满⾜条件的前N个”这种⽅ 式,还有“跳过前N个之后全拿”这种场景。
skip接受⼀个count参数,会默默忽略上游Observable吐出的前count个 数据,然后,从第count+1个数据开始,就和上游Observable保持⼀致了。
上游Observable吐出什么数据,skip产⽣的Observable就吐出什么数据,上 游Observable完结,skip产⽣的Observable跟着完结。当然,如果上游吐出 的数据不够count个,那skip产⽣的Observable就会在上游Observable完结的 时候⽴刻完结。
const { interval } = rxjs;
const { skip, takeUntil } = rxjs.operators;
const source$ = interval(1000);
const skip$ = source$.pipe(
skip(3)
);
skip$.subscribe(
res => console.log(res),
);
// 3 4 5 6 ......
在等待3秒之后,skip吐出的前3个数据0、1、2,就是被skip掉了。
7.skipWhile 忽略数据直到某种情况发生
skip和take采⽤的是正好相反的两种过滤策略,和take⼀样,skip也有 两个兄弟,分别是skipWhile和shipUntil。
下⾯是使⽤skipWhile的⽰例代码:
const { interval } = rxjs;
const { skipWhile } = rxjs.operators;
const source$ = interval(1000);
const skipWhile$ = source$.pipe(
skipWhile(value => value % 2 === 0)
)
skipWhile$.subscribe(
res => console.log(res),
);
skipWhile的参数可以判断⼀个数据是否为偶数,所以skipWhile 会跳过数据流前⾯的偶数数据。
只要 某个数据让判定函数返回false,之后skipWhile就不做跳过的动作,所有的 上游数据都转⼿给下游。
8.skipUntil 忽略数据直到某种情况发生
const { interval, timer } = rxjs;
const { skipUntil } = rxjs.operators;
const source = interval(1000);
const example = source.pipe(skipUntil(timer(6000)));
const subscribe = example.subscribe(val => console.log(val));
// 5 6 7 8 9 10 ......
skipUntil是跳过另一个observable还在产生数据的阶段。
回压控制
“回压”(Back Pressure)也称为“背压”,是⼀个源⾃于传统⼯程中的概 念,在⼀个传输管道中,液体或者⽓体应该朝某⼀个⽅向流动,但是前⽅ 管道⼜径变⼩,这时候液体或者⽓体就会在管道中淤积,产⽣⼀个和流动 ⽅向相反的压⼒,因为这个压⼒的⽅向是往回⾛的,所以称为回压。
在RxJS的世界中,数据管道就像是现实世界中的管道,数据就像是现 实中的液体或者⽓体,如果数据管道中某⼀个环节处理数据的速度跟不上 数据涌⼊的速度,上游⽆法把数据推送给下游,就会在缓冲区中积压数 据,这就相当于对上游施加了压⼒,这就是RxJS世界中的“回压”。
当zip合并两个 Observable对象,其中⼀个A产⽣数据速度快,另⼀个B产⽣数据速度慢, 因为每⼀个来⾃A的数据都要⼀个B中的数据⼀对⼀配对,那么zip就不得 不缓存A推送的数据,时间⼀长,zip需要缓存的A产⽣的数据就会越来越 多,这就是回压的问题。
回压控制操作符包含以下这些:
- throttle
- debounce
- audit
- sample
- throttleTime
- debounceTime
- auditTime
- sampleTime
9.throttle和debounce 基于时间的数据流量筛选
为了理解RxJS的throttle和debounce,让我们先理解throttleTime和 debounceTime。
这两个操作符名字包含Time,参数也就是代表毫秒数的时间。
throttleTime的作⽤是限制在duration时间范围内,从上游传递给下游数 据的个数;debounceTime的作⽤是让传递给下游的数据间隔不能⼩于给定 的时间dueTime。对于有点经验的前端来说,这两个概念并不陌生。
使⽤throttleTime的⽰例代码如下所⽰:
const { interval, timer } = rxjs;
const { throttleTime } = rxjs.operators;
const source$ = interval(1000);
const result$ = source$.pipe(
throttleTime(2000)
)
result$.subscribe(
console.log,
null,
() => console.log('complete')
);
// 0 3 6 9 ... 12 15 ...
其中,throttleTime的参数duration是2000,代表上游source$中产⽣的数 据,2000毫秒之内只有⼀个数据会传给下游。
把throttleTime换成debounceTime:
const { interval, timer } = rxjs;
const { debounceTime } = rxjs.operators;
const source$ = interval(1000);
const result$ = source$.pipe(
debounceTime(2000)
)
result$.subscribe(
console.log,
null,
() => console.log('complete')
);
// 什么都没有
运⾏这样的代码不会产⽣任何结果。
因为debounceTime要等上游在dueTime毫秒范围内不产⽣任何其他数据 时才把这个数据传递给下游,如果在dueTime范围内上游产⽣了新的数 据,那么debounceTime就又要重新开始计时。
了解了throttleTime这debounceTime之后,再来了解throttle和debounce 就不难了。
throttle的参数是⼀个函数,这个函数应该返回⼀个 Observable对象,这个Observable对象可以决定throttle如何控制上游和下游 之间的流量。
const { interval, timer } = rxjs;
const { throttle } = rxjs.operators;
const source$ = interval(1000);
const durationSelector = (value) => {
return timer(2000);
};
const result$ = source$.pipe(
throttle(durationSelector)
)
result$.subscribe(
console.log,
null,
() => console.log('complete')
);
// 0 3 6 9 ... 12 15 ...
会发现throttle输出和throttleTime是一样的,他们的区别只不过是一个参数为Observable 一个为时间。debounce和 debounceTime 是一样的不过多阐述了。
10.auditTime和audit 基于时间的数据流量筛选
可以认为audit是做throttle类似的⼯作,不同的是在“节流 时间”范围内,throttle把第⼀个数据传给下游,audit是把最后⼀个数据传给 下游。
const { interval, timer } = rxjs;
const { auditTime } = rxjs.operators;
const source$ = interval(1000);
const result$ = source$.pipe(
auditTime(2000)
)
result$.subscribe(
console.log,
null,
() => console.log('complete')
);
// 2 5 8 ... 10 12 14 16 ...
如果把throttleTime认为是立即执行而auditTime 是非立即执行,就容易理解很多了。audit也不过多解释理解了auditTime就能理解audit 只不过是接受的参数类型不一致。
根据数据序列做回压控制
1.distinct 舍弃重复数据
distinct的含义就是“不同”,RxJS中这个操作符的作⽤就是只返回从没 出现过的数据,上游同样的数据只有第⼀次产⽣时会传给下游,其余的都 被舍弃掉了。
const { of } = rxjs;
const { distinct, } = rxjs.operators;
const source$ = of(0, 1, 1, 2, 0, 0, 1, 3, 3);
const distinct$ = source$.pipe(
distinct()
)
distinct$.subscribe(
console.log,
null,
() => console.log('complete')
);
// 0,1,2,3,
distinct判断两个数据是否相同就是⽤JavaScript的===操作符,对于普 通的数值和字符串数据,distinct默认的⽐较⽅式⾜够,但是对于普通 JavaScript对象,===操作符就没有什么意义了,所以,distinct提供了⼀个 函数参数keySelector,⽤于定制distinct应该⽐对什么样的属性。
const { of } = rxjs;
const { distinct, } = rxjs.operators;
const source$ = of({a:'1',b:'2'},{a:'1',b:'2'},{a:'2',b:'2'});
const distinct$ = source$.pipe(
distinct( x => x.a)
)
distinct$.subscribe(
console.log,
null,
() => console.log('complete')
);
// {a:'1',b:'2'} {a:'2',b:'2'}
实际上 distinct() 会在背地里建立一个 Set,当接收到元素时会先去判断 Set 内是否有相同的值,如果有就不送出,如果没有则存到 Set 并送出。所以记得尽量不要直接把 distinct 用在一个无限的 observable 里,这样很可能会让 Set 越来越大,有可能导致内存泄漏,建议大家可以放第二个参数或用 distinctUntilChanged$。
2.distinctUntilChanged 只有当当前值与之前最后一个值不同时才将其发出。
distinctUntilChanged的⼯作和distinct类似,也是淘汰掉重复的数据,但 distinct-UntilChanged拿到⼀个数据不是和⼀个“唯⼀数据集合”⽐较,⽽是 直接和上⼀个数据⽐较,也就是说,这个操作符要保存上游产⽣的上⼀个 数据就⾜够,当然,也就没有了distinct潜在的内存泄露问题。
const { of, interval } = rxjs;
const { distinctUntilChanged, } = rxjs.operators;
const source$ = of(0, 1, 1, 2, 0, 0, 1, 3, 3);
const distinctUntilChanged$ = source$.pipe(
distinct()
);
distinctUntilChanged$.subscribe(
console.log,
null,
() => console.log('complete') // 0 1 2 0 1 3
);
可以看到,结果中出现了重复数据,⽐如1,出现了两次,但是在上游 连续出现1的部分,都只有⼀个数据1进⼊了下游,出现了两处1,是因为这 两处1之间有其他数据产⽣⽽已。
distinctUntilChanged 和 distinct 的区别是, distinct 是整个序列不会重复,而其另一个是只会和上一个数据作比较。
小结
本文介绍RxJS中过滤数据的⽅法,在数据管道中,对数据很重要的⼀ 部分操作就是把不相关的数据清理掉,这就是过滤类操作符的⼯作。
在数据管道中,当数据产⽣的速度过快,超过下游处理能⼒时,就会 产⽣回压现象。数据过滤是进⾏回压控制的最简单⽅法,通过抛弃⼀些数 据来缓解压⼒。但是具体抛弃哪些数据,需要根据不同应⽤场景来决定使 ⽤什么样的过滤类操作符。
参考《深入浅出RxJs》——程墨