Open lei4519 opened 5 months ago
2021-03-01
好的程序员懂得如何从重复的工作中逃脱:
- 操作DOM时,发现了Jquery。 - 操作JS时,发现了lodash。 - 操作事件时,发现了Rx。
Rxjs 本身的 概念 并不复杂,简单点说就是对观察者模式的封装,观察者模式在前端领域大行其道,不管是使用框架还是原生 JS,你一定都体验过。
在我看来,Rxjs 的强大和难点主要体现对其近 120 个操作符的灵活运用。
可惜官网中对这些操作符的介绍晦涩难懂,这就导致了很多人明明理解 Rxjs 的概念,却苦于不懂的使用操作符而黯然离场。
本文总结自《深入浅出 Rxjs》一书,旨在于用最简洁、通俗易懂的方式来说明 Rxjs 常用操作符的作用。学习的同时,也可以做为平时快速查阅的索引列表。
subscribe
next
complete
需要注意流的完成和订阅时间,某些操作符必须等待流完成之后才会触发。 其实根据操作符的功能我们也可以大致推断出结果:如果一个操作符需要拿到所有数据做操作、判断,那一定是需要等到流完成之后才能进行。
需要注意流的完成和订阅时间,某些操作符必须等待流完成之后才会触发。
其实根据操作符的功能我们也可以大致推断出结果:如果一个操作符需要拿到所有数据做操作、判断,那一定是需要等到流完成之后才能进行。
创建流操作符最为流的起点,不存在复杂难懂的地方,这里只做简单的归类,具体使用查阅官网即可,不再赘述。
订阅多条流,将接收到的数据向下吐出。
首尾连接
当流全部完成时 concat 流结束。
concat(source1$, source2$)
先到先得
当流全部完成时 merge 流结束。
merge(source1$, source2$)
一对一合并(像拉链一样)
i
zip(source1$, source2$)
合并所有流的最后一个数据
所有的流都完成后,combineLatest 流才会完成。
combineLatest(source1$, source2$)
合并所有流的最后一个数据,功能同 combineLatest,区别在于:
withLatestFrom:当所有流准备完毕后(都有了最后值),只有调用 withLatestFrom 的流吐出数据才会向下吐出数据,其他流触发时仅记录最后值。
source1$.pipe(withLatesFrom(source2$, source3$))
胜者通吃
race(source1$, source2$)
在流的前面填充数据
source1$.pipe(startWith(1))
forkJoin(source1$, source2$)
当前流完成之后,统计流一共发出了多少个数据。
source$.pipe(count())
当前流完成之后,计算 最小值/最大值。
source$.pipe(max())
同数组用法,当前流完成之后,将接受的所有数据依次传入计算。
source$.pipe(reduce(() => {}, 0))
同数组,需要注意的是:如果条件都为 true,也要等到流完成才会吐出结果。
原因也很简单,如果流没有完成,那怎么保证后面的数据条件也为 true 呢。
source$.pipe(every(() => true / false))
同数组,注意点同 every
source$.pipe(find(() => true / false))
判断流是不是一个数据都没有吐出就完成了。
source$.pipe(isEmpty())
如果流满足 isEmpty,吐出默认值。
source$.pipe(defaultIfEmpty(1))
同数组
source$.pipe(filter(() => true / false))
取第一个满足条件的数据,如果不传入条件,就取第一个
source$.pipe(first(() => true / false))
取第一个满足条件的数据,如果不传入条件,就取最后一个,流完成才会触发。
source$.pipe(last(() => true / false))
拿够前 N 个就完成
N
source$.pipe(take(N))
拿够后 N 个就结束,因为是后几个所以只有流完成了才会将数据一次发出。
source$.pipe(takeLast(N))
给我传判断函数,什么时候结束你来定
source$.pipe(takeWhile(() => true / false))
给我一个流 (A),什么时候这个流 (A) 吐出数据了,我就完成
source$.pipe(takeUntil(timer(1000)))
跳过前 N 个数据
source$.pipe(skip(N))
给我传函数,跳过前几个你来定
source$.pipe(skipWhile(() => true / false))
给我一个流 (A),什么时候这个流 (A) 吐出数据了,我就不跳了
source$.pipe(skipUntil(timer(1000)))
source$.pipe(map(() => {}))
source$.pipe(mapTo("a"))
source$.pipe(pluck("v"))
对防抖、节流不了解的请自行查阅相关说明。
传入一个流 (A),对上游数据进行节流,直到流 (A) 吐出数据时结束节流向下传递数据,然后重复此过程
source$.pipe(throttle(interval(1000)))
根据时间 (ms) 节流
source$.pipe(throttleTime(1000))
传入一个流 (A),对上游数据进行防抖,直到流 (A) 吐出数据时结束防抖向下传递数据,然后重复此过程
source$.pipe(debounce(interval(1000)))
根据时间 (ms) 防抖
source$.pipe(debounceTime(1000))
audit 同 throttle,区别在于:
source$.pipe(audit(interval(1000)))
同上,不再赘述
source$.pipe(auditTime(1000))
传入一个流 (A),对上游数据吐出的最新数据进行缓存,直到流 (A) 吐出数据时从缓存中取出数据向下传递,然后重复此过程
source$.pipe(sample(interval(1000)))
根据时间 (ms) 取数
source$.pipe(sampleTime(1000))
所有元素去重,返回当前流中从来没有出现过的数据。
传入函数时,根据函数的返回值分配唯一 key。
source$.pipe(distinct()) Observable.of({ age: 4, name: "Foo" }).pipe(distinct((p) => p.name))
相邻元素去重,只返回与上一个数据不同的数据。
source$.pipe(distinctUntilChanged())
source$.pipe(distinctUntilKeyChanged("id"))
忽略上游的所有数据,当上游完成时,ignoreElements 也会完成。(我不关心你做了什么,只要告诉我完没完成就行)
source$.pipe(ignoreElements())
只获取上游数据发出的第 N 个数据。
第二个参数相当于默认值:当上游没发出第 N 个数据就结束时,发出这个参数给下游。
source$.pipe(elementAt(4, null))
source$.pipe(single(() => true / false))
缓存上游吐出的数据,到指定时间后吐出,然后重复。
source$.pipe(bufferTime(1000))
缓存上游吐出的数据,到指定个数后吐出,然后重复。
第二个参数用来控制每隔几个数据开启一次缓存区,不传时可能更符合我们的认知。
source$.pipe(bufferCount(10))
传入一个返回流 (A) 的工厂函数
流程如下:
randomSeconds = () => timer((Math.random() * 10000) | 0) source$.pipe(bufferWhen(randomSeconds))
第一个参数为开启缓存流 (O),第二个参数为返回关闭缓存流 (C) 的工厂函数
source$.pipe(bufferToggle(interval(1000), () => randomSeconds))
传入一个关闭流 (C),区别与 bufferWhen:传入的是流,而不是返回流的工厂函数。
触发订阅时,开始缓存,当关闭流 (C) 吐出数据时,将缓存的值向下传递并重新开始缓存。
source$.pipe(buffer(interval(1000)))
scan 和 reduce 的区别在于:
区别于其他流,scan 拥有了保存、记忆状态的能力。
source$.pipe(scan(() => {}, 0))
同 scan,但是返回的不是数据而是一个流。
source$.pipe(mergeScan(() => interval(1000)))
捕获错误
source$.pipe(catch(err => of('I', 'II', 'III', 'IV', 'V')))
传入数字 N,遇到错误时,重新订阅上游,重试 N 次结束。
source$.pipe(retry(3))
传入流 (A),遇到错误时,订阅流 (A),流 (A) 每吐出一次数据,就重试一次。流完成,retrywfhen 也完成。
source$.pipe(retryWhen((err) => interval(1000)))
source$.pipe(finally())
接收返回 Subject 的工厂函数,返回一个 hot observable(HO)
Subject
hot observable
当链接开始时,订阅上游获取数据,调用工厂函数拿到 Subject,上游吐出的数据通过 Subject 进行多播。
connect
refCount
unsubscribe
source$.pipe(multicast(() => new Subject()))
source$.pipe(publish())
基于 publish 的封装,返回调用 refCount 后的结果(看代码)
source$.pipe(share()) // 等同于 source$.pipe(publish().refCount())
当上游完成后,多播上游的最后一个数据并完成当前流。
source$.pipe(publishLast())
传入缓存数量 N,缓存上游最新的 N 个数据,当有新的订阅时,将缓存吐出。
source$.pipe(publishReplay(1))
缓存上游吐出的最新数据,当有新的订阅时,将最新值吐出。如果被订阅时上游从未吐出过数据,就吐出传入的默认值。
source$.pipe(publishBehavior(0))
如下代码示例,顶层的流吐出的并不是普通的数据,而是两个会产生数据的流,那么此时下游在接受时,就需要对上游吐出的流进行订阅获取数据,如下:
of(of(1, 2, 3), of(4, 5, 6)) .subscribe( ob => ob.subscribe((num) => { console.log(num) }) )
上面的代码只是简单的将数据从流中取出,如果我想对吐出的流运用前面讲的操作符应该怎么办?
cache = [] of(of(1, 2, 3), of(4, 5, 6)) .subscribe({ next: ob => cache.push(ob), complete: { concat(...cache).subscribe(console.log) zip(...cache).subscribe(console.log) } })
先不管上述实现是否合理,我们已经可以对上游吐出的流运用操作符了,但是这样实现未免也太过麻烦,所以 Rxjs 为我们封装了相关的操作符来帮我们实现上述的功能。
总结一下:高阶操作符操作的是流,普通操作符操作的是数据。
对应 concat,缓存高阶流吐出的每一个流,依次订阅,当所有流全部完成,concatAll 随之完成。
source$.pipe(concatAll())
对应 merge,订阅高阶流吐出的每一个流,任意流吐出数据,mergeAll 随之吐出数据。
source$.pipe(mergeAll())
对应 zip,订阅高阶流吐出的每一个流,合并这些流吐出的相同索引的数据向下传递。
source$.pipe(zipAll())
对应 combineLatest,订阅高阶流吐出的每一个流,合并所有流的最后值向下传递。
source$.pipe(combineAll())
切换流 - 喜新厌旧
高阶流每吐出一个流时,就会退订上一个吐出的流,订阅最新吐出的流。
source$.pipe(switch())
切换流 - 长相厮守
当高阶流吐出一个流时,订阅它。在这个流没有完成之前,忽略这期间高阶流吐出的所有的流。当这个流完成之后,等待订阅高阶流吐出的下一个流订阅,重复。
source$.pipe(exhaust())
看完例子,即知定义。
实现如下功能:
mousedown
mousemove
mousedown$ = formEvent(document, "mousedown") mousemove$ = formEvent(document, "mousemove") mousedown$.pipe( map(() => mousemove$), mergeAll() )
map
mergeAll
event
注:由于只有一个事件流,所以使用上面介绍的任意高阶合并操作符都是一样的效果。
mousedown$.pipe(mergeMap(() => mousemove$))
不难看出,所谓高阶 map,就是
concatMap = map + concatAll mergeMap = map + mergeAll switchMap = map + switch exhaustMap = map + exhaust concatMapTo = mapTo + concatAll mergeMapTo = mapTo + mergeAll switchMapTo = mapTo + switch
类似于 mergeMap,但是,所有传递给下游的数据,同时也会传递给自己,所以 expand 是一个递归操作符。
mergeMap
source$.pipe(expand((x) => (x === 8 ? EMPTY : x * 2)))
输出流,将上游传递进来的数据,根据 key 值分类,为每一个分类创建一个流传递给下游。
key 值由第一个函数参数来控制。
source$.pipe(groupBy((i) => i % 2))
groupBy 的简化版,传入判断条件,满足条件的放入第一个流中,不满足的放入第二个流中。
简单说:
source$.pipe(partition())
以上就是本文的全部内容了,希望你看了会有收获。
如果有不理解的部分,可以在评论区提出,大家一起成长进步。
祝大家早日拿下 Rxjs 这块难啃的骨头。
前言
好的程序员懂得如何从重复的工作中逃脱:
Rxjs 本身的 概念 并不复杂,简单点说就是对观察者模式的封装,观察者模式在前端领域大行其道,不管是使用框架还是原生 JS,你一定都体验过。
在我看来,Rxjs 的强大和难点主要体现对其近 120 个操作符的灵活运用。
可惜官网中对这些操作符的介绍晦涩难懂,这就导致了很多人明明理解 Rxjs 的概念,却苦于不懂的使用操作符而黯然离场。
本文总结自《深入浅出 Rxjs》一书,旨在于用最简洁、通俗易懂的方式来说明 Rxjs 常用操作符的作用。学习的同时,也可以做为平时快速查阅的索引列表。
阅读提醒
subscribe
next
complete
创建流操作符
创建流操作符最为流的起点,不存在复杂难懂的地方,这里只做简单的归类,具体使用查阅官网即可,不再赘述。
同步流
异步流
合并类操作符
订阅多条流,将接收到的数据向下吐出。
Concat
首尾连接
当流全部完成时 concat 流结束。
Merge
先到先得
当流全部完成时 merge 流结束。
Zip
一对一合并(像拉链一样)
i
次,将第i
次的数据合并成数组向下传递。combineLatest
合并所有流的最后一个数据
所有的流都完成后,combineLatest 流才会完成。
withLatestFrom
合并所有流的最后一个数据,功能同 combineLatest,区别在于:
withLatestFrom:当所有流准备完毕后(都有了最后值),只有调用 withLatestFrom 的流吐出数据才会向下吐出数据,其他流触发时仅记录最后值。
Race
胜者通吃
startWith
在流的前面填充数据
forkJoin
合并所有流的最后一个数据
辅助类操作符
Count
当前流完成之后,统计流一共发出了多少个数据。
mix/max
当前流完成之后,计算 最小值/最大值。
Reduce
同数组用法,当前流完成之后,将接受的所有数据依次传入计算。
布尔类操作符
Every
同数组,需要注意的是:如果条件都为 true,也要等到流完成才会吐出结果。
原因也很简单,如果流没有完成,那怎么保证后面的数据条件也为 true 呢。
find、findIndex
同数组,注意点同 every
isEmpty
判断流是不是一个数据都没有吐出就完成了。
defaultIfEmpty
如果流满足 isEmpty,吐出默认值。
过滤类操作符
Filter
同数组
First
取第一个满足条件的数据,如果不传入条件,就取第一个
Last
取第一个满足条件的数据,如果不传入条件,就取最后一个,流完成才会触发。
Take
拿够前
N
个就完成takeLast
拿够后
N
个就结束,因为是后几个所以只有流完成了才会将数据一次发出。takeWhile
给我传判断函数,什么时候结束你来定
takeUntil
给我一个流 (A),什么时候这个流 (A) 吐出数据了,我就完成
Skip
跳过前
N
个数据skipWhile
给我传函数,跳过前几个你来定
skipUntil
给我一个流 (A),什么时候这个流 (A) 吐出数据了,我就不跳了
转化类操作符
Map
mapTo
Pluck
有损回压控制
Throttle
传入一个流 (A),对上游数据进行节流,直到流 (A) 吐出数据时结束节流向下传递数据,然后重复此过程
throttleTime
根据时间 (ms) 节流
Debounce
传入一个流 (A),对上游数据进行防抖,直到流 (A) 吐出数据时结束防抖向下传递数据,然后重复此过程
debounceTime
根据时间 (ms) 防抖
Audit
audit 同 throttle,区别在于:
auditTime
同上,不再赘述
Sample
传入一个流 (A),对上游数据吐出的最新数据进行缓存,直到流 (A) 吐出数据时从缓存中取出数据向下传递,然后重复此过程
sampleTime
根据时间 (ms) 取数
Distinct
所有元素去重,返回当前流中从来没有出现过的数据。
传入函数时,根据函数的返回值分配唯一 key。
distinctUntilChanged
相邻元素去重,只返回与上一个数据不同的数据。
传入函数时,根据函数的返回值分配唯一 key。
distinctUntilKeyChanged
ignoreElements
忽略上游的所有数据,当上游完成时,ignoreElements 也会完成。(我不关心你做了什么,只要告诉我完没完成就行)
elementAt
只获取上游数据发出的第 N 个数据。
第二个参数相当于默认值:当上游没发出第 N 个数据就结束时,发出这个参数给下游。
Single
无损回压控制
bufferTime、windowTime
缓存上游吐出的数据,到指定时间后吐出,然后重复。
bufferCount、windowCount
缓存上游吐出的数据,到指定个数后吐出,然后重复。
第二个参数用来控制每隔几个数据开启一次缓存区,不传时可能更符合我们的认知。
bufferWhen、windowWhen
传入一个返回流 (A) 的工厂函数
流程如下:
bufferToggle、windowToggle
第一个参数为开启缓存流 (O),第二个参数为返回关闭缓存流 (C) 的工厂函数
流程如下:
buffer、window
传入一个关闭流 (C),区别与 bufferWhen:传入的是流,而不是返回流的工厂函数。
触发订阅时,开始缓存,当关闭流 (C) 吐出数据时,将缓存的值向下传递并重新开始缓存。
累计数据
Scan
scan 和 reduce 的区别在于:
区别于其他流,scan 拥有了保存、记忆状态的能力。
mergeScan
同 scan,但是返回的不是数据而是一个流。
错误处理
Catch
捕获错误
Retry
传入数字
N
,遇到错误时,重新订阅上游,重试N
次结束。retryWhen
传入流 (A),遇到错误时,订阅流 (A),流 (A) 每吐出一次数据,就重试一次。流完成,retrywfhen 也完成。
Finally
多播操作符
Multicast
接收返回
Subject
的工厂函数,返回一个hot observable
(HO)当链接开始时,订阅上游获取数据,调用工厂函数拿到
Subject
,上游吐出的数据通过Subject
进行多播。connect
、refCount
方法。connect
才会真正开始订阅顶流并发出数据。refCount
则会根据subscribe
数量自动进行connect
和unsubscribe
操作。Publish
Share
基于 publish 的封装,返回调用 refCount 后的结果(看代码)
publishLast
当上游完成后,多播上游的最后一个数据并完成当前流。
publishReplay
传入缓存数量
N
,缓存上游最新的N
个数据,当有新的订阅时,将缓存吐出。publishBehavior
缓存上游吐出的最新数据,当有新的订阅时,将最新值吐出。如果被订阅时上游从未吐出过数据,就吐出传入的默认值。
高阶合并类操作符
如下代码示例,顶层的流吐出的并不是普通的数据,而是两个会产生数据的流,那么此时下游在接受时,就需要对上游吐出的流进行订阅获取数据,如下:
上面的代码只是简单的将数据从流中取出,如果我想对吐出的流运用前面讲的操作符应该怎么办?
先不管上述实现是否合理,我们已经可以对上游吐出的流运用操作符了,但是这样实现未免也太过麻烦,所以 Rxjs 为我们封装了相关的操作符来帮我们实现上述的功能。
总结一下:高阶操作符操作的是流,普通操作符操作的是数据。
concatAll
对应 concat,缓存高阶流吐出的每一个流,依次订阅,当所有流全部完成,concatAll 随之完成。
mergeAll
对应 merge,订阅高阶流吐出的每一个流,任意流吐出数据,mergeAll 随之吐出数据。
zipAll
对应 zip,订阅高阶流吐出的每一个流,合并这些流吐出的相同索引的数据向下传递。
combineAll
对应 combineLatest,订阅高阶流吐出的每一个流,合并所有流的最后值向下传递。
高阶切换类操作符
Switch
切换流 - 喜新厌旧
高阶流每吐出一个流时,就会退订上一个吐出的流,订阅最新吐出的流。
Exhaust
切换流 - 长相厮守
当高阶流吐出一个流时,订阅它。在这个流没有完成之前,忽略这期间高阶流吐出的所有的流。当这个流完成之后,等待订阅高阶流吐出的下一个流订阅,重复。
高阶 Map 操作符
看完例子,即知定义。
例子
实现如下功能:
mousedown
事件触发后,监听mousemove
事件普通实现
mousedown
事件触发后,使用map
操作符,将向下吐出的数据转换成mousemove
事件流。mergeAll
操作符帮我们将流中的数据展开。mousemove
的event
事件对象了。注:由于只有一个事件流,所以使用上面介绍的任意高阶合并操作符都是一样的效果。
高阶 Map 实现
不难看出,所谓高阶 map,就是
Expand
类似于
mergeMap
,但是,所有传递给下游的数据,同时也会传递给自己,所以 expand 是一个递归操作符。数据分组
groupBy
输出流,将上游传递进来的数据,根据 key 值分类,为每一个分类创建一个流传递给下游。
key 值由第一个函数参数来控制。
Partition
groupBy 的简化版,传入判断条件,满足条件的放入第一个流中,不满足的放入第二个流中。
简单说:
结语
以上就是本文的全部内容了,希望你看了会有收获。
如果有不理解的部分,可以在评论区提出,大家一起成长进步。
祝大家早日拿下 Rxjs 这块难啃的骨头。
参考资料